File size: 11,183 Bytes
2fe29a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import { constraintsFor, getArtifact, getConstraint } from "./catalog.js";
import { historyPreorder, MAX_VOTES_PER_OUTCOME } from "./domain.js";

const button = (label, className, action, dataset = {}) => {
  const element = document.createElement("button");
  element.type = "button";
  element.className = className;
  element.textContent = label;
  Object.assign(element.dataset, dataset);
  element.addEventListener("click", action);
  return element;
};

const setFocusKey = (element, key) => {
  element.dataset.focusKey = key;
  return element;
};

export function voteCountLabel(count) {
  return `${count} ${count === 1 ? "vote" : "votes"}`;
}

export function historyNodeMetadata(node, artifactTitle, parentTitle, isLineage, selected) {
  const status = node.status === "not-selected"
    ? "Non-selected sibling"
    : node.status[0].toUpperCase() + node.status.slice(1);
  const labels = [
    node.depth === 0 ? "Seed origin" : `Round ${node.depth}`,
    node.depth === 0 ? null : status,
    isLineage ? "Active lineage" : null,
    selected ? "Currently inspected" : null
  ].filter(Boolean);
  const relationship = node.depth === 0 ? "no parent" : `parent ${parentTitle}`;
  return {
    accessibleName: `${artifactTitle}, ${labels[0]}, ${relationship}${labels.slice(1).map((label) => `, ${label.toLowerCase()}`).join("")}`,
    visibleText: labels.join(" · ")
  };
}

export function renderRound(container, state, source, actions) {
  container.replaceChildren();
  const phase = document.createElement("p");
  phase.className = "phase-status";
  phase.textContent = state.phase === "choose"
    ? "Current phase: choose a constraint. Next: reveal two fixed outcomes."
    : state.phase === "compare"
      ? "Current phase: compare both outcomes. Next: record at least one vote."
      : state.phase === "vote"
        ? "Current phase: operator records participant preferences. Next: resolve the vote."
        : state.phase === "resolved"
          ? "Current phase: decision recorded. Next: continue from the winner."
          : "Current phase: branch complete. Next: explore any runnable history node.";
  container.append(phase);
  const kicker = document.createElement("p");
  kicker.className = "section-kicker";
  kicker.textContent = state.phase === "choose" ? "Shape the next fork" : `Round ${Object.keys(state.roundsById).length}`;
  container.append(kicker);

  if (state.phase === "choose") {
    const heading = document.createElement("h2");
    heading.id = "round-heading";
    heading.tabIndex = -1;
    heading.textContent = state.activeNodeId === state.seedNodeId ? "Choose a constraint" : "Continue the winning branch";
    container.append(heading);
    const intro = document.createElement("p");
    intro.textContent = "Select one curated rule, then reveal its two fixed interpretations.";
    container.append(intro);
    const group = document.createElement("fieldset");
    const legend = document.createElement("legend");
    legend.textContent = "Available constraints";
    group.append(legend);
    for (const constraint of constraintsFor(source, state.nodesById[state.activeNodeId].artifactId)) {
      const label = document.createElement("label");
      label.className = "constraint-card";
      const radio = document.createElement("input");
      radio.type = "radio";
      radio.name = "constraint";
      radio.value = constraint.id;
      setFocusKey(radio, `constraint-${constraint.id}`);
      radio.checked = state.selectedConstraintId === constraint.id;
      radio.addEventListener("change", () => actions.selectConstraint(constraint.id));
      const copy = document.createElement("span");
      const name = document.createElement("strong");
      name.textContent = constraint.title;
      const detail = document.createElement("small");
      detail.textContent = constraint.description;
      copy.append(name, detail);
      label.append(radio, copy);
      group.append(label);
    }
    container.append(group);
    const reveal = button("Reveal outcomes", "primary-button", actions.reveal);
    setFocusKey(reveal, "reveal-outcomes");
    reveal.disabled = !state.selectedConstraintId;
    container.append(reveal);
    return;
  }

  if (state.phase === "complete") {
    const heading = document.createElement("h2");
    heading.id = "round-heading";
    heading.tabIndex = -1;
    heading.textContent = "Branch complete";
    container.append(heading);
    const end = document.createElement("p");
    end.className = "end-card";
    end.textContent = "This pre-authored branch ends here in the local MVP.";
    container.append(end);
    return;
  }

  const round = state.roundsById[state.currentRoundId];
  const recipe = source.recipes.find((item) => item.id === round.recipeId);
  const constraint = getConstraint(source, recipe.constraintId);
  const heading = document.createElement("h2");
  heading.id = state.phase === "resolved" ? "result-heading" : "comparison-heading";
  heading.tabIndex = -1;
  heading.textContent = state.phase === "resolved" ? "The branch is decided" : `Compare: ${constraint.title}`;
  container.append(heading);
  const local = document.createElement("p");
  local.className = "context-disclosure";
  local.textContent = "These two outcomes were selected from a pre-authored local catalog for this artifact and constraint. The same choices always produce the same results.";
  container.append(local);
  const cards = document.createElement("div");
  cards.className = "outcome-grid";
  recipe.outcomes.forEach((outcome) => {
    const artifact = getArtifact(source, outcome.artifactId);
    const card = document.createElement("article");
    const selected = state.compareSelection === outcome.slot;
    card.className = `outcome-card branch-${outcome.slot}${selected ? " is-selected" : ""}`;
    const title = document.createElement("h3");
    title.textContent = `${outcome.slot.toUpperCase()}${artifact.title}`;
    const summary = document.createElement("p");
    summary.textContent = artifact.summary;
    const divergence = document.createElement("p");
    divergence.className = "divergence-note";
    const divergenceLabel = document.createElement("strong");
    divergenceLabel.textContent = "Meaningful split: ";
    divergence.append(divergenceLabel);
    divergence.append(outcome.divergenceNote);
    const play = button(selected ? "Playing this outcome" : "Play this outcome", "compare-button", () => actions.compare(outcome.slot));
    setFocusKey(play, `compare-${outcome.slot}`);
    play.setAttribute("aria-pressed", String(selected));
    if (selected) {
      play.disabled = true;
    }
    card.append(title, summary, divergence, play);
    cards.append(card);
  });
  container.append(cards);

  if (state.phase === "resolved") {
    const winnerArtifact = getArtifact(source, state.nodesById[state.activeNodeId].artifactId);
    const result = document.createElement("p");
    result.className = "result-card";
    result.textContent = `${round.winnerSlot.toUpperCase()} wins: ${winnerArtifact.title}. ${round.resolution === "authored-tie-rule" ? "The displayed authored tie rule decided this round." : "The higher recorded vote total decided this round."}`;
    const continueButton = button("Continue from winner", "primary-button", actions.continue);
    setFocusKey(continueButton, "continue-winner");
    container.append(result, continueButton);
    return;
  }

  const voting = document.createElement("section");
  voting.className = "voting";
  voting.setAttribute("aria-labelledby", "voting-heading");
  const voteHeading = document.createElement("h3");
  voteHeading.id = "voting-heading";
  voteHeading.textContent = "Operator-recorded voting";
  const disclosure = document.createElement("p");
  disclosure.className = "vote-disclosure";
  disclosure.textContent = "Votes are entered by the local operator in this browser on this device. Participant devices are not connected.";
  const tie = document.createElement("p");
  tie.textContent = `Tie rule, shown before resolution: outcome ${recipe.tieWinnerSlot.toUpperCase()} wins an equal total.`;
  voting.append(voteHeading, disclosure, tie);
  for (const slot of ["a", "b"]) {
    const row = document.createElement("div");
    row.className = "vote-row";
    const name = document.createElement("strong");
    name.textContent = `Outcome ${slot.toUpperCase()}: ${voteCountLabel(round.votes[slot])}`;
    const subtract = button("−", "vote-button", () => {
      if (round.votes[slot] > 0) actions.vote(slot, -1);
    });
    setFocusKey(subtract, `vote-${slot}-subtract`);
    subtract.setAttribute("aria-label", `Remove one vote from outcome ${slot.toUpperCase()}`);
    subtract.disabled = round.votes[slot] === 0;
    const add = button("+", "vote-button", () => actions.vote(slot, 1));
    setFocusKey(add, `vote-${slot}-add`);
    add.setAttribute("aria-label", `Add one vote to outcome ${slot.toUpperCase()}`);
    add.disabled = round.votes[slot] >= MAX_VOTES_PER_OUTCOME;
    row.append(name, subtract, add);
    voting.append(row);
  }
  const resolve = button("Resolve vote", "primary-button", actions.resolve);
  setFocusKey(resolve, "resolve-vote");
  resolve.disabled = state.phase !== "vote";
  voting.append(resolve);
  container.append(voting);
}

export function renderHistory(container, state, source, inspect) {
  const nodes = historyPreorder(state);
  const activeLineage = new Set();
  for (let id = state.activeNodeId; id; id = state.nodesById[id].parentNodeId) activeLineage.add(id);
  const children = new Map();
  for (const node of nodes) {
    const key = node.parentNodeId ?? "root";
    children.set(key, [...(children.get(key) ?? []), node]);
  }
  const build = (parent) => {
    const list = document.createElement("ul");
    for (const node of children.get(parent) ?? []) {
      const item = document.createElement("li");
      const selected = state.inspectedNodeId === node.nodeId;
      item.className = `history-node status-${node.status}${activeLineage.has(node.nodeId) ? " is-lineage" : ""}${selected ? " is-inspected" : ""}`;
      const artifact = getArtifact(source, node.artifactId);
      const parentTitle = node.parentNodeId ? getArtifact(source, state.nodesById[node.parentNodeId].artifactId).title : "none";
      const control = button(artifact.title, "history-button", () => inspect(node.nodeId));
      setFocusKey(control, `history-${node.nodeId}`);
      const metadata = historyNodeMetadata(node, artifact.title, parentTitle, activeLineage.has(node.nodeId), selected);
      control.setAttribute("aria-label", metadata.accessibleName);
      if (selected) control.setAttribute("aria-current", "true");
      control.setAttribute("aria-pressed", String(selected));
      const row = document.createElement("div");
      row.className = "history-node-row";
      const meta = document.createElement("span");
      meta.className = "node-meta";
      meta.textContent = metadata.visibleText;
      row.append(control, meta);
      item.append(row);
      if (children.has(node.nodeId)) item.append(build(node.nodeId));
      list.append(item);
    }
    return list;
  };
  container.replaceChildren(build("root"));
}