From 64302e02210bd4e7d87b70275199af03aaee022d Mon Sep 17 00:00:00 2001 From: Maggie Appleton <5599295+MaggieAppleton@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:21:50 +0100 Subject: [PATCH 1/4] Show resolved decisions as margin markers beside their prose A linked, answered decision collapses out of the plan and becomes a gutter marker on the first line of the first anchored block. Hovering the marker or the prose washes the passage and previews the decision; pressing pins the popover, which also lists the options not chosen. Pending, empty and orphaned decisions keep a compact card. The decisions pane is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- apps/web/src/icon-tooltip.tsx | 2 +- e2e/decision-marker.e2e.ts | 334 +++++++++++ packages/editor/src/marks.ts | 33 +- packages/editor/src/questionnaires.ts | 28 + packages/editor/src/resolved-layer.tsx | 526 ++++++++++++++++++ packages/editor/src/resolved.test.ts | 162 ++++++ packages/editor/src/resolved.ts | 162 ++++++ packages/editor/src/styles.css | 210 +++++++ packages/editor/src/widgets-plugin.tsx | 2 + packages/editor/src/widgets/questionnaire.tsx | 27 +- packages/icons/src/index.ts | 2 + packages/icons/src/line.tsx | 19 + .../exceptions/dynamic-editor.json | 37 +- .../exceptions/dynamic-packages.json | 16 +- 14 files changed, 1547 insertions(+), 13 deletions(-) create mode 100644 e2e/decision-marker.e2e.ts create mode 100644 packages/editor/src/resolved-layer.tsx create mode 100644 packages/editor/src/resolved.test.ts create mode 100644 packages/editor/src/resolved.ts diff --git a/apps/web/src/icon-tooltip.tsx b/apps/web/src/icon-tooltip.tsx index e7739bd2..2e9ee287 100644 --- a/apps/web/src/icon-tooltip.tsx +++ b/apps/web/src/icon-tooltip.tsx @@ -25,7 +25,7 @@ function iconButton(target: EventTarget | null): HTMLElement | null { if ( !button || button.disabled || button.closest("[inert]") || button.matches( - ".sr-only, .navigation-drawer-backdrop, .navigation-modal-backdrop, .plan-comment-button", + ".sr-only, .navigation-drawer-backdrop, .navigation-modal-backdrop, .plan-comment-button, .plan-decision-marker", ) ) { return null; diff --git a/e2e/decision-marker.e2e.ts b/e2e/decision-marker.e2e.ts new file mode 100644 index 00000000..082d463b --- /dev/null +++ b/e2e/decision-marker.e2e.ts @@ -0,0 +1,334 @@ +/** + * A resolved decision is a margin marker beside the prose it produced. + * + * Geometry, hover, pinning, scrolling and the highlight registry are browser + * behaviour, so this runs in Chromium; the pure pieces (which options were not + * chosen, how hover and pin combine, where things sit) are in + * `packages/editor/src/resolved.test.ts`. + * + * Anchors are seeded with a stale epoch and the block's digest, which is how + * the server finds a block again after its history is gone. + */ + +import { createHash } from "node:crypto"; + +import { authenticate, content, expect, roomPath, test } from "./room"; + +import type { Page } from "@playwright/test"; + +const FIRST = "The rollout goes team by team, starting with the docs team."; +const SECOND = "After two weeks we review the pilot and decide whether to widen it."; +const WIDGET_A = "01K0N4TR8K7JGM4R1J7PW4R8YJ"; +const WIDGET_B = "01K0N4TR8K7JGM4R1J7PW4R8YK"; +const QUESTION_A = "01K0N4V4E7Y6P4MJ5WD8XZF3B2"; +const QUESTION_B = "01K0N4V4E7Y6P4MJ5WD8XZF3B3"; +const AT = "2026-09-23T15:13:00.000Z"; + +function digest(text: string): string { + return `sha256:${createHash("sha256").update(`${text}\n`).digest("hex")}`; +} + +/** A ULID, which is what the dialect insists an option id is. */ +function optionId(question: string, index: number): string { + return `${question.slice(0, 22)}${question.slice(-2)}0${index}`; +} + +function options(question: string, labels: string[]): string { + return labels.map((label, index) => + `