Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/design-layout-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@reddb-io/redcode": minor
"@reddb-io/redcode-core": minor
---

Design mode: a passive layout audit with an inbox the person triages

The prototype now audits its own layout after fonts, geometry and finite animations settle: text clipped by its container, controls cut off or outside the viewport, text off-screen, a page that scrolls sideways, text covered by an opaque sibling. Findings survive only if two samples agree, and every pass reports its own completeness. They land in a "Layout issues" inbox on the review page — badge, drawer, select, queue, dismiss, reveal — and nothing in it reaches the agent until the person queues it, when it becomes one ordinary note. A warning is cleared only by a complete pass on a newer revision that no longer finds it; a failed pass, a different viewport or a reload in flight never clears anything, and a dismissal lasts one revision. Every frame load is named by a token so a pass from a replaced frame is discarded. The page holds the prototype behind a short curtain until its first pass (`experimental.design.gate`, `gate_timeout`, or `?gate=0` for one tab), asks the server whether the document can be served when the frame stays silent, and the one report that does wake the agent unasked is a prototype that cannot be shown at all (`<artifact-failures>`). Viewport classes can be narrowed with `experimental.design.viewports`; a class left out has its warnings marked obsolete rather than resolved.
11 changes: 11 additions & 0 deletions packages/core/src/v1/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,17 @@ export const Info = Schema.Struct({
}),
}),
).annotate({ description: "Limits on images attached to design-review notes." }),
viewports: Schema.optional(Schema.Array(Schema.Literals(["mobile", "compact", "desktop"]))).annotate({
description:
"Viewport classes the browser's layout audit reports on (default: all three). A class left out is never re-checked, and its warnings are marked obsolete.",
}),
gate: Schema.optional(Schema.Boolean).annotate({
description:
"Hold the prototype behind a curtain until its first layout pass, so a person never sees a half-laid-out page (default: true; ?gate=0 on the review URL disables it for one tab).",
}),
gate_timeout: Schema.optional(PositiveInt).annotate({
description: "Milliseconds the gate may hold the prototype before revealing it anyway (default: 12000, at most 60000).",
}),
}),
).annotate({ description: "Design mode: the review surface and its stores." }),
subtask_concurrency: Schema.optional(PositiveInt).annotate({
Expand Down
36 changes: 36 additions & 0 deletions packages/redcode/src/design/client/artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import type * as Helpers from "./helpers"
import type { artifactAudit } from "./audit"

export interface ArtifactConfig {
/** The revision this document was served for; every message carries it back. */
Expand All @@ -27,6 +28,9 @@ export type HelperTable = {
readonly [K in keyof typeof Helpers as (typeof Helpers)[K] extends (...args: any[]) => any
? K
: never]: (typeof Helpers)[K]
} & {
/** The layout audit, declared beside the helpers; it takes the table itself. */
readonly artifactAudit: typeof artifactAudit
}

export function artifactMain(config: ArtifactConfig, h: HelperTable) {
Expand Down Expand Up @@ -862,6 +866,11 @@ export function artifactMain(config: ArtifactConfig, h: HelperTable) {
case "restoreReviewState":
restoreReviewState(payload.state)
return
case "requestLayoutDiagnostics":
// The shell wants fresh evidence (it opened the inbox, or a pass was lost to a load
// race): run again and publish even if nothing changed.
audit.schedule(true)
return
case "attachmentResult":
// Only from the shell, and only for this document: a result for a chip of the previous
// document must not mark a new chip ready with the wrong image.
Expand Down Expand Up @@ -894,6 +903,33 @@ export function artifactMain(config: ArtifactConfig, h: HelperTable) {
scheduleReviewStateReport()
})

// --- the passive layout audit, and the one fatal path -----------------------------------------
const audit = h.artifactAudit({ h, post, isUi, selector, load: config.load })
// A local subresource the prototype declares but the server cannot serve makes the review
// unusable rather than merely flawed, so it bypasses the passive inbox. Only same-origin
// references count: a remote host failing is the viewer's network, not the prototype's defect.
window.addEventListener(
"error",
(event) => {
const el = event.target as any
if (!(el instanceof Element) || isUi(el)) return
const tag = String(el.tagName || "").toLowerCase()
if (!["img", "script", "link", "source", "video", "audio", "iframe"].includes(tag)) return
const raw = String(el.getAttribute("src") || el.getAttribute("href") || "")
if (!raw) return
let resolved: URL
try {
resolved = new URL(raw, document.baseURI)
} catch {
return
}
if (resolved.origin !== window.location.origin) return
post("artifactAssetFailure", { detail: "<" + tag + "> could not load " + resolved.pathname })
},
true,
)
audit.start()

const icon = document.querySelector('link[rel~="icon"]') as HTMLLinkElement | null
setAnnotationMode(true)
post("ready", {
Expand Down
Loading
Loading