Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fe3e85e
Add a reader-local order model for decision decks
MaggieAppleton Oct 11, 2026
70c008c
Publish runs of adjacent decisions from the document
MaggieAppleton Oct 11, 2026
83ed8b6
Add a decision deck with keyboard cycling and announcements
MaggieAppleton Oct 11, 2026
69f0db7
Ignore modified arrows in the deck and fix the count spacing
MaggieAppleton Oct 11, 2026
cc2fbef
Shuffle decision deck cards and settle answered ones to the back
MaggieAppleton Oct 11, 2026
1c95e89
Keep reversed deck shuffles on their layer and clip the incoming card
MaggieAppleton Oct 11, 2026
e9188c4
Stack adjacent decisions in a document as a deck
MaggieAppleton Oct 11, 2026
16413ce
Keep inline decks below editor overlays and update affected specs
MaggieAppleton Oct 11, 2026
3199693
Host inline decks on the run's first card and count stack position
MaggieAppleton Oct 11, 2026
476a2ee
Reveal deck members from Chat and settle answered decisions
MaggieAppleton Oct 11, 2026
b22fd82
Cover decision deck motion, fix deck time tokens, and record the comp…
MaggieAppleton Oct 11, 2026
0097569
Use the shared focus ring on the deck's top card
MaggieAppleton Oct 11, 2026
4667f3d
Harden deck motion samples and time token tests
MaggieAppleton Oct 11, 2026
fb43896
Address final review of inline decision decks
MaggieAppleton Oct 11, 2026
9059af8
Renew design-contract review for decision decks
MaggieAppleton Oct 11, 2026
d50306f
Keep phone decision chips out of inline decks
MaggieAppleton Oct 11, 2026
9021e89
Renew design-contract review after rebasing decision decks
MaggieAppleton Oct 11, 2026
0063677
Cover phone chips in place of an inline deck
MaggieAppleton Oct 11, 2026
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
1 change: 1 addition & 0 deletions apps/web/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ Rectangular controls use a restrained four-step radius scale: small for grips an
- **GitHub reference pills:** A pull request or issue link in a document is a tinted capsule over the link itself: the state surface mixed half with the page, a hairline at 12% of the state icon colour, a 14px state glyph, the authored title in primary ink and a never-truncated `#number` in tertiary ink. Open is success, merged and completed are merged, closed is danger, and draft, not planned, loading and no access stay neutral.
- **Document:** The authored page is white, readable, and wider only where content requires it. Headings, lists, tables, code, and callouts retain document semantics inside the editor.
- **Document images:** Click opens a full-screen preview with zoom and close controls. Hover or keyboard focus reveals metadata editing and corner resize handles. Resizing preserves the image ratio, shares the chosen width with collaborators, and remains bounded by the document or column on narrow screens.
- **Decision decks:** Adjacent decisions stack as a deck. The top card is live; up to two cards behind it show their header edges and are inert. Previous and next controls, Left and Right arrows, and a count of the top card's place cycle the deck with a short shuffle; answered decisions settle to the back. Reduced motion reorders without animation. Deck order is each reader's own.

## Do's and Don'ts

Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/room-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
PlanStatus,
QuestionnaireStore,
ResearchLauncher,
revealDeckItem,
selectDecisionView,
ThreadStore,
useConnectionNotice,
Expand Down Expand Up @@ -743,6 +744,8 @@ export function RoomWorkspace(
}"]`,
);
if (!card) return;
// A card behind the top of a deck is inert and cannot take focus.
revealDeckItem(card);
card.tabIndex = -1;
card.focus();
});
Expand Down
88 changes: 88 additions & 0 deletions e2e/decision-deck-fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { storedQuestion } from "../apps/server/src/testing/plan";

import type { SeedState } from "../apps/server/src/testing/plan";

/** The digest of a paragraph reading exactly `Anchored paragraph.` */
export const ANCHORED_DIGEST =
"sha256:3ccc3e648811df8180799f8b012c6934bcf44a306f5eb8b8c9d2676b0493ccf4";
const HEADERS = ["Storage", "Scope", "Rollout", "Naming"];
const PROMPTS = [
"Where should room state live?",
"What belongs in the first cut?",
"How should we deploy?",
"What should we call it?",
];

/** Seed `count` open Planner cards with records, next to each other unless separated. */
export function deckSeed(
prose: { before?: string; after?: string },
count: number,
options: { separated?: boolean; optionCounts?: number[]; anchored?: number[] } = {},
): { source: string; state: SeedState; prompts: string[]; ids: string[] } {
let ids: string[] = [];
let cards: string[] = [];
let questions: unknown[] = [];
let openQuestions: unknown[] = [];
for (let index = 0; index < count; index++) {
let widget = `01K0N4TR8K7JGM4R1J7PW4R8Y${index}`;
let question = `01K0N4V4E7Y6P4MJ5WD8XZF3B${index}`;
let extra = (options.optionCounts?.[index] ?? 1) - 1;
let choices = Array.from({ length: extra + 1 }, (_, at) => ({
id: `01K0N4W3B7P27CBAEC7A8C8W${"EFGHJKMNPQRS"[at]}${index}`,
label: at === 0 ? `Option ${index + 1}` : `Extra option ${at}`,
description: at === 0 ? "" : "A longer option that adds height to the card.",
}));
let definition = {
questions: [{
id: question,
header: HEADERS[index]!,
question: PROMPTS[index]!,
multiple: false,
options: choices,
}],
};
ids.push(widget);
cards.push(`<Questionnaire id="${widget}">
<Question id="${question}" header="${HEADERS[index]}" prompt="${PROMPTS[index]}" multiple="false">
${choices.map(choice => `<Option id="${choice.id}" label="${choice.label}" />`).join("\n")}
</Question>
</Questionnaire>`);
questions.push({
id: widget,
definition,
status: "open",
origin: "planner",
history: [],
optionOrigins: {},
editors: [],
...(options.anchored?.includes(index)
? {
anchors: {
widget,
questions: {
[question]: {
anchors: [{ epoch: "stale", position: "", digest: ANCHORED_DIGEST }],
pending: false,
},
},
},
}
: {}),
});
openQuestions.push({
definition,
id: widget,
model: storedQuestion(definition),
revision: 0,
widget,
});
}
let joiner = options.separated ? "\n\nProse between decisions.\n\n" : "\n\n";
let source = [prose.before, cards.join(joiner), prose.after].filter(Boolean).join("\n\n") + "\n";
return {
source,
state: { revision: 1, questions, openQuestions },
prompts: PROMPTS.slice(0, count),
ids,
};
}
Loading
Loading