oficina / meet Public
Calls native design
The first Oficina native design pass uses Calls' existing teal identity, quiet surfaces, clear type hierarchy and native controls. Apple's communication and productivity apps are references for interaction quality; Oficina retains its own artwork, wording and colour. Mac and phone share the joining ceremony while their call layouts suit the available space.
First pass
CallsLobbyrenders welcome, browser approval, ready-to-join and error states on Mac, iPhone and iPad. The confirmation code and browser approval remain part of the existing authentication flow. Signed out, the welcome is the server's name, one prominent Sign in (the suite's label, exactly) and a secondary Join a call as a guest; there is no address field until the person is signed in. A link waiting on a sign-in names the server it is on.GuestJoinView(a sheet on both platforms) is one invite-link field with a Paste control, inline plain-words validation, Join and Cancel.ServerSettingsSectionis the Calls server setting: a section in the Mac's Settings scene after the mini-window settings, and the whole of the iPhone's settings sheet, reached by the home screen's gear.MacCallViewdefaults to a remote participant filling the stage with a local thumbnail. Speaker/Grid switches layouts (Focus until 0.4.0; see below); a pin menu chooses what is on the stage in group calls. Video crops to fill while preserving its aspect ratio. Chat starts hidden and can open as a sidebar or a separate window; its draft survives moving and closing. The compact, coloured icon controls remain below the stage. On iPhone, chat remains a sheet and controls remain in the bottom safe area.- Video names sit on an opaque dark label over the image. Microphone and camera actions use both text and changing symbols, so colour is never the only cue.
CallsAppearanceandCallControllive in CallsKit for both Apple clients. These are Calls presentation choices. General native infrastructure belongs in Rastrillo Native; Oficina's shared visual components belong in Estilo as subsequent apps establish what they need in common.
Visual review
After swift build --package-path native/apple, render the shipping lobby:
native/apple/.build/debug/CallsInterop --lobby-snapshots native/build/polishThis generates light and dark welcome, approval, home, ready, error, expired, link-on-another-server and visiting fixtures, the signed-out home and the settings sheet at iPhone width (phone-*), the guest screen empty and with an invalid link (guest-*), and the Mac Settings window (settings-*), without a server, real account or device permissions. Models read the server from a throwaway defaults suite. The off-screen window is never key, so prominent buttons draw untinted in these fixtures. The fixture entry point is only in CallsInterop, never the application.
TestNativeMacChat also captures native/build/call-focus.png, call-chat.png and call-grid.png using the shipping Mac call view and messages received from a real browser participant. The off-screen bitmap capture does not capture Metal video frames; media continuity is verified separately by the existing decoded-frame/audio counters.
The first pass was checked with Mac and iOS Simulator builds, the iPhone invalid-link UI test, and native/browser authentication, media, chat and sharing checks. Lobby review includes light/dark Mac states and iPhone at standard and accessibility text sizes. Physical-device call controls, VoiceOver, camera and microphone behaviour still need hands-on acceptance.
Next iterations
Camera preview and device selection before joining, remote camera-off signalling, automatic speaker selection, and keyboard commands are follow-up product work. Those should be designed alongside the relevant media and lifecycle behaviour.
Run swift test --package-path native/apple for chat-window lifecycle and draft retention checks. Window closure never invokes the meeting leave action.
iPhone and iPad call treatment · 2026-09-12
The iOS call stage now defaults to remote focus with a local thumbnail, offers Focus/Grid and participant selection, and uses the same compact colored icon control vocabulary. Chat is a native sheet with unread count and a retained draft. iOS continues to end calls when backgrounded, except while sharing the screen; general background-call continuity remains follow-up media work.
Safari member/guest sign-in and the real iPhone simulator native-media tests passed after this change. The signed development app was built and installed on Paul's paired iPhone. That installation does not substitute for a physical camera/microphone and interruption acceptance call.
Full-window call · 2026-09-25 (0.3.4)
The call fills the window (Mac) or the screen (iPhone), with the controls over it on demand. Call layout: Full window (new) / Classic — Calls → Settings… on Mac, the gear's sheet on iPhone — keeps the previous layout one choice away; Full window is the default. The mini window and tab are unchanged.
- When the controls show is
ControlsVisibility(CallChrome.swift), one state machine for both apps with the time passed in, so tests drive it. Mac: pointer movement shows them, 3 s still hides them (pointerDelay; 1.5 s in 0.3.4 was too quick), leaving the window hides them 1.5 s later (pointerExitDelay, never later than they were due anyway), and the pointer hides with them over the picture (NSCursor.setHiddenUntilMouseMoves). iPhone: a tap toggles, 3 s after the last touch hides (touchDelay). They stay up while the pointer is on the bar, a menu, popover, sheet or panel is open (AppKit's menu-tracking and window-sheet notifications cover the ones SwiftUI does not report), a bar control has keyboard focus, VoiceOver is running, or an alert shows. Tab reveals them. Space and ⌘⇧M work with them hidden. Fades are opacity only, eased out: 0.35 s going, 0.2 s coming back; Reduce Motion shortens them to a 0.1 s opacity change and panels fade instead of sliding. - The stage is edge to edge: square tiles for Focus and a screen, a grid that fits every tile without scrolling until they would be under 200 pt (
GridFit). On Mac there is no title bar while the call is full window (0.3.5): the picture goes to the window's top edge, the traffic lights sit over it and fade with the controls (not in full screen, where the system shows them), a shade behind them keeps them legible, and a 32 pt strip across the top drags the window and double-clicks as the person's title-bar setting says (WindowDragStrip,TitleBarDoubleClick). Since 0.3.7 the picture does too, as in FaceTime (WindowMove): a drag anywhere on the stage that is not a control, a panel or the self view moves the window once it has gone 4 pt (less is a click, which shows the controls), and a double click there does what the title bar's would. Resizing is AppKit's own, from every edge and corner, the top included. The title pill sits to the right of the traffic lights and the notices in a column beneath it.CallWindowChromesets the window's title bar, watches it so SwiftUI cannot quietly restore it mid-call, and puts it back for the lobby and Classic. Names are small capsules with the muted mark (shape and word) inside, lifted clear of the bar while it shows, visible while it does not. - The self view floats in Focus, draggable, settling in the corner it was flicked towards (
PiPLayout), kept clear of the bar, an open chat and the camera strip beside a shared screen, and remembered. It always says Muted when you are. VoiceOver has "Move self view to …" actions. In Grid you are a tile, as before. - Title and notices. Title and status go with the controls. Reconnecting, waiting to be let in, full, ended (with Join again), problems (dismissible), "You are sharing your screen" and each person waiting with Let in stay until resolved (
CallNotices); the relay-floor line fades. - Over the picture (0.3.5): one dark system, whatever the system appearance, because what is underneath is video (
CallOverlay,CallOverlaySurface,CallOverlayButtonStyle). Every surface — the bar, the title pill, notices, name labels, the iPhone's round buttons — is black at 70 % over a dark blur with a hairline edge; the black alone holds the contrast, so white on it is at least 8.5:1 even over a white wall. Buttons are white symbols on a light fill (5.5:1 worst case); a control that is on (sharing, a panel open) is near-white with a dark symbol; microphone or camera off is amber with the slashed symbol; Leave is a deeper red than the system's (5.0:1 for its symbol).CallOverlayContrastTestsholds the ratios over white, black and grey. Classic keeps its own look. - Mac bar: a dark bar along the bottom — microphone, camera, share, devices, layout (Focus/Grid and who is on the stage, in a popover), share link — with chat and Leave on the right. Devices is a floating panel over the stage above the bar, not a column that shrinks it.
- Mac chat is an overlay sidebar sliding over the stage's trailing edge, with the existing pop-out to a separate window kept. Chosen over the window alone because the window is window management mid-call on a laptop screen, while the sidebar is one click and leaves the picture its full size underneath; anyone who wants it beside the call still has the window.
- iPhone bar: a kebab (More controls) on the left and End call on the right. The kebab opens a sheet: mute, camera, flip camera, share screen (the system broadcast picker, which is why it is a sheet and not a menu), devices, Mic Mode, layout, share link, chat and Reduce background noise. The status bar and home indicator hide with the controls.
-CallsMicBesideMenu YESputs a microphone button beside the kebab, off by default.
Snapshots: CallsInterop --call-snapshots <dir> (also part of --lobby-snapshots) renders the Mac call, both layouts, light and dark. The 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 valuecallOverlayGlassturns 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 as a halo behind the name label, bright as the voice (
SpeakingGlow, andjudgeGlow/glowStylein room.js). The halo redraws on a timeline only until it arrives. "No sound from …" and the microphone warnings stay as words by the name; the Devices panel keeps a compact meter. The teal halo is superseded by the warm rim (0.4.3, below).
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).
The warm rim · 2026-09-28 (0.4.3)
Supersedes the teal halo of 0.3.7's speaking glow. After three rounds of HTML mock-ups (speaking-indicator-options/, served at calls-led-options.bds.oncarlos.com) Paul chose T5, "Warm soft breath", and it is drawn exactly as that mock-up's box-shadow recipe, on Mac, iPhone, web and the mini window's tab (WarmRim, TileSpeakingName; glowStyle and .calls-tile-name::after on the web).
Over the name's own glass and in its shape, where on is 0 or 1 and lvl the smoothed level 0…1 (-42 to -12 dBFS, no floor any more):
| Layer | Drawn | Colour | Opacity | | --- | --- | --- | --- | | Rim | 1 pt inside the edge | hsl(38°, 80 %, 82 %) | on × (0.10 + lvl × 0.45) | | Top highlight | none in T5 | — | 0 | | Inner light | inset blur 3 + lvl × 6 pt | hsl(38°, 80 %, 72 %) | on × (0.06 + lvl × 0.30) | | Outer breath | blur 1 + lvl × 7 pt, outside only | hsl(38°, 80 %, 66 %) | on × lvl × 0.25 |
Blurs are CSS box-shadow radii; SwiftUI draws them at half (its radius is the Gaussian's deviation). The timing is 0.3.7's: lit once speech has kept up 0.15 s, 50 ms attack, 300 ms release. Once no longer lit, on holds until the level has fallen under 0.05 (SpeakingGlow.outBelow; on the web --on waits out the release), as the mock-up's does, so the rim fades rather than vanishing. Reduce Motion: a steady lvl of 0.6 while lit, and nothing moves. Only the overlay observes the level, and it redraws only until it arrives.
Snapshots: CallsInterop --glass-snapshots <dir> writes mac-rim-<level>.png (WarmRimGallery: the mock-up's dark and bright tiles, white, and the busy frame); the iPhone's are CallsPhoneUITests.testWarmRimSnapshots (-CallsFixtureRimGallery <level>).
Speaker view and your own order · 2026-09-27 (0.4.0)
Paul asked whether he could rearrange the people in Grid, and for a Speaker view: the speaker in the main window, everyone else underneath.
- Two layouts: Speaker and Grid. Speaker replaced Focus. Focus was "one person on the stage, chosen by hand", which is Speaker with that person pinned, so keeping both would have been two names for one thing with a worse default. Choosing a pin (from the layout menu, a tile's menu or a click on a filmstrip tile) switches to Speaker. The choice of layout is now kept (
VideoLayout.key,videoLayoutin UserDefaults;calls.videoLayoutin the browser's localStorage), and a keptfocusreads as Speaker. Nothing had kept one before 0.4.0 — every call opened in Focus — so the migration is a rule inVideoLayout.stored/storedLayoutrather than data to move. The iPhone's ⋯ sheet says Speaker/Grid and "On the stage". - The stage is, in order: what you pinned while it is there; otherwise the newest shared screen (as before, so a new screen takes the stage unless you have pinned something); otherwise whoever has the floor; otherwise the first other person in your order (
Stage.speaker). You are on it only if you pin yourself. - Who has the floor is
ActiveSpeaker, fed five times a second from the per-peer levels and speaking gate the glow already uses. Someone takes the stage once they have been the loudest speaker fordominance(1.3 s), and whoever is on it keeps it for at leastminimumHold(3 s). A pause keeps the speaker; an interjection shorter than 1.3 s never takes it; two people trading sentences faster than that do not make it flap; someone who leaves loses it at once. You are never the speaker. room.js'sspeakerStepis the same rule with the same constants (SPEAKER_DOMINANCE,SPEAKER_HOLD), androom-layout.test.mjsholds them equal. - The filmstrip holds everything not on the stage — other screens first, then everyone in your order, you included. On the Mac it runs along the bottom of the window, tiles a sixth of the window high (72–120 pt; five fit across the smallest window, 760 × 540, and more scroll sideways), and rises clear of the bar while the controls show, as the self view does; with the controls hidden nothing lies over the stage. On iPhone it is a sideways scroller of upright 3:4 tiles above the home indicator (4:3 in landscape), rising with the bar in the same way. Tiles in it keep the glass name labels, the Muted mark and the speaking glow.
- You in the filmstrip, not floating — except in a call of two. The order is one list shared by Grid and the filmstrip, and you are in it like anyone else, so you can put yourself first or last; a floating self view would have been the one tile that ignored the order. In a call of two the filmstrip would hold nobody but you, so there you float in the corner exactly as since 0.3.4 (
Stage.selfFloats), and the full-window look of the commonest call is unchanged. - Pinning. A click or tap on a filmstrip tile, or Pin in a tile's menu (Mac context menu) or its accessibility actions, pins it. A pinned stage carries a Pinned badge; pressing it, or Unpin, goes back to following the speaker. On the Mac a single click on a filmstrip tile pins once the double-click interval has passed (a double click zooms the window, as on the picture).
- Your own order. Drag a tile onto another's place — in Grid or in the filmstrip — and the others make room; the dragged tile lifts (larger, with a shadow and the accent edge) and follows the pointer. On iPhone and iPad a long press picks it up. With Reduce Motion nothing grows or springs. Every platform has a way without dragging: the Mac's tile context menu, and on every platform the tile's accessibility actions (Pin/Unpin, Move earlier, Move later, Move to first); the browser gives each tile those as buttons. The order is yours alone and never sent. It is kept per meeting for the rest of the app's run (
ParticipantOrders; the browser's sessionStorage,calls.order.<meeting>), under keys that survive a rejoin — "self" and the person's name, since a peer id is new on every join. New arrivals go on the end; someone who leaves is removed; a remembered person who comes back takes their old place (ParticipantOrder). - VoiceOver hears the stage tile as "On stage: Tony" (", pinned" when it is), and a pin, an unpin or a move is announced once, politely. An automatic change of speaker is not announced: in a lively call it would talk over the talking.
- The mini window shows Speaker view's stage, whichever layout the call window is in.
Snapshots: CallsInterop --speaker-snapshots <dir> (Mac, real windows: the filmstrip with the controls up and down and at 760 × 540, a pin, a shared screen, a drag half way in Speaker and in Grid, Grid after a reorder, and a call of two); CallsPhoneUITests.testSpeakerViewSnapshots (iPhone, portrait and landscape, with -CallsFixtureLayout, -CallsFixtureSpeaker, -CallsFixturePinned, -CallsFixtureLift and -CallsFixtureOrder); and the browser gate's speaker scenario with CALLS_SPEAKER_SNAPSHOTS=<dir>.