diff --git a/docs/ui-direction.md b/docs/ui-direction.md new file mode 100644 index 0000000..4a539af --- /dev/null +++ b/docs/ui-direction.md @@ -0,0 +1,223 @@ +# Resident UI direction + +Research snapshot: 2026-09-06. The reference study and remaining slices are design +proposals, not a completed accessibility audit. See the implementation record below. + +## Recommended references + +Use Rally for legibility and physical feedback, bsky38 for strong row hierarchy, +plyr.fm for component discipline, and Doodl for personal visual identity. Use +Typeahead/pub-search stats for compact information grouping. Keep ZDS's +forest/paper palette and familiar account navigation. These references suggest +patterns to adapt; they do not require importing their frameworks or themes. + +| Project | Evidence inspected | Worth adapting | Limits | +| --- | --- | --- | --- | +| [Rally](https://rally.waow.tech) | Live sign-in page and keyboard entry; `rally/src/style.css`, `src/ui/handle-autocomplete.ts` | Clear hierarchy, 18px body type, Atkinson Hyperlegible font, generous controls, visible focus, help disclosed beside its field, slight button depression | Ruled notebook background and large hard shadows are too visually active for every account section. Font choice alone does not establish accessibility. | +| [plyr.fm](https://plyr.fm) | Live track list and search overlay; `frontend/src/lib/components/BottomSheet.svelte`, `ConfirmDialog.svelte`, `SearchModal.svelte`; design-token documentation | Recognizable item identity, separate primary and secondary information, shared surface/type/spacing tokens, modal lifecycle and short transitions | Some secondary text is visually subdued. Search focuses its input on opening; this inspection did not establish correct focus return. Do not copy its custom overlays wholesale. | +| [Doodl](https://doodl.waow.tech) | Live drawing toolbar; `doodl/src/ui/slots.ts`, `src/style.css`, `index.html` | Small explicit slot registry, meaningful fallback labels, bounded artwork, approachable controls | Its viewport explicitly disables zoom. ZDS must preserve zoom. ZDS should retain visible action labels alongside unfamiliar drawings, and keep its individual-drawing configuration. | +| [Haunt](https://haunt.at) | Live home page; local `haunt/src/styles/globals.css` | A distinctive identity mark, generous breathing room, limited atmospheric color | Subdued small text and ambient animation are unsuitable models for account tasks. Local checkout is among the user's projects; the live site credits aly.codes. Useful as an atmosphere reference, not an account-layout reference. | +| [Pensieve](https://pensieve.waow.tech) | Live sign-in page; `pensieve/src/public/index.html`, `style.css` | One clear next step; explicit empty/blocked/ready states in source; labeled progress and status | Signed-in progress was inspected in source, not exercised live. Sparse sign-in UI does not demonstrate how well a dense account page works. | + +Source paths above are relative to each sibling repository under +`~/tangled.org/zzstoatzz.io/`. The additional references below were reviewed +following the initial shortlist. + +Rally's concrete details are especially useful: `style.css:91` defines a 4px +focus outline, `:281` begins its large button treatment, and `:293` supplies +pressed feedback. Its skip link was the first keyboard stop in the live page. +Plyr's native confirmation dialog uses `showModal()`; its bottom sheet contains +explicit focus handling and a reduced-motion override. These are implementation +references to review independently, not proof of conformance. + +### Additional references: bsky38 and the stats pages + +- **[The race to 38](https://bsky38.waow.tech/):** local checkout is + `~/tangled.org/zzstoatzz.io/race38`; its live source link names + `tangled.org/waow.tech/bsky38`. This is Nate's companion viewer, distinct from + the upstream voting site at bsky38.com, which the initial review mistakenly + substituted. The correct live UI combines paper/ink surfaces, crisp framing, + blue actions, serif headings, and readable sans-serif controls. The chart and + replay controls occupy the primary space; race details and selection editing + use disclosure. Portraits connect visual identity to the data. + Source reviewed: `race38/web/style.css`, `docs/design-origin.md`, and + `docs/ui-rigor.md`. The strongest transferable ideas are stable DOM identity, + explicit interaction contracts, bounded visual emphasis, native controls, + and feedback tied to accepted events. CSS includes 44px control targets, + focus styling, pressed-state selectors, and reduced-motion overrides. These + are implementation evidence, not a completed accessibility audit. For ZDS, + borrow the clear surface separation, intentional artwork placement, and + continuity through updates. Account actions need prompt, accurate confirmation; + do not copy the race's delayed animated count arrival into security feedback. +- **[Typeahead stats](https://typeahead.waow.tech/stats):** summary metrics are + grouped near their relevant chart, range controls sit beside the content they + affect, and the traffic chart is supplemented by named totals and a table. + Selecting 24h updated the search count, latency, and chart granularity in place. + Source: `typeahead/src/pages/stats.ts` and `theme.ts`. Particularly useful: + number/unit pairs wrap together, and home/docs/stats share theme tokens. For + ZDS, keep summaries short and technical detail close to the task. Do not copy + the very small captions. The inspected 24h control was 34px high and exposed + its selection through a CSS class without `aria-pressed`; preserve explicit + programmatic selection state when adapting this interaction. +- **[pub-search stats](https://pub-search.waow.tech/stats) and + [search](https://pub-search.waow.tech/):** the stats page achieves density with + aligned rows, whitespace, a compact metric strip, and section-local controls + instead of boxing every fact. The search page makes its primary task immediate. + Source: `pub-search/site/stats.html`, `dashboard.css`, and `components.css`. + ZDS can use this economy for metadata inside expanded app details. The stats + CSS has 9–12px labels and very subdued text/border tokens; these need stronger + treatment for a friendly account UI. Chart inspection was visual, not a full + keyboard or screen-reader test. + +This strengthens the recommendation for fewer, better-structured rows rather +than a separate decorative card for every piece of metadata. Doodl artwork can +occupy the row's identity position while names, state, and controls remain clear. + +### Anti-slop review + +Adopt an explicit design critique before implementation and again on rendered +output: every visual choice should support this account task, hierarchy should +survive without decoration, and repeated panels/chips/animations should earn +their space. Evaluate real content, awkward lengths, failure states, and generic +as well as custom artwork. Accessibility and predictable behavior take priority +over stylistic prohibitions or novelty. + +The exact named anti-slop skill is pending identification. No matching installed +skill was found in the local skill locations searched, and web discovery found +several unrelated implementations. Do not claim one is installed or applied +until its source and linked guidance have been reviewed. The recommendation is +to use it as a critique discipline alongside the existing frontend-design skill, +not to introduce another runtime design system or replace the accessibility +acceptance baseline. + +## What ZDS should feel like + +A quiet, well-made account utility with personal illustrations. Clear headings +and readable content carry the page; artwork helps people recognize places. +Surfaces should have visibly different roles: page background, section surface, +editable field, and active control. Borders and spacing should remain useful +without shadows or color perception. + +Use a small type scale: comfortable body and field text, stronger section +headings, and secondary copy that remains readable. Keep monospace for protocol +identifiers. Try Atkinson Hyperlegible as a comparison against the system sans +before accepting an additional font asset; its name is not a substitute for +testing. Avoid making every sentence the same weight or every section a stack +of nested boxes. + +“Silky” should first mean continuity: stable layout while artwork loads, retained +form input after errors, immediate pending feedback, no duplicate submissions, +and predictable focus when changing views. Add restrained press/surface feedback +after that. A proposed motion budget is 120–180ms for small feedback and at most +200ms for an overlay, with no ornamental looping motion in account views. Under +reduced motion, remove spatial movement and keep clear static state changes. + +## Slottable presentation contract + +Keep the registry and rendering under `src/internal/web`. Each slot describes a +visual purpose and fallback; it does not carry authentication, authorization, +navigation, or storage behavior. + +- ZDS owns the semantic element, visible label, accessible name, keyboard + behavior, focus ring, state indicator, and target dimensions. +- Artwork occupies a reserved box with `object-fit: contain`. A large drawing + does not enlarge or shift the form; a small drawing does not shrink the target. +- Decorative images use empty alt text. If future artwork conveys content, + explicitly define a separate content slot with an appropriate text equivalent. +- An operator can select individual drawings. No iconset membership is required. + Unconfigured deployments retain generic defaults. +- Missing, broken, unusually proportioned, mostly white, mostly black, and + transparent artwork must all leave the interface usable. Do not use a blanket + white tile to rescue every asset. Preserve the fallback until a replacement + loads, and evaluate artwork against both themes. +- New slots can cover section illustrations, empty-state artwork, or header + marks. Introduce them where they aid recognition; do not make arbitrary HTML, + CSS, scripts, or control geometry remotely replaceable. +- Keep the current explicit build-time artwork bundling. No remote resolution + on startup or on account requests, and no drawing dependency in auth/storage. + +## Accessibility acceptance baseline + +Target WCAG 2.2 AA across complete resident flows, with a few deliberate comfort +targets beyond the minimum. The [W3C's WCAG 2.2 overview](https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/) +specifically covers unobscured focus, target spacing, accessible authentication, +and alternatives to dragging. Its AA target-size minimum is 24 CSS pixels with +exceptions; use at least 44px targets for ZDS standalone controls as a stronger +product target. Focus Appearance is AAA, not an AA requirement; nevertheless +use a conspicuous, contrasting focus indicator. + +Use the full [WCAG 2.2 standard](https://www.w3.org/TR/WCAG22/) as the audit basis: + +- Keyboard-only completion, logical focus order, a skip link, current-page + indication, meaningful headings, and focus that is not hidden by sticky UI. +- Text contrast of at least 4.5:1 for normal text and 3:1 for qualifying large + text; sufficient non-text contrast for required control/state information. + Never communicate active, revoked, error, or danger by color alone. +- Text resize to 200%, reflow at 320 CSS pixels, browser zoom, and text-spacing + overrides without losing information or controls. Long DIDs/scopes must wrap. +- Labels and instructions connected to inputs; specific errors near the field; + preserve entered values and explain recovery. Allow password managers and + paste. Avoid repeated entry when the value is already available. +- Announce concise results and pending state, not an entire refreshed account + page. Keep critical feedback present long enough to find and act on. +- Test light/dark, forced colors, reduced motion, keyboard, touch, and a screen + reader. Automated checks supplement manual flow tests; they do not certify it. + +For confirmations, prefer native dialog semantics and follow the +[WAI modal-dialog interaction guidance](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/): +intentional initial focus, contained keyboard navigation, an accessible name, +and sensible focus restoration. Use dialogs only when the decision warrants an +interruption; ordinary help and technical details fit native disclosure better. + +## Successive implementation slices + +1. **Shared foundation.** Consolidate presentation tokens and control states + inside the web layer. Improve section hierarchy, target sizing, readable + secondary text, and focus styling across existing pages. Compare a real + account fixture with generic and instance artwork in both themes. +2. **Navigation and feedback.** Give view changes appropriate page titles and + focus behavior. Narrow live announcements. Add local pending/error/success + handling without losing input or rebuilding focused controls unnecessarily. + Current `account.html` marks the whole resident section live, and `showView` + changes classes/current navigation without updating focus or title. +3. **Account actions.** Polish field groups and credential rows. Replace ad hoc + prompts/confirmations only where a well-tested inline form or native dialog + improves the task. Test cancellation, failures, retries, and duplicate clicks. +4. **Additional artwork slots.** Add a few useful section/empty-state slots after + the surrounding layout and semantic contract are stable. Expand fixtures for + asset failure and unusual drawings before broadening configurability. + +Keep each runtime slice separately reviewable and deployable. Use browser +request/response evidence for reported failures, existing required tests/smokes, +and a small consistent manual flow matrix. Before/after startup benchmarks and +asset-size checks should show whether presentation work adds cost; deploy once +per verified slice, avoiding restarts for research documents alone. This review +does not add account-management surfaces or alter the planned lifetime work. + +The original research changed no runtime files or sibling projects. Live inspection covered public desktop surfaces and +limited keyboard interactions, not a full mobile or assistive-technology audit. + +## Implementation: first resident UI slice (2026-09-07) + +The first release implements distinct account-section surfaces, a consolidated +identity summary, a larger type hierarchy, 44px controls, 48px fields, and mobile +navigation that wraps into two columns. Existing drawing slots retain their +labels and reserved sizes. Layout and palette changes stay in the web assets; +there are no additional fonts, packages, remote requests, or account surfaces. + +View navigation now updates the document title and focuses its heading. Modified +link clicks retain browser behavior. The whole resident region is no longer a +live announcement; the existing status element is an atomic polite status region. +Hover/press feedback respects pointer and motion preferences. Session lifetimes +and endpoint behavior are unchanged. Remaining action feedback/dialog work is +still a subsequent slice. + +Validation: existing test suite, endpoint smoke, permissioned smoke, diff check, +and Zig Zen completed successfully. Browser checks covered sign-in, reload +persistence, active navigation/focus, custom artwork, desktop, 390px and 320px +reflow, and the light palette through a local preview proxy. This is not a full +assistive-technology audit. Startup probe: five warm-cache samples per scenario, +ReleaseSafe, 10,000 history entries. Before/after median readiness was 16.19/20.96ms +fresh, 9.02/8.79ms restart, and 9.68/9.57ms with history. Fresh database creation +varied; restart results show no observed regression. These exclude VM boot and +proxy routing. diff --git a/src/internal/web/account.html b/src/internal/web/account.html index 38393a2..23614f5 100644 --- a/src/internal/web/account.html +++ b/src/internal/web/account.html @@ -7,24 +7,24 @@ @@ -99,26 +107,27 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> +
zds -

account

-

The resident control center for an account hosted on this PDS.

+

Your account

+

Manage your sign-in methods and the apps connected to your account.

-

-
+

+
-

home

+

Overview

This PDS stores your public repo, private permissioned-space records, blobs, credentials, and app access state. Use the sections above to manage what this server can control.

-

Experimental data tools: browse permissioned spaces.

-

quick checks

+

Experimental data tools: Browse permissioned spaces.

+

At a glance

App access

@@ -140,21 +149,21 @@ SOFTWARE.

-

spaces

+

Permissioned spaces

View private records in permissioned spaces for this account.

-

record set

-

selected record

Choose a record.
+

Record set

+

Selected record

Choose a record.
-

account

- - - - +

Sign-in & account

+ + + +
-

about

+

About this PDS

ZDS is the PDS hosting this account. It stores repo data, blobs, account credentials, OAuth token state, and experimental permissioned-space data for residents on this server.

This page is local to the PDS. It is not a Bluesky app view, and it only manages things this PDS is responsible for.

@@ -173,9 +182,21 @@ async function fail(r,msg){if(r.ok){const t=await r.text();return t?JSON.parse(t const authed=(url,opts={})=>fetch(url,{...opts,headers:{...(opts.headers||{}),authorization:`Bearer ${accessJwt}`}}); async function xrpc(path,params={},options={}){const qs=new URLSearchParams(params);return fail(await authed(path+(qs.size?'?'+qs:''),options),'request failed')} function viewName(){const p=location.pathname; if(p.endsWith('/security'))return'manage'; if(p.endsWith('/sessions')||p.endsWith('/apps'))return'sessions'; if(p.endsWith('/spaces'))return'spaces'; if(p.endsWith('/manage'))return'manage'; if(p.endsWith('/about'))return'about'; return'home'} -function showView(name=viewName()){for(const el of document.querySelectorAll('.view'))el.classList.toggle('on',el.id==='view-'+name);for(const a of document.querySelectorAll('.nav a'))a.setAttribute('aria-current',a.dataset.view===name?'page':'false')} -document.addEventListener('click',e=>{const a=e.target.closest('a[data-view]');if(!a)return;e.preventDefault();history.pushState(null,'',a.href);showView(a.dataset.view)}); -addEventListener('popstate',()=>showView()); +function showView(name=viewName(),moveFocus=false){ + for(const el of document.querySelectorAll('.view'))el.classList.toggle('on',el.id==='view-'+name); + for(const a of document.querySelectorAll('.nav a')){if(a.dataset.view===name)a.setAttribute('aria-current','page');else a.removeAttribute('aria-current')} + const heading=$('view-'+name).querySelector('h2'); + document.title=heading.textContent+' · ZDS'; + heading.tabIndex=-1; + if(moveFocus)heading.focus(); +} +document.addEventListener('click',e=>{ + if(!(e.target instanceof Element)||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)return; + const a=e.target.closest('a[data-view]');if(!a)return; + e.preventDefault();if(location.pathname!==a.pathname)history.pushState(null,'',a.href); + showView(a.dataset.view,true); +}); +addEventListener('popstate',()=>showView(viewName(),resident.classList.contains('on'))); function updateHeader(){ $('account-handle').textContent=account.handle; $('account-did').textContent=account.did; $('account-status').textContent=account.status||'active'; $('account-email-state').textContent=account.emailConfirmed?'verified':'needs verification'; $('email-copy').textContent=`Current email: ${account.email}${account.emailConfirmed?' (verified)':' (not verified)'}`; $('verify-email-block').classList.toggle('hidden',!!account.emailConfirmed); } function quickChecks(sessions){const rows=[['email',account.emailConfirmed?'verified':'needs verification',account.emailConfirmed?'active':'inactive'],['app access',`${(sessions.oauthGrants||[]).filter(g=>g.active).length} active`, 'active'],['direct API tokens',`${(sessions.sessions||[]).filter(s=>s.active).length} active`, 'active'],['permissioned spaces',`${spaces.length}`, spaces.length?'active':'inactive']];$('home-checks').innerHTML=rows.map(([a,b,c])=>`
${esc(a)}
${esc(b)}
`).join('')} const b64ToBuf=(v)=>{const b64=v.replace(/-/g,'+').replace(/_/g,'/');const bin=atob(b64+'==='.slice((b64.length+3)%4));const out=new Uint8Array(bin.length);for(let i=0;i