A second real-iPhone review of the Stage 1 Dashboard found three defects. This fixes them, and ADR 0065 records the decisions. - Settings -> Displays now sets up a screen in one place. A "Set up a new screen" panel shows the platform display page to open on the screen, a Copy link button, and Add display directly beneath it; the step-by-step version is folded into a <details>. The addresses come from a new GET /api/v1/devices/setup, which requires core.devices.manage and lists each running display module's /display/<module-id> page. Nothing names Signage. With no display module installed, the panel says so and offers no Add display. - iOS Safari zoomed into the pairing field because the shared .ek-control rendered at 14px. On phones and touch screens it now renders at max(1rem, 16px). The viewport is untouched, so zoom stays available, and a test guards that. The shared focus trap now returns focus with preventScroll, so closing a dialog no longer moves the page under the phone header. - While the phone drawer is open, the page behind it no longer scrolls. The root's overflow is hidden, a non-passive touchmove listener cancels drags outside the drawer's list and overscroll past its ends, and root overscroll is turned off. The list itself still scrolls. Closing restores the exact scroll position unless another page opened. Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.5 KiB
0065: Settings → Displays says how to set up a screen; phone forms never zoom; the drawer locks the page
Context
A second hand review on a real iPhone, after ADR 0064, found three defects:
- Settings → Displays did not say how to set up a screen. Add display took a pairing code, but nothing on the page told an administrator how to get a physical screen to show one. The only setup instructions were in Signage's own Displays panel. A platform-owned display has to be set up from the platform, including on an installation without Signage.
- iOS Safari zoomed into the Add display form. Focusing the pairing code zoomed the page into the
dialog, and the page stayed zoomed after the dialog closed. The cause is the shared
.ek-controlfont size:typography.size-sm, 0.875rem, which is 14px. iOS Safari zooms the page into any focused text field whose font is smaller than 16px. - The page behind the phone drawer still scrolled, and pulling down behind it triggered the browser's pull-to-refresh.
The display host is already generic. /display/<module-id> is the platform's own page (ADR 0033): it
shows a pairing code, waits for approval, then hosts that module's display artifact. A pairing request
names the module because approval grants exactly that module's display permission (ADR 0036).
However, nothing told an administrator which of these pages exist. ADR 0033's display routes noted:
"There is deliberately no display inventory in this pass. A management UI may need one later." That
management UI now exists.
Decision
GET /api/v1/devices/setup: where a new screen starts
The API answers, for an administrator holding core.devices.manage, the platform display page of
every running module that shows content on screens:
{ "displays": [{ "moduleId": "signage", "name": "Signage", "url": "/display/signage" }] }
It reads the same eligible, running display registry as the display routes. It answers nothing to
anyone else: it is never anonymous, so an unpaired screen still cannot enumerate what is installed.
It is no-store. The URL is root-relative, and the browser resolves it against the API origin,
because the display host lives there in both topologies (ADR 0045).
No module-independent display page is added. A page that let a screen pair before choosing a module would need a device to exist without a module permission. That is the ROADMAP §5.4 global display model, and it needs its own decision about assigning devices to modules. Until then, every screen pairs through a module's display page, and this endpoint is how the platform names those pages.
Setting up a screen is one flow, in one place
Settings → Displays opens with a Set up a new screen panel, above the paired displays. Its contents are, in order:
- one sentence: Open this address on the new screen, then add it with the code it shows;
- the absolute display page, with Copy link;
- Add display, right beneath it.
Each step is directly under the one before, so nobody has to go back up the page. Add display lives here and only here, not in the page header.
The step-by-step version is folded away in a native <details> (Step-by-step instructions), for
whoever wants it, instead of a block of text everyone has to read past. With more than one module
that uses displays, each address is labelled with its module.
When no installed module uses displays, the panel says so and that one installed from Discover will appear there. It offers no Add display, which could only fail. Nothing names Signage. If the setup address cannot be read, the panel says so and still offers Add display, since a screen may already be showing its code. The paired list keeps working either way.
Copy link reaches the clipboard through the clicked button's own window, never a browser global (ADR 0058). If the browser cannot copy, the button says so and the address stays selectable.
Text controls are at least 16px on phones and touch screens
@ekklesia/ui keeps .ek-control at typography.size-sm on desktop. Under the Dashboard's compact
breakpoint (max-width: 47.999em) or on a coarse pointer, it uses
max(typography.size-md, 16px). Every shared text field and select is affected, in the Dashboard and
in every module root, so later Settings, People, and module forms cannot reintroduce the zoom.
The viewport is not touched. There is no user-scalable=no, maximum-scale, or minimum-scale,
and a test holds both shell pages to that, so pinch and accessibility zoom stay available.
Opening and closing a dialog must also not move the page. The shared focus trap now returns focus to
the opening control with preventScroll, because that control was on screen when it was used.
Scrolling it into view again, past the phone header's scroll padding, moved the page by the header's
height. The Add display button focuses itself the same way before opening the dialog.
The drawer locks the page behind it
While the phone drawer is open, lockDocumentScroll (surfaces/shell/src/browser/scroll-lock.ts)
does three things:
- The root stops scrolling. It sets
overflow: hiddenon<html>and<body>, which ends wheel, keyboard, and scrollbar scrolling, and touch scrolling on current iOS Safari. It keeps the scroll position in place. The older iOS workaround,position: fixedon the body, moves the page and its sticky header, which would show through the backdrop as a jump. - Touches outside the drawer go nowhere. A non-passive
touchmovelistener cancels a one-finger drag that starts outside the drawer's list. It also cancels a drag inside the list that would only overscroll it past its top or bottom. This covers an iOS that ignores the root'soverflow, and it is what stops pull-to-refresh. Two-finger gestures are never cancelled, so pinch zoom stays. - Overscroll does not chain. It sets
overscroll-behavior: noneon the root while locked. The drawer and its list carryoverscroll-behavior: contain, and the backdrop carriestouch-action: pinch-zoom.
The drawer's own list still scrolls by touch when it is taller than the screen.
Closing restores the inline styles that were there before, and the exact scroll position, unless a different page opened meanwhile. A page chosen in the drawer, or reached by Back or Forward, starts at its own top, as before.
When the drawer is closed nothing is set: the page scrolls and refreshes normally. The desktop
sidebar never locks anything. The focus trap, Escape, focus return, inert, and safe areas are
unchanged.
Alternatives
- Copy Signage's setup address into Settings. Rejected: it names one module, and it is wrong on an installation without it.
- A public
/displaypage listing the modules that use displays. Rejected: it would tell anyone on the network what is installed, which ADR 0036 deliberately prevents. - Pair first, choose the module later. That is the right long-term model (ROADMAP §5.4). It is deferred because it changes how a device's permission is granted, and needs its own decision.
- Use the Dashboard navigation's
display.url. Rejected: the navigation lists only modules with a Dashboard UI, and filters by module permissions. A display-only module, or one the administrator cannot open, would be missing. maximum-scale=1oruser-scalable=no. Rejected: it removes zoom for people who need it, and hides the cause instead of fixing it.- 16px controls everywhere. Rejected: desktop does not zoom, and the compact size is the design.
position: fixedbody scroll lock. Rejected: it jumps the page, and the sticky phone header, behind the backdrop.
Consequences
- An administrator can set up a screen from Settings → Displays alone. Released Signage 0.1.5 keeps
its own instructions, which point to the same platform page, until its follow-up issue
(
Zayin/Signage#24) is resolved. - Text fields are 16px on phones and touch screens everywhere
@ekklesia/uiis used, module UIs included. - A real-iPhone recheck remains necessary. Headless Chromium does not reproduce iOS Safari's focus-zoom or pull-to-refresh, so automated checks prove the computed font size and the cancelled gestures, not Safari's own behaviour.