Files
kandev/docs/plans/webkit-task-dialog-rendering/plan.md
T
2026-08-01 17:07:32 +01:00

167 lines
10 KiB
Markdown

---
spec: docs/specs/ui/webkit-task-dialog-rendering.md
created: 2026-08-01
status: complete
---
# Implementation Plan: WebKit Task Dialog Rendering
## Overview
Classify the active rendering engine before React mounts, then opt only the Create Task dialog into
a WebKit-safe CSS path. Prove engine classification with unit tests and prove both the unchanged
Chromium path and the WebKit desktop/mobile geometry through focused browser tests.
## Confirmed root cause
`DialogContent` combines fixed centering with translated positioning and a `scale(0.95)` enter/exit
animation on the same element that contains the form text. WebKit rasterizes transformed,
composited text-bearing layers differently from Blink, producing the reported soft dialog while
the same Go-served SPA and fonts remain sharp in Chrome. The overlay and content also share
`z-50`, leaving their relative composited-layer ordering implicit. The Tauri window is opaque and
does not configure vibrancy or transparency.
## Frontend
### Rendering-engine marker
- Add `apps/web/lib/browser/rendering-engine.ts` with a pure classifier and a small document-marker
helper.
- Classify a user agent as WebKit when it contains `AppleWebKit` and is not a desktop Blink runtime.
iPhone, iPad, and iPod UAs without Blink compatibility tokens remain on the WebKit path. Branded
iPadOS desktop-mode Chrome/Edge UAs that carry `Chrome/` or `Edg/` are intentionally treated as
`other`; this token-based classifier does not infer touch capability from a desktop UA.
- Treat desktop `Chrome`, `Chromium`, `HeadlessChrome`, `Edg`, `OPR`, and `SamsungBrowser` tokens as
Blink/non-WebKit compatibility UAs. Firefox and unknown engines fall back to `other`.
- In `apps/web/src/main.tsx`, apply `data-rendering-engine="webkit|other"` to the document root
before boot-payload loading and React rendering. Do not persist the value.
### Create Task dialog rendering
- Add a semantic opt-in data attribute to the Create Task `DialogContent` in
`apps/web/components/task-create-dialog.tsx`; do not modify the shared default motion in
`apps/packages/ui/src/dialog.tsx`.
- Add a semantic nested-confirmation marker to
`apps/web/components/discard-local-changes-dialog.tsx` so the WebKit stack can elevate that
confirmation without styling by test IDs.
- Allow the shared `AlertDialogContent` primitive to pass a scoped overlay class to its portaled
overlay, keeping the nested stack rule local to the discard confirmation.
- Add scoped WebKit selectors and opacity-only keyframes in `apps/web/app/globals.css`.
- Under `html[data-rendering-engine="webkit"]`, override the opted-in Create Task and nested
confirmation surfaces' animation names so their keyframes never write `transform`; replace
translated centering with `inset: 0`, automatic margins, and desktop `height: fit-content`; lower
only the task dialog's own overlay to `z-49` while keeping content at the shared `z-50` modal
level; and elevate the nested discard confirmation to an explicit `z-52` overlay and `z-53`
content layer with `height: fit-content` at every viewport width.
- Preserve the current mobile `width: 100%` and `height: 100%`, desktop 900px width, 85vh maximum
height, form overflow ownership, rounded corners, focus, and dismissal behavior.
- Pass `showCloseButton={false}` only to the Create Task dialog so the generic top-right close
control is absent in every browser while the footer Cancel action remains available.
- Add `pt-0` to the Create Task surface so it no longer reserves top padding for the removed
control while preserving horizontal and bottom padding.
- Leave the existing scale-and-fade utility classes active for non-WebKit engines.
### Mobile design contract
- **Desktop outcome:** Safari and WebKit-backed Tauri show the existing centered 900px Create Task
form without transformed text; Chromium retains its current motion.
- **Mobile entry point:** the existing Kanban floating New Task action opens the same full-height
dialog.
- **Nearest shipped exemplar:** the existing mobile Create Task surface and
`MobileKanbanPage.mobileFab` remain the composition and entry-point baseline.
- **Hierarchy and primary action:** task name, prompt, repositories, agent profile, and submit
controls remain in their existing order; task creation remains the primary action.
- **Presentation rationale:** this is a rendering workaround, so preserving the existing
full-height mobile dialog is less disruptive than introducing a new drawer or route.
- **Geometry:** the dialog remains the single viewport-bound surface, keeps its existing internal
form scroll owner and safe-area behavior, and introduces no document horizontal overflow.
- **Shared logic:** all task state, validation, selectors, and submit handlers remain unchanged;
only engine classification and presentation CSS differ.
- **Mobile proof:** Pixel 5 E2E forces the WebKit marker and verifies full-viewport containment,
accessible inputs, internal usability, and zero document horizontal overflow.
## Tests
- **What:** Safari/WKWebView/WebKitGTK, desktop Blink, Firefox, iOS branded browsers, iPadOS
desktop mode, and unknown-UA classification.
- **File:** `apps/web/lib/browser/rendering-engine.test.ts`
- **How:** table-driven Vitest cases against the pure classifier plus marker-helper assertions.
- **What:** the marker is applied before React boot without persistence.
- **File:** `apps/web/lib/browser/rendering-engine.test.ts`
- **How:** pass a detached document root and navigator-like input to the marker helper and assert
the exact `data-rendering-engine` value.
## E2E Tests
- **Scenario:** Chromium retains its existing Create Task scale motion and positioning when the
runtime marker is `other`.
- **File:** `apps/web/e2e/tests/task/create-task-webkit-rendering.spec.ts`
- **What to verify:** the early marker is `other`, the dialog keeps the existing animation name,
translated centering, dimensions, and content-over-overlay hit testing.
- **Scenario:** the WebKit override removes transforms while preserving centered desktop geometry.
- **File:** `apps/web/e2e/tests/task/create-task-webkit-rendering.spec.ts`
- **What to verify:** force the root marker to `webkit` before opening the dialog; assert the
opacity-only animation name, identity `transform`/zero `translate`, explicit overlay/content
stacking levels, 900px capped width, viewport-centered bounds, and focused task-name input.
- **Scenario:** a narrow WebKit Create Task dialog keeps its full-height usable composition.
- **File:** `apps/web/e2e/tests/task/mobile-create-task-webkit-rendering.spec.ts`
- **What to verify:** force the WebKit marker, open from `MobileKanbanPage.mobileFab`, assert
viewport containment, task-name and prompt reachability, internal scrolling when required, and
no document horizontal overflow.
- **Scenario:** a nested dirty-tree confirmation remains compact and above the task form.
- **File:** `apps/web/e2e/tests/task/create-task-branch-selector.spec.ts`
- **What to verify:** force the WebKit marker, trigger the discard confirmation, and assert
opacity-only motion, transform-free centering, the explicit `z-52` overlay / `z-53` content
stack, and preserved form state after Cancel.
## Implementation Tasks
- [x] [Task 01: Classify the rendering engine](task-01-rendering-engine-marker.md)
- [x] [Task 02: Apply WebKit-safe task dialog rendering](task-02-webkit-dialog-rendering.md)
Execution is sequential in the primary conversation. No subagent delegation is planned or
authorized.
## Validation
Run from `apps` unless noted otherwise:
- `pnpm --filter @kandev/web test -- --run lib/browser/rendering-engine.test.ts` — 14 passed,
including the desktop Mac Chrome Blink-token regression.
- `pnpm run typecheck` from `apps/web` — no errors.
- `pnpm --filter @kandev/web lint` — passed.
- `pnpm exec prettier --check app/globals.css components/discard-local-changes-dialog.tsx
components/task-create-dialog.tsx e2e/tests/task/create-task-webkit-rendering.spec.ts
lib/browser/rendering-engine.test.ts lib/browser/rendering-engine.ts
../../docs/plans/webkit-task-dialog-rendering/plan.md
../../docs/plans/webkit-task-dialog-rendering/task-02-webkit-dialog-rendering.md` from `apps/web`
— passed.
- `pnpm --filter @kandev/web build` — passed.
- `pnpm --filter @kandev/web e2e:run --no-build --host --project chromium --
tests/task/create-task-webkit-rendering.spec.ts --workers=1` — 2 passed.
- `pnpm --filter @kandev/web e2e:run --no-build --host --project mobile-chrome --
tests/task/mobile-create-task-webkit-rendering.spec.ts --workers=1` — 1 passed.
- `pnpm --filter @kandev/web e2e:run --no-build --host --project chromium --
tests/task/create-task-branch-selector.spec.ts --grep "confirm modal lists files" --workers=1` — 1
passed, confirming the nested WebKit discard confirmation's motion and explicit z-52/z-53
stacking.
- `pnpm --filter @kandev/web e2e:run --no-build --host --project chromium --
tests/task/dialog-body-lock.spec.ts --workers=1` — 1 passed.
- `pnpm --filter @kandev/web e2e:run --no-build --host --project mobile-chrome --
tests/task/mobile-dialog-body-lock.spec.ts --workers=1` — 1 passed.
- Native macOS Safari/Tauri visual comparison — not run; this Linux worktree cannot launch those
engines. PR screenshots document the desktop and mobile states for native review.
## Risks
- Browsers do not expose a standardized rendering-engine API. The classifier therefore needs
explicit compatibility-UA tests and must fail safe to the existing rendering path.
- Chromium E2E can prove selector behavior and geometry after forcing the root marker, but it
cannot reproduce WKWebView rasterization. Final acceptance requires a macOS Tauri/Safari visual
comparison at actual-size zoom (`Cmd+0`) after the automated checks pass.
- Transform-free absolute centering can accidentally stretch an auto-height desktop dialog. The
WebKit desktop selector must set `height: fit-content`, while the narrow layout retains the
existing full-height rule.
- The opt-in attribute deliberately limits blast radius. Other dialogs remain out of scope until
they show the same reproduced WebKit defect.