Files
2026-08-24 11:09:03 +00:00

10 KiB

spec, created, status
spec created status
docs/specs/ui/requirements/webkit-task-dialog-rendering.md 2026-08-01 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

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.