@skillcdn/console 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +6 -4
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +260 -6
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/cli/format.d.ts +7 -1
  6. package/dist/cli/format.d.ts.map +1 -1
  7. package/dist/cli/format.js +22 -0
  8. package/dist/cli/format.js.map +1 -1
  9. package/dist/cli/help.d.ts.map +1 -1
  10. package/dist/cli/help.js +69 -2
  11. package/dist/cli/help.js.map +1 -1
  12. package/dist/components/calendar.d.ts +15 -0
  13. package/dist/components/calendar.d.ts.map +1 -1
  14. package/dist/components/calendar.js +57 -30
  15. package/dist/components/calendar.js.map +1 -1
  16. package/dist/components/decision-list.d.ts.map +1 -1
  17. package/dist/components/decision-list.js +1 -1
  18. package/dist/components/decision-list.js.map +1 -1
  19. package/dist/components/skills.d.ts.map +1 -1
  20. package/dist/components/skills.js +1 -1
  21. package/dist/components/skills.js.map +1 -1
  22. package/dist/components/work.d.ts +4 -0
  23. package/dist/components/work.d.ts.map +1 -1
  24. package/dist/components/work.js +8 -3
  25. package/dist/components/work.js.map +1 -1
  26. package/dist/connect-page.d.ts.map +1 -1
  27. package/dist/connect-page.js +1 -1
  28. package/dist/connect-page.js.map +1 -1
  29. package/dist/console.css +35 -1
  30. package/dist/console.d.ts +3 -2
  31. package/dist/console.d.ts.map +1 -1
  32. package/dist/console.js +24 -58
  33. package/dist/console.js.map +1 -1
  34. package/dist/dates.d.ts +4 -0
  35. package/dist/dates.d.ts.map +1 -1
  36. package/dist/dates.js +26 -0
  37. package/dist/dates.js.map +1 -1
  38. package/dist/i18n/en.d.ts +7 -19
  39. package/dist/i18n/en.d.ts.map +1 -1
  40. package/dist/i18n/en.js +10 -22
  41. package/dist/i18n/en.js.map +1 -1
  42. package/dist/i18n/ko.d.ts.map +1 -1
  43. package/dist/i18n/ko.js +10 -22
  44. package/dist/i18n/ko.js.map +1 -1
  45. package/dist/index.d.ts +3 -3
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +3 -3
  48. package/dist/index.js.map +1 -1
  49. package/dist/router.d.ts +4 -0
  50. package/dist/router.d.ts.map +1 -1
  51. package/dist/router.js +10 -2
  52. package/dist/router.js.map +1 -1
  53. package/docs/specs/cli.md +15 -2
  54. package/docs/specs/rest.md +13 -13
  55. package/package.json +1 -1
  56. package/skills/building-a-console/SKILL.md +2 -2
  57. package/skills/working-the-board/SKILL.md +5 -1
  58. package/src/cli/cli.ts +333 -5
  59. package/src/cli/format.ts +27 -0
  60. package/src/cli/help.ts +86 -1
  61. package/src/components/calendar.tsx +219 -91
  62. package/src/components/decision-list.tsx +0 -1
  63. package/src/components/skills.tsx +0 -1
  64. package/src/components/work.tsx +66 -46
  65. package/src/connect-page.tsx +0 -1
  66. package/src/console.tsx +22 -74
  67. package/src/dates.ts +28 -0
  68. package/src/i18n/en.ts +10 -28
  69. package/src/i18n/ko.ts +10 -28
  70. package/src/index.ts +5 -0
  71. package/src/router.ts +15 -5
  72. package/src/styles/console.css +35 -1
@@ -1,8 +1,11 @@
1
+ import type { ReactNode } from "react";
1
2
  import { OPEN_TASK_STATES, type RestCategory, type RestPerson, type RestTask } from "../api.js";
2
3
  import {
4
+ addDays,
3
5
  addMonths,
4
6
  daysBetween,
5
7
  firstDayOfWeek,
8
+ formatDays,
6
9
  formatMonth,
7
10
  formatWeekday,
8
11
  type GridDay,
@@ -10,15 +13,17 @@ import {
10
13
  monthOf,
11
14
  today as todayOf,
12
15
  weekdays,
16
+ weekOf,
13
17
  } from "../dates.js";
14
18
  import { useLanguage, useWords } from "../i18n/index.js";
15
19
  import { cx, PersonChip, personName } from "./ui.js";
16
20
 
17
- // The calendar of a project (ADR-0019): the month as a grid. A task planned from a start to a
18
- // due day lies across its days as a bar, in a lane of its own within each week so that the bars
19
- // of one week never cross; a task with a due day alone sits on it as a dot, and one with a
20
- // start alone is marked on that day. Who has what says whether the work is on its days. Takes
21
- // its data as props and nothing from the network; the month shown is the address's (ADR-0010).
21
+ // The calendar of a project (ADR-0019): the month as a grid, or one week on its own. A task
22
+ // planned from a start to a due day lies across its days as a bar, in a lane of its own within
23
+ // each week so that the bars of one week never cross; a task with a due day alone sits on it as
24
+ // a dot, and one with a start alone is marked on that day. Who has what says whether the work
25
+ // is on its days. Takes its data as props and nothing from the network; the month or the week
26
+ // shown is the address's (ADR-0010).
22
27
 
23
28
  const isOpen = (task: RestTask): boolean => OPEN_TASK_STATES.includes(task.state);
24
29
 
@@ -99,15 +104,14 @@ function push(map: Map<string, RestTask[]>, day: string, task: RestTask): void {
99
104
  }
100
105
  }
101
106
 
102
- export function Calendar(props: CalendarProps) {
103
- const words = useWords().calendar;
104
- const { tag } = useLanguage();
105
- const now = props.today ?? todayOf();
106
- const firstDay = firstDayOfWeek(tag);
107
- const weeks = monthGrid(props.month, firstDay);
107
+ /** The tasks that sit on one day each: due on it, or beginning on it with no due day yet. */
108
+ function onDays(tasks: readonly RestTask[]): {
109
+ readonly due: ReadonlyMap<string, readonly RestTask[]>;
110
+ readonly starting: ReadonlyMap<string, readonly RestTask[]>;
111
+ } {
108
112
  const due = new Map<string, RestTask[]>();
109
113
  const starting = new Map<string, RestTask[]>();
110
- for (const task of props.tasks) {
114
+ for (const task of tasks) {
111
115
  if (spans(task)) {
112
116
  continue;
113
117
  }
@@ -121,6 +125,121 @@ export function Calendar(props: CalendarProps) {
121
125
  for (const list of due.values()) {
122
126
  list.sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || a.number - b.number);
123
127
  }
128
+ return { due, starting };
129
+ }
130
+
131
+ /** How a task reads on a day: struck through once it is over, and marked once it is late. */
132
+ const tone = (task: RestTask, day: string, now: string): string =>
133
+ cx(!isOpen(task) && "sc-calendar-over", isOpen(task) && day < now && "sc-calendar-late");
134
+
135
+ interface WeekRowProps {
136
+ readonly week: readonly GridDay[];
137
+ readonly segments: readonly WeekSegment[];
138
+ readonly due: ReadonlyMap<string, readonly RestTask[]>;
139
+ readonly starting: ReadonlyMap<string, readonly RestTask[]>;
140
+ readonly now: string;
141
+ readonly taskHref: (task: RestTask) => string;
142
+ /** What stands before a task's title where the tasks are of several projects: the project's mark. */
143
+ readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
144
+ }
145
+
146
+ /** One week as a row of cells: the bars across the days in their lanes, then the tasks on each day. */
147
+ function WeekRow(props: WeekRowProps) {
148
+ const words = useWords().calendar;
149
+ const { week, segments, now } = props;
150
+ const lanes = segments.reduce((most, segment) => Math.max(most, segment.lane + 1), 0);
151
+ return (
152
+ <tr>
153
+ {week.map((cell, column) => (
154
+ <td
155
+ key={cell.day}
156
+ className={cx(
157
+ "sc-calendar-cell",
158
+ !cell.inMonth && "sc-calendar-cell-outside",
159
+ cell.day === now && "sc-calendar-cell-today",
160
+ )}
161
+ >
162
+ <span className="sc-calendar-day">{Number(cell.day.slice(8))}</span>
163
+ {Array.from({ length: lanes }, (_, lane) => {
164
+ const segment = segments.find(
165
+ (candidate) =>
166
+ candidate.lane === lane && candidate.from <= column && column <= candidate.to,
167
+ );
168
+ if (segment === undefined) {
169
+ // The lane is someone else's on this day: the bars below keep their height.
170
+ // biome-ignore lint/suspicious/noArrayIndexKey: an empty lane is nothing but its position, and the lanes of a week never reorder
171
+ return <span key={lane} className="sc-calendar-lane" aria-hidden="true" />;
172
+ }
173
+ const { task } = segment;
174
+ return (
175
+ <a
176
+ key={task.id}
177
+ className={cx(
178
+ "sc-calendar-bar",
179
+ `sc-category-${task.category?.color ?? "gray"}`,
180
+ segment.from === column && segment.begins && "sc-calendar-bar-begins",
181
+ segment.to === column && segment.ends && "sc-calendar-bar-ends",
182
+ tone(task, task.dueOn ?? cell.day, now),
183
+ )}
184
+ href={props.taskHref(task)}
185
+ title={`#${task.number} ${task.title}`}
186
+ >
187
+ {segment.from === column && (
188
+ <>
189
+ {props.markOf?.(task)}
190
+ <span className="sc-calendar-task-title">{task.title}</span>
191
+ </>
192
+ )}
193
+ </a>
194
+ );
195
+ })}
196
+ {(props.due.get(cell.day) ?? []).map((task) => {
197
+ const mark = props.markOf?.(task);
198
+ return (
199
+ <a
200
+ key={task.id}
201
+ className={cx("sc-calendar-task", tone(task, cell.day, now))}
202
+ href={props.taskHref(task)}
203
+ title={`#${task.number} ${task.title}`}
204
+ >
205
+ {mark === undefined || mark === null ? (
206
+ <span
207
+ className={cx(
208
+ "sc-calendar-dot",
209
+ `sc-category-${task.category?.color ?? "gray"}`,
210
+ )}
211
+ />
212
+ ) : (
213
+ mark
214
+ )}
215
+ <span className="sc-calendar-task-title">{task.title}</span>
216
+ </a>
217
+ );
218
+ })}
219
+ {(props.starting.get(cell.day) ?? []).map((task) => (
220
+ <a
221
+ key={task.id}
222
+ className="sc-calendar-task sc-calendar-task-start"
223
+ href={props.taskHref(task)}
224
+ title={`#${task.number} ${task.title}: ${words.startsOnly}`}
225
+ >
226
+ {props.markOf?.(task)}
227
+ <span className="sc-calendar-task-title">{task.title}</span>
228
+ </a>
229
+ ))}
230
+ </td>
231
+ ))}
232
+ </tr>
233
+ );
234
+ }
235
+
236
+ export function Calendar(props: CalendarProps) {
237
+ const words = useWords().calendar;
238
+ const { tag } = useLanguage();
239
+ const now = props.today ?? todayOf();
240
+ const firstDay = firstDayOfWeek(tag);
241
+ const weeks = monthGrid(props.month, firstDay);
242
+ const { due, starting } = onDays(props.tasks);
124
243
  const inMonth = (day: string) => monthOf(day) === props.month;
125
244
  const monthWeeks = weeks.map((week) => ({ week, segments: weekSegments(week, props.tasks) }));
126
245
  const nothing =
@@ -131,8 +250,6 @@ export function Calendar(props: CalendarProps) {
131
250
  (segment) => !week.slice(segment.from, segment.to + 1).some((cell) => cell.inMonth),
132
251
  ),
133
252
  );
134
- const tone = (task: RestTask, day: string) =>
135
- cx(!isOpen(task) && "sc-calendar-over", isOpen(task) && day < now && "sc-calendar-late");
136
253
  return (
137
254
  <div className="sc-calendar">
138
255
  <div className="sc-calendar-head">
@@ -171,83 +288,17 @@ export function Calendar(props: CalendarProps) {
171
288
  </tr>
172
289
  </thead>
173
290
  <tbody>
174
- {monthWeeks.map(({ week, segments }) => {
175
- const lanes = segments.reduce((most, segment) => Math.max(most, segment.lane + 1), 0);
176
- return (
177
- <tr key={week[0]?.day}>
178
- {week.map((cell, column) => (
179
- <td
180
- key={cell.day}
181
- className={cx(
182
- "sc-calendar-cell",
183
- !cell.inMonth && "sc-calendar-cell-outside",
184
- cell.day === now && "sc-calendar-cell-today",
185
- )}
186
- >
187
- <span className="sc-calendar-day">{Number(cell.day.slice(8))}</span>
188
- {Array.from({ length: lanes }, (_, lane) => {
189
- const segment = segments.find(
190
- (candidate) =>
191
- candidate.lane === lane &&
192
- candidate.from <= column &&
193
- column <= candidate.to,
194
- );
195
- if (segment === undefined) {
196
- // The lane is someone else's on this day: the bars below keep their height.
197
- // biome-ignore lint/suspicious/noArrayIndexKey: an empty lane is nothing but its position, and the lanes of a week never reorder
198
- return <span key={lane} className="sc-calendar-lane" aria-hidden="true" />;
199
- }
200
- const { task } = segment;
201
- return (
202
- <a
203
- key={task.id}
204
- className={cx(
205
- "sc-calendar-bar",
206
- `sc-category-${task.category?.color ?? "gray"}`,
207
- segment.from === column && segment.begins && "sc-calendar-bar-begins",
208
- segment.to === column && segment.ends && "sc-calendar-bar-ends",
209
- tone(task, task.dueOn ?? cell.day),
210
- )}
211
- href={props.taskHref(task)}
212
- title={`#${task.number} ${task.title}`}
213
- >
214
- {segment.from === column && (
215
- <span className="sc-calendar-task-title">{task.title}</span>
216
- )}
217
- </a>
218
- );
219
- })}
220
- {(due.get(cell.day) ?? []).map((task) => (
221
- <a
222
- key={task.id}
223
- className={cx("sc-calendar-task", tone(task, cell.day))}
224
- href={props.taskHref(task)}
225
- title={`#${task.number} ${task.title}`}
226
- >
227
- <span
228
- className={cx(
229
- "sc-calendar-dot",
230
- `sc-category-${task.category?.color ?? "gray"}`,
231
- )}
232
- />
233
- <span className="sc-calendar-task-title">{task.title}</span>
234
- </a>
235
- ))}
236
- {(starting.get(cell.day) ?? []).map((task) => (
237
- <a
238
- key={task.id}
239
- className="sc-calendar-task sc-calendar-task-start"
240
- href={props.taskHref(task)}
241
- title={`#${task.number} ${task.title}: ${words.startsOnly}`}
242
- >
243
- <span className="sc-calendar-task-title">{task.title}</span>
244
- </a>
245
- ))}
246
- </td>
247
- ))}
248
- </tr>
249
- );
250
- })}
291
+ {monthWeeks.map(({ week, segments }) => (
292
+ <WeekRow
293
+ key={week[0]?.day}
294
+ week={week}
295
+ segments={segments}
296
+ due={due}
297
+ starting={starting}
298
+ now={now}
299
+ taskHref={props.taskHref}
300
+ />
301
+ ))}
251
302
  </tbody>
252
303
  </table>
253
304
  {(props.categories ?? []).length > 0 && (
@@ -267,6 +318,83 @@ export function Calendar(props: CalendarProps) {
267
318
  );
268
319
  }
269
320
 
321
+ export interface WeekProps {
322
+ readonly tasks: readonly RestTask[];
323
+ /** A day of the week shown; today's week when left out. */
324
+ readonly day?: string | undefined;
325
+ /** The day to count from; the browser's today when left out. */
326
+ readonly today?: string | undefined;
327
+ readonly taskHref: (task: RestTask) => string;
328
+ /** Where the week of a day is shown. */
329
+ readonly weekHref: (day: string) => string;
330
+ /** What stands before a task's title where the tasks are of several projects: the project's mark. */
331
+ readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
332
+ }
333
+
334
+ /** One week on its own: the days as columns, the tasks on them, and the weeks around a step away. */
335
+ export function Week(props: WeekProps) {
336
+ const words = useWords().calendar.week;
337
+ const { tag } = useLanguage();
338
+ const now = props.today ?? todayOf();
339
+ const firstDay = firstDayOfWeek(tag);
340
+ const days = weekOf(props.day ?? now, firstDay);
341
+ const first = days[0] ?? now;
342
+ const last = days[days.length - 1] ?? now;
343
+ const week: GridDay[] = days.map((day) => ({ day, inMonth: true }));
344
+ const { due, starting } = onDays(props.tasks);
345
+ const segments = weekSegments(week, props.tasks);
346
+ const nothing = segments.length === 0 && days.every((day) => !due.has(day) && !starting.has(day));
347
+ return (
348
+ <div className="sc-calendar sc-week">
349
+ <div className="sc-calendar-head">
350
+ <h2 className="sc-calendar-month">{formatDays(first, last, tag)}</h2>
351
+ <nav className="sc-calendar-nav" aria-label={words.title}>
352
+ <a
353
+ className="sc-button sc-button-ghost sc-button-sm"
354
+ href={props.weekHref(addDays(first, -7))}
355
+ aria-label={words.previous}
356
+ >
357
+ {"<"}
358
+ </a>
359
+ <a className="sc-button sc-button-ghost sc-button-sm" href={props.weekHref(now)}>
360
+ {words.thisWeek}
361
+ </a>
362
+ <a
363
+ className="sc-button sc-button-ghost sc-button-sm"
364
+ href={props.weekHref(addDays(first, 7))}
365
+ aria-label={words.next}
366
+ >
367
+ {">"}
368
+ </a>
369
+ </nav>
370
+ </div>
371
+ <table className="sc-calendar-grid" aria-label={words.title}>
372
+ <thead>
373
+ <tr>
374
+ {weekdays(firstDay).map((weekday) => (
375
+ <th key={weekday} scope="col">
376
+ {formatWeekday(weekday, tag)}
377
+ </th>
378
+ ))}
379
+ </tr>
380
+ </thead>
381
+ <tbody>
382
+ <WeekRow
383
+ week={week}
384
+ segments={segments}
385
+ due={due}
386
+ starting={starting}
387
+ now={now}
388
+ taskHref={props.taskHref}
389
+ markOf={props.markOf}
390
+ />
391
+ </tbody>
392
+ </table>
393
+ {nothing && <p className="sc-muted">{words.nothing}</p>}
394
+ </div>
395
+ );
396
+ }
397
+
270
398
  /** One person's share of the open work, as the table of who has what says it. */
271
399
  export interface WorkloadRow {
272
400
  /** The person, or nobody for the tasks assigned to no one. */
@@ -326,7 +326,6 @@ export function DecisionList(props: DecisionListProps) {
326
326
  <h2 className="sc-section-title">
327
327
  {words.decisions.record} <span className="sc-column-count">{answered.length}</span>
328
328
  </h2>
329
- <p className="sc-lead">{words.decisions.recordLead}</p>
330
329
  <DecisionLog
331
330
  decisions={answered}
332
331
  tasks={props.tasks}
@@ -30,7 +30,6 @@ export function SkillList(props: SkillListProps) {
30
30
  <a href={skills.page} target="_blank" rel="noopener noreferrer">
31
31
  <code>{skills.address}</code>
32
32
  </a>
33
- {words.howLoaded}
34
33
  </p>
35
34
  );
36
35
  if (skills.status !== "ready") {
@@ -1,20 +1,26 @@
1
1
  import type { RestTask, RestWork } from "../api.js";
2
2
  import { addDays, today as todayOf } from "../dates.js";
3
3
  import { useWords } from "../i18n/index.js";
4
+ import { Week } from "./calendar.js";
4
5
  import { ProjectMark } from "./projects.js";
5
6
  import { TaskList } from "./task-list.js";
6
7
  import { PersonChip, Time } from "./ui.js";
7
8
 
8
- // What is on a person's plate (ADR-0019), across the projects they may see: the open tasks
9
- // assigned to them, by when they are due, and the decisions that wait for a person. Takes its
10
- // data as props and nothing from the network.
9
+ // What is on a person's plate (ADR-0019), across the projects they may see: the week at the
10
+ // top, with the open tasks assigned to them on its days; below, the same tasks by when they
11
+ // are due, and the decisions that wait for a person. Takes its data as props and nothing from
12
+ // the network; the week shown is the address's (ADR-0010).
11
13
 
12
14
  export interface WorkListProps {
13
15
  readonly work: RestWork;
14
16
  /** The day to count from; the browser's today when left out. */
15
17
  readonly today?: string | undefined;
18
+ /** A day of the week shown at the top; today's week when left out. */
19
+ readonly week?: string | undefined;
16
20
  readonly taskHref: (projectKey: string, task: RestTask) => string;
17
21
  readonly decisionHref: (projectKey: string, decisionId: string) => string;
22
+ /** Where the page is with the week of a day at the top. */
23
+ readonly weekHref: (day: string) => string;
18
24
  }
19
25
 
20
26
  export function WorkList(props: WorkListProps) {
@@ -25,6 +31,10 @@ export function WorkList(props: WorkListProps) {
25
31
  const { tasks, decisions } = props.work;
26
32
  const projectOf = new Map(tasks.map((item) => [item.task.id, item.project]));
27
33
  const hrefOf = (task: RestTask) => props.taskHref(projectOf.get(task.id)?.key ?? "", task);
34
+ const markOf = (task: RestTask) => {
35
+ const project = projectOf.get(task.id);
36
+ return project === undefined ? undefined : <ProjectMark project={project} size="sm" />;
37
+ };
28
38
  const nameOf = (task: RestTask) => {
29
39
  const project = projectOf.get(task.id);
30
40
  return project === undefined ? undefined : (
@@ -45,49 +55,59 @@ export function WorkList(props: WorkListProps) {
45
55
  }
46
56
  return (
47
57
  <div className="sc-work">
48
- {groups
49
- .filter((group) => group.tasks.length > 0)
50
- .map((group) => (
51
- <section key={group.title} className="sc-task-section" aria-label={group.title}>
52
- <h2 className="sc-section-title">
53
- {group.title} <span className="sc-column-count">{group.tasks.length}</span>
54
- </h2>
55
- <TaskList
56
- tasks={group.tasks}
57
- taskHref={hrefOf}
58
- projectOf={nameOf}
59
- showState
60
- today={now}
61
- />
62
- </section>
63
- ))}
64
- <section className="sc-task-section" aria-label={work.decisions}>
65
- <h2 className="sc-section-title">
66
- {work.decisions} <span className="sc-column-count">{decisions.length}</span>
67
- </h2>
68
- {decisions.length === 0 ? (
69
- <p className="sc-muted">{work.noDecisions}</p>
70
- ) : (
71
- <ol className="sc-decision-log">
72
- {decisions.map(({ project, decision }) => (
73
- <li key={decision.id} className="sc-decision-row">
74
- <a
75
- className="sc-decision-row-question"
76
- href={props.decisionHref(project.key, decision.id)}
77
- >
78
- {decision.question}
79
- </a>
80
- <span className="sc-task-row-project">
81
- <ProjectMark project={project} size="sm" /> {work.inProject(project.name)}
82
- </span>
83
- <span className="sc-decision-row-meta">
84
- <PersonChip person={decision.raisedBy} /> <Time iso={decision.createdAt} />
85
- </span>
86
- </li>
87
- ))}
88
- </ol>
89
- )}
90
- </section>
58
+ <Week
59
+ tasks={tasks.map((item) => item.task)}
60
+ day={props.week}
61
+ today={now}
62
+ taskHref={hrefOf}
63
+ weekHref={props.weekHref}
64
+ markOf={markOf}
65
+ />
66
+ <div className="sc-work-lists">
67
+ {groups
68
+ .filter((group) => group.tasks.length > 0)
69
+ .map((group) => (
70
+ <section key={group.title} className="sc-task-section" aria-label={group.title}>
71
+ <h2 className="sc-section-title">
72
+ {group.title} <span className="sc-column-count">{group.tasks.length}</span>
73
+ </h2>
74
+ <TaskList
75
+ tasks={group.tasks}
76
+ taskHref={hrefOf}
77
+ projectOf={nameOf}
78
+ showState
79
+ today={now}
80
+ />
81
+ </section>
82
+ ))}
83
+ <section className="sc-task-section" aria-label={work.decisions}>
84
+ <h2 className="sc-section-title">
85
+ {work.decisions} <span className="sc-column-count">{decisions.length}</span>
86
+ </h2>
87
+ {decisions.length === 0 ? (
88
+ <p className="sc-muted">{work.noDecisions}</p>
89
+ ) : (
90
+ <ol className="sc-decision-log">
91
+ {decisions.map(({ project, decision }) => (
92
+ <li key={decision.id} className="sc-decision-row">
93
+ <a
94
+ className="sc-decision-row-question"
95
+ href={props.decisionHref(project.key, decision.id)}
96
+ >
97
+ {decision.question}
98
+ </a>
99
+ <span className="sc-task-row-project">
100
+ <ProjectMark project={project} size="sm" /> {work.inProject(project.name)}
101
+ </span>
102
+ <span className="sc-decision-row-meta">
103
+ <PersonChip person={decision.raisedBy} /> <Time iso={decision.createdAt} />
104
+ </span>
105
+ </li>
106
+ ))}
107
+ </ol>
108
+ )}
109
+ </section>
110
+ </div>
91
111
  </div>
92
112
  );
93
113
  }
@@ -70,7 +70,6 @@ export function ConnectPage(props: ConnectPageProps) {
70
70
  );
71
71
  const ask = (
72
72
  <>
73
- <p className="sc-lead">{words.lead}</p>
74
73
  <section className="sc-panel" aria-label={words.aria}>
75
74
  <ConnectCodeForm busy={busy} onSubmit={(typed) => navigation.go(connectHref(typed))} />
76
75
  </section>