@skillcdn/console 0.1.14 → 0.1.16

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 (165) hide show
  1. package/README.md +5 -4
  2. package/SKILLCDN.md +2 -0
  3. package/dist/agent-name.d.ts +3 -0
  4. package/dist/agent-name.d.ts.map +1 -0
  5. package/dist/agent-name.js +9 -0
  6. package/dist/agent-name.js.map +1 -0
  7. package/dist/api.d.ts +6 -4
  8. package/dist/api.d.ts.map +1 -1
  9. package/dist/api.js +6 -4
  10. package/dist/api.js.map +1 -1
  11. package/dist/cli/cli.d.ts +2 -0
  12. package/dist/cli/cli.d.ts.map +1 -1
  13. package/dist/cli/cli.js +225 -24
  14. package/dist/cli/cli.js.map +1 -1
  15. package/dist/cli/format.d.ts +14 -4
  16. package/dist/cli/format.d.ts.map +1 -1
  17. package/dist/cli/format.js +59 -10
  18. package/dist/cli/format.js.map +1 -1
  19. package/dist/cli/help.d.ts.map +1 -1
  20. package/dist/cli/help.js +28 -12
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/main.js +5 -0
  23. package/dist/cli/main.js.map +1 -1
  24. package/dist/client.d.ts +20 -2
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +11 -2
  27. package/dist/client.js.map +1 -1
  28. package/dist/components/calendar.d.ts +35 -2
  29. package/dist/components/calendar.d.ts.map +1 -1
  30. package/dist/components/calendar.js +80 -31
  31. package/dist/components/calendar.js.map +1 -1
  32. package/dist/components/connect.d.ts +4 -1
  33. package/dist/components/connect.d.ts.map +1 -1
  34. package/dist/components/connect.js +7 -3
  35. package/dist/components/connect.js.map +1 -1
  36. package/dist/components/decision-list.d.ts.map +1 -1
  37. package/dist/components/decision-list.js +2 -2
  38. package/dist/components/decision-list.js.map +1 -1
  39. package/dist/components/documents.d.ts +5 -0
  40. package/dist/components/documents.d.ts.map +1 -1
  41. package/dist/components/documents.js +3 -2
  42. package/dist/components/documents.js.map +1 -1
  43. package/dist/components/event-feed.d.ts +1 -1
  44. package/dist/components/event-feed.d.ts.map +1 -1
  45. package/dist/components/event-feed.js +10 -1
  46. package/dist/components/event-feed.js.map +1 -1
  47. package/dist/components/people.d.ts +24 -1
  48. package/dist/components/people.d.ts.map +1 -1
  49. package/dist/components/people.js +35 -3
  50. package/dist/components/people.js.map +1 -1
  51. package/dist/components/projects.d.ts +10 -1
  52. package/dist/components/projects.d.ts.map +1 -1
  53. package/dist/components/projects.js +8 -5
  54. package/dist/components/projects.js.map +1 -1
  55. package/dist/components/rules.d.ts +40 -0
  56. package/dist/components/rules.d.ts.map +1 -0
  57. package/dist/components/rules.js +46 -0
  58. package/dist/components/rules.js.map +1 -0
  59. package/dist/components/runs.d.ts +1 -1
  60. package/dist/components/runs.d.ts.map +1 -1
  61. package/dist/components/runs.js +2 -1
  62. package/dist/components/runs.js.map +1 -1
  63. package/dist/components/skills.d.ts.map +1 -1
  64. package/dist/components/skills.js +5 -1
  65. package/dist/components/skills.js.map +1 -1
  66. package/dist/components/task-form.d.ts.map +1 -1
  67. package/dist/components/task-form.js +1 -1
  68. package/dist/components/task-form.js.map +1 -1
  69. package/dist/components/tokens.d.ts.map +1 -1
  70. package/dist/components/tokens.js +2 -9
  71. package/dist/components/tokens.js.map +1 -1
  72. package/dist/components/ui.d.ts +7 -2
  73. package/dist/components/ui.d.ts.map +1 -1
  74. package/dist/components/ui.js +13 -0
  75. package/dist/components/ui.js.map +1 -1
  76. package/dist/components/work.d.ts +5 -1
  77. package/dist/components/work.d.ts.map +1 -1
  78. package/dist/components/work.js +1 -1
  79. package/dist/components/work.js.map +1 -1
  80. package/dist/console.css +263 -17
  81. package/dist/console.d.ts.map +1 -1
  82. package/dist/console.js +197 -43
  83. package/dist/console.js.map +1 -1
  84. package/dist/data.d.ts +19 -2
  85. package/dist/data.d.ts.map +1 -1
  86. package/dist/data.js +27 -1
  87. package/dist/data.js.map +1 -1
  88. package/dist/i18n/en.d.ts +84 -9
  89. package/dist/i18n/en.d.ts.map +1 -1
  90. package/dist/i18n/en.js +84 -13
  91. package/dist/i18n/en.js.map +1 -1
  92. package/dist/i18n/ko.d.ts.map +1 -1
  93. package/dist/i18n/ko.js +84 -13
  94. package/dist/i18n/ko.js.map +1 -1
  95. package/dist/index.d.ts +4 -3
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +4 -3
  98. package/dist/index.js.map +1 -1
  99. package/dist/limits.d.ts +4 -0
  100. package/dist/limits.d.ts.map +1 -1
  101. package/dist/limits.js +4 -0
  102. package/dist/limits.js.map +1 -1
  103. package/dist/router.d.ts +30 -2
  104. package/dist/router.d.ts.map +1 -1
  105. package/dist/router.js +50 -10
  106. package/dist/router.js.map +1 -1
  107. package/dist/routes.d.ts +14 -4
  108. package/dist/routes.d.ts.map +1 -1
  109. package/dist/routes.js +17 -2
  110. package/dist/routes.js.map +1 -1
  111. package/dist/rules.d.ts +28 -0
  112. package/dist/rules.d.ts.map +1 -0
  113. package/dist/rules.js +102 -0
  114. package/dist/rules.js.map +1 -0
  115. package/dist/schemas.d.ts +652 -4
  116. package/dist/schemas.d.ts.map +1 -1
  117. package/dist/schemas.js +96 -6
  118. package/dist/schemas.js.map +1 -1
  119. package/dist/vocabulary.d.ts +21 -1
  120. package/dist/vocabulary.d.ts.map +1 -1
  121. package/dist/vocabulary.js +21 -0
  122. package/dist/vocabulary.js.map +1 -1
  123. package/docs/specs/cli.md +13 -9
  124. package/docs/specs/rest.md +33 -9
  125. package/package.json +1 -1
  126. package/skills/building-a-console/SKILL.md +1 -1
  127. package/skills/deploying-a-console/SKILL.md +5 -5
  128. package/skills/deploying-a-console/assets/secrets.example.json +2 -1
  129. package/skills/deploying-a-console/references/access.md +10 -6
  130. package/skills/deploying-a-console/references/resources.md +10 -7
  131. package/skills/working-the-board/SKILL.md +10 -9
  132. package/skills/working-the-board/references/project-rules.md +36 -0
  133. package/src/agent-name.ts +9 -0
  134. package/src/api.ts +37 -0
  135. package/src/cli/cli.ts +264 -26
  136. package/src/cli/format.ts +74 -10
  137. package/src/cli/help.ts +31 -11
  138. package/src/cli/main.ts +5 -0
  139. package/src/client.ts +47 -1
  140. package/src/components/calendar.tsx +252 -51
  141. package/src/components/connect.tsx +12 -6
  142. package/src/components/decision-list.tsx +2 -1
  143. package/src/components/documents.tsx +7 -3
  144. package/src/components/event-feed.tsx +16 -3
  145. package/src/components/people.tsx +244 -35
  146. package/src/components/projects.tsx +37 -14
  147. package/src/components/rules.tsx +167 -0
  148. package/src/components/runs.tsx +4 -2
  149. package/src/components/skills.tsx +22 -10
  150. package/src/components/task-form.tsx +1 -0
  151. package/src/components/tokens.tsx +2 -14
  152. package/src/components/ui.tsx +25 -2
  153. package/src/components/work.tsx +34 -28
  154. package/src/console.tsx +524 -45
  155. package/src/data.ts +52 -2
  156. package/src/i18n/en.ts +94 -14
  157. package/src/i18n/ko.ts +90 -14
  158. package/src/index.ts +16 -1
  159. package/src/limits.ts +4 -0
  160. package/src/router.ts +67 -12
  161. package/src/routes.ts +23 -3
  162. package/src/rules.ts +109 -0
  163. package/src/schemas.ts +121 -4
  164. package/src/styles/console.css +263 -17
  165. package/src/vocabulary.ts +30 -0
@@ -1,10 +1,17 @@
1
- import type { ReactNode } from "react";
2
- import { OPEN_TASK_STATES, type RestCategory, type RestPerson, type RestTask } from "../api.js";
1
+ import { type ReactNode, type SyntheticEvent, useState } from "react";
2
+ import {
3
+ OPEN_TASK_STATES,
4
+ type RestCategory,
5
+ type RestGoal,
6
+ type RestPerson,
7
+ type RestTask,
8
+ } from "../api.js";
3
9
  import {
4
10
  addDays,
5
11
  addMonths,
6
12
  daysBetween,
7
13
  firstDayOfWeek,
14
+ formatDay,
8
15
  formatDays,
9
16
  formatMonth,
10
17
  formatWeekday,
@@ -16,14 +23,16 @@ import {
16
23
  weekOf,
17
24
  } from "../dates.js";
18
25
  import { useLanguage, useWords } from "../i18n/index.js";
19
- import { cx, PersonChip, personName } from "./ui.js";
26
+ import { Avatar, CategoryBadge, cx, PersonChip, personName, StateBadge, Time } from "./ui.js";
20
27
 
21
28
  // The calendar of a project (ADR-0019): the month as a grid, or one week on its own. A task
22
29
  // 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).
30
+ // each week so that the bars of one week never cross; a task with one day alone, a due day or
31
+ // a start, sits on that day as a dot. On a project's calendar the person who has a task stands
32
+ // before its title, since the days hold several people's work. Who has what says whether the
33
+ // work is on its days. The task under the pointer is shown in full within its item, with the
34
+ // facts its own page lists, above or below it where there is more room. Takes its data as props
35
+ // and nothing from the network; the month or the week shown is the address's (ADR-0010).
27
36
 
28
37
  const isOpen = (task: RestTask): boolean => OPEN_TASK_STATES.includes(task.state);
29
38
 
@@ -39,6 +48,8 @@ export interface CalendarProps {
39
48
  readonly today?: string | undefined;
40
49
  /** The workspace's categories, for the legend to say what the colours mean. */
41
50
  readonly categories?: readonly RestCategory[] | undefined;
51
+ /** The project's goals, so that the task under the pointer is shown with its goal's title. */
52
+ readonly goals?: readonly RestGoal[] | undefined;
42
53
  readonly taskHref: (task: RestTask) => string;
43
54
  /** Where the calendar is on another month. */
44
55
  readonly monthHref: (month: string) => string;
@@ -104,49 +115,239 @@ function push(map: Map<string, RestTask[]>, day: string, task: RestTask): void {
104
115
  }
105
116
  }
106
117
 
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
- } {
112
- const due = new Map<string, RestTask[]>();
113
- const starting = new Map<string, RestTask[]>();
118
+ /** The tasks that sit on one day each, by day: due on it, or beginning on it with no due day yet. */
119
+ function onDays(tasks: readonly RestTask[]): ReadonlyMap<string, readonly RestTask[]> {
120
+ const on = new Map<string, RestTask[]>();
114
121
  for (const task of tasks) {
115
122
  if (spans(task)) {
116
123
  continue;
117
124
  }
118
- if (task.dueOn !== null) {
119
- push(due, task.dueOn, task);
120
- } else if (task.startsOn !== null) {
121
- push(starting, task.startsOn, task);
125
+ const day = task.dueOn ?? task.startsOn;
126
+ if (day !== null) {
127
+ push(on, day, task);
122
128
  }
123
129
  }
124
130
  // Within a day, what is still open comes first; the rest reads as what was.
125
- for (const list of due.values()) {
131
+ for (const list of on.values()) {
126
132
  list.sort((a, b) => Number(isOpen(b)) - Number(isOpen(a)) || a.number - b.number);
127
133
  }
128
- return { due, starting };
134
+ return on;
135
+ }
136
+
137
+ /** How a task reads: struck through once it is over, and marked once its due day is past. */
138
+ const tone = (task: RestTask, now: string): string =>
139
+ cx(
140
+ !isOpen(task) && "sc-calendar-over",
141
+ isOpen(task) && task.dueOn !== null && task.dueOn < now && "sc-calendar-late",
142
+ );
143
+
144
+ /** Where the card of the item under the pointer goes: the side with more room, kept within the window. */
145
+ export interface TipPlace {
146
+ readonly side: "above" | "below";
147
+ /** How far the card moves from the item's left edge, in pixels, to stay within the window: zero or less. */
148
+ readonly shift: number;
149
+ }
150
+
151
+ /** What the card may grow to, in pixels: 20rem at the usual root size, as the style says. */
152
+ const TIP_WIDTH = 320;
153
+ /** What stays between the card and the window's edge, in pixels. */
154
+ const TIP_MARGIN = 8;
155
+
156
+ /**
157
+ * Places the card beside an item, from the item's box and the window's size, both in pixels from
158
+ * the window's top left corner: above the item where the room above is at least the room below,
159
+ * else below it, and moved left where it would otherwise leave the window on the right.
160
+ */
161
+ export function placeTip(
162
+ item: { readonly top: number; readonly bottom: number; readonly left: number },
163
+ window: { readonly width: number; readonly height: number },
164
+ ): TipPlace {
165
+ const side = item.top >= window.height - item.bottom ? "above" : "below";
166
+ const left = Math.max(TIP_MARGIN, Math.min(item.left, window.width - TIP_WIDTH - TIP_MARGIN));
167
+ return { side, shift: Math.min(0, left - item.left) };
168
+ }
169
+
170
+ /** The item the pointer is on: a task on a day, since a bar has an item on each of its days, and where its card goes. */
171
+ interface Pointed {
172
+ readonly task: RestTask;
173
+ readonly day: string;
174
+ readonly place: TipPlace;
129
175
  }
130
176
 
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");
177
+ /** The item under the pointer, measured as the pointer arrives, since only the browser knows where it is. */
178
+ function usePointed(): [
179
+ Pointed | null,
180
+ (task: RestTask, day: string, item: HTMLElement) => void,
181
+ () => void,
182
+ ] {
183
+ const [pointed, setPointed] = useState<Pointed | null>(null);
184
+ const point = (task: RestTask, day: string, item: HTMLElement): void =>
185
+ setPointed({
186
+ task,
187
+ day,
188
+ place: placeTip(item.getBoundingClientRect(), {
189
+ width: window.innerWidth,
190
+ height: window.innerHeight,
191
+ }),
192
+ });
193
+ const leave = (): void => setPointed(null);
194
+ return [pointed, point, leave];
195
+ }
196
+
197
+ export interface TaskTipProps {
198
+ readonly task: RestTask;
199
+ /** The project's goals, so that the task's goal is named by its title; without them the row is left out. */
200
+ readonly goals?: readonly RestGoal[] | undefined;
201
+ readonly place: TipPlace;
202
+ }
203
+
204
+ /**
205
+ * The task under the pointer in full: its number and its title, then the facts its own page lists
206
+ * beside the text, those that are set. Lies within the item it belongs to, above or below it, and
207
+ * out of the accessible tree: the item's text is the title, and the facts are on the task's page.
208
+ */
209
+ export function TaskTip(props: TaskTipProps) {
210
+ const words = useWords();
211
+ const { tag } = useLanguage();
212
+ const { task, place } = props;
213
+ const goal = props.goals?.find((candidate) => candidate.id === task.goalId);
214
+ const day = (value: string): string => formatDay(value, tag);
215
+ return (
216
+ <div
217
+ className={cx(
218
+ "sc-task-tip",
219
+ place.side === "above" ? "sc-task-tip-above" : "sc-task-tip-below",
220
+ )}
221
+ style={{ left: place.shift }}
222
+ aria-hidden="true"
223
+ >
224
+ <p className="sc-task-tip-title">
225
+ #{task.number} {task.title}
226
+ </p>
227
+ <dl className="sc-task-tip-facts">
228
+ <div>
229
+ <dt>{words.task.state}</dt>
230
+ <dd>
231
+ <StateBadge state={task.state} />
232
+ </dd>
233
+ </div>
234
+ <div>
235
+ <dt>{words.task.form.priority}</dt>
236
+ <dd>{words.vocabulary.priority[task.priority]}</dd>
237
+ </div>
238
+ {task.category !== null && (
239
+ <div>
240
+ <dt>{words.task.category}</dt>
241
+ <dd>
242
+ <CategoryBadge category={task.category} />
243
+ </dd>
244
+ </div>
245
+ )}
246
+ {goal !== undefined && (
247
+ <div>
248
+ <dt>{words.task.goal}</dt>
249
+ <dd>{goal.title}</dd>
250
+ </div>
251
+ )}
252
+ <div>
253
+ <dt>{words.task.owner}</dt>
254
+ <dd>
255
+ <PersonChip person={task.owner} />
256
+ </dd>
257
+ </div>
258
+ <div>
259
+ <dt>{words.task.assignee}</dt>
260
+ <dd>
261
+ {task.assignee === null ? (
262
+ <span className="sc-muted">{words.common.nobody}</span>
263
+ ) : (
264
+ <PersonChip person={task.assignee} />
265
+ )}
266
+ </dd>
267
+ </div>
268
+ {(task.startsOn !== null || task.dueOn !== null) && (
269
+ <div>
270
+ <dt>{words.task.plan}</dt>
271
+ <dd>
272
+ {words.task.planned(
273
+ task.startsOn === null ? undefined : day(task.startsOn),
274
+ task.dueOn === null ? undefined : day(task.dueOn),
275
+ )}
276
+ </dd>
277
+ </div>
278
+ )}
279
+ {task.startedAt !== null && (
280
+ <div>
281
+ <dt>{words.task.started}</dt>
282
+ <dd>
283
+ <Time iso={task.startedAt} />
284
+ </dd>
285
+ </div>
286
+ )}
287
+ {task.doneAt !== null && (
288
+ <div>
289
+ <dt>{words.task.done}</dt>
290
+ <dd>
291
+ <Time iso={task.doneAt} />
292
+ </dd>
293
+ </div>
294
+ )}
295
+ <div>
296
+ <dt>{words.task.written}</dt>
297
+ <dd>
298
+ <Time iso={task.createdAt} />
299
+ </dd>
300
+ </div>
301
+ <div>
302
+ <dt>{words.task.changed}</dt>
303
+ <dd>
304
+ <Time iso={task.updatedAt} />
305
+ </dd>
306
+ </div>
307
+ </dl>
308
+ </div>
309
+ );
310
+ }
134
311
 
135
312
  interface WeekRowProps {
136
313
  readonly week: readonly GridDay[];
137
314
  readonly segments: readonly WeekSegment[];
138
- readonly due: ReadonlyMap<string, readonly RestTask[]>;
139
- readonly starting: ReadonlyMap<string, readonly RestTask[]>;
315
+ /** The tasks on one day each, by day. */
316
+ readonly on: ReadonlyMap<string, readonly RestTask[]>;
140
317
  readonly now: string;
141
318
  readonly taskHref: (task: RestTask) => string;
142
319
  /** What stands before a task's title where the tasks are of several projects: the project's mark. */
143
320
  readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
321
+ /** Whether the person who has a task stands before its title, where the days hold several people's work. */
322
+ readonly people?: boolean | undefined;
323
+ /** The item under the pointer, shown in full within that item, or none. */
324
+ readonly pointed: Pointed | null;
325
+ /** The project's goals, so that the item under the pointer names its goal; none across projects. */
326
+ readonly goals?: readonly RestGoal[] | undefined;
327
+ /** The pointer, or the focus, arrived on a task's item on a day. */
328
+ readonly onPoint: (task: RestTask, day: string, item: HTMLElement) => void;
329
+ /** It left. */
330
+ readonly onLeave: () => void;
144
331
  }
145
332
 
146
333
  /** One week as a row of cells: the bars across the days in their lanes, then the tasks on each day. */
147
334
  function WeekRow(props: WeekRowProps) {
148
- const words = useWords().calendar;
149
335
  const { week, segments, now } = props;
336
+ const pointing = (task: RestTask, day: string) => ({
337
+ onMouseEnter: (event: SyntheticEvent<HTMLElement>) =>
338
+ props.onPoint(task, day, event.currentTarget),
339
+ onMouseLeave: props.onLeave,
340
+ onFocus: (event: SyntheticEvent<HTMLElement>) => props.onPoint(task, day, event.currentTarget),
341
+ onBlur: props.onLeave,
342
+ });
343
+ const tipIn = (task: RestTask, day: string): ReactNode =>
344
+ props.pointed !== null && props.pointed.task.id === task.id && props.pointed.day === day ? (
345
+ <TaskTip task={task} goals={props.goals} place={props.pointed.place} />
346
+ ) : null;
347
+ const who = (task: RestTask): ReactNode =>
348
+ props.people === true && task.assignee !== null ? (
349
+ <Avatar person={task.assignee} size="xs" />
350
+ ) : null;
150
351
  const lanes = segments.reduce((most, segment) => Math.max(most, segment.lane + 1), 0);
151
352
  return (
152
353
  <tr>
@@ -179,28 +380,30 @@ function WeekRow(props: WeekRowProps) {
179
380
  `sc-category-${task.category?.color ?? "gray"}`,
180
381
  segment.from === column && segment.begins && "sc-calendar-bar-begins",
181
382
  segment.to === column && segment.ends && "sc-calendar-bar-ends",
182
- tone(task, task.dueOn ?? cell.day, now),
383
+ tone(task, now),
183
384
  )}
184
385
  href={props.taskHref(task)}
185
- title={`#${task.number} ${task.title}`}
386
+ {...pointing(task, cell.day)}
186
387
  >
187
388
  {segment.from === column && (
188
389
  <>
189
390
  {props.markOf?.(task)}
391
+ {who(task)}
190
392
  <span className="sc-calendar-task-title">{task.title}</span>
191
393
  </>
192
394
  )}
395
+ {tipIn(task, cell.day)}
193
396
  </a>
194
397
  );
195
398
  })}
196
- {(props.due.get(cell.day) ?? []).map((task) => {
399
+ {(props.on.get(cell.day) ?? []).map((task) => {
197
400
  const mark = props.markOf?.(task);
198
401
  return (
199
402
  <a
200
403
  key={task.id}
201
- className={cx("sc-calendar-task", tone(task, cell.day, now))}
404
+ className={cx("sc-calendar-task", tone(task, now))}
202
405
  href={props.taskHref(task)}
203
- title={`#${task.number} ${task.title}`}
406
+ {...pointing(task, cell.day)}
204
407
  >
205
408
  {mark === undefined || mark === null ? (
206
409
  <span
@@ -212,21 +415,12 @@ function WeekRow(props: WeekRowProps) {
212
415
  ) : (
213
416
  mark
214
417
  )}
418
+ {who(task)}
215
419
  <span className="sc-calendar-task-title">{task.title}</span>
420
+ {tipIn(task, cell.day)}
216
421
  </a>
217
422
  );
218
423
  })}
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
424
  </td>
231
425
  ))}
232
426
  </tr>
@@ -236,15 +430,15 @@ function WeekRow(props: WeekRowProps) {
236
430
  export function Calendar(props: CalendarProps) {
237
431
  const words = useWords().calendar;
238
432
  const { tag } = useLanguage();
433
+ const [pointed, point, leave] = usePointed();
239
434
  const now = props.today ?? todayOf();
240
435
  const firstDay = firstDayOfWeek(tag);
241
436
  const weeks = monthGrid(props.month, firstDay);
242
- const { due, starting } = onDays(props.tasks);
437
+ const on = onDays(props.tasks);
243
438
  const inMonth = (day: string) => monthOf(day) === props.month;
244
439
  const monthWeeks = weeks.map((week) => ({ week, segments: weekSegments(week, props.tasks) }));
245
440
  const nothing =
246
- [...due.keys()].every((day) => !inMonth(day)) &&
247
- [...starting.keys()].every((day) => !inMonth(day)) &&
441
+ [...on.keys()].every((day) => !inMonth(day)) &&
248
442
  monthWeeks.every(({ week, segments }) =>
249
443
  segments.every(
250
444
  (segment) => !week.slice(segment.from, segment.to + 1).some((cell) => cell.inMonth),
@@ -293,10 +487,14 @@ export function Calendar(props: CalendarProps) {
293
487
  key={week[0]?.day}
294
488
  week={week}
295
489
  segments={segments}
296
- due={due}
297
- starting={starting}
490
+ on={on}
298
491
  now={now}
299
492
  taskHref={props.taskHref}
493
+ pointed={pointed}
494
+ goals={props.goals}
495
+ onPoint={point}
496
+ onLeave={leave}
497
+ people
300
498
  />
301
499
  ))}
302
500
  </tbody>
@@ -335,15 +533,16 @@ export interface WeekProps {
335
533
  export function Week(props: WeekProps) {
336
534
  const words = useWords().calendar.week;
337
535
  const { tag } = useLanguage();
536
+ const [pointed, point, leave] = usePointed();
338
537
  const now = props.today ?? todayOf();
339
538
  const firstDay = firstDayOfWeek(tag);
340
539
  const days = weekOf(props.day ?? now, firstDay);
341
540
  const first = days[0] ?? now;
342
541
  const last = days[days.length - 1] ?? now;
343
542
  const week: GridDay[] = days.map((day) => ({ day, inMonth: true }));
344
- const { due, starting } = onDays(props.tasks);
543
+ const on = onDays(props.tasks);
345
544
  const segments = weekSegments(week, props.tasks);
346
- const nothing = segments.length === 0 && days.every((day) => !due.has(day) && !starting.has(day));
545
+ const nothing = segments.length === 0 && days.every((day) => !on.has(day));
347
546
  return (
348
547
  <div className="sc-calendar sc-week">
349
548
  <div className="sc-calendar-head">
@@ -382,11 +581,13 @@ export function Week(props: WeekProps) {
382
581
  <WeekRow
383
582
  week={week}
384
583
  segments={segments}
385
- due={due}
386
- starting={starting}
584
+ on={on}
387
585
  now={now}
388
586
  taskHref={props.taskHref}
389
587
  markOf={props.markOf}
588
+ pointed={pointed}
589
+ onPoint={point}
590
+ onLeave={leave}
390
591
  />
391
592
  </tbody>
392
593
  </table>
@@ -2,7 +2,7 @@ import { useState } from "react";
2
2
  import { normalizeConnectCode, type RestConnectRequest, type RestTokenInput } from "../api.js";
3
3
  import { useWords } from "../i18n/index.js";
4
4
  import { TokenForm } from "./tokens.js";
5
- import { Button, Callout, Time } from "./ui.js";
5
+ import { Button, Callout, CopyButton, Time } from "./ui.js";
6
6
 
7
7
  // Connecting an agent (ADR-0011): where a person types the code their agent showed, what asks
8
8
  // to connect under a code with the way to approve it, and the words that tell a person what to
@@ -106,16 +106,22 @@ export interface ConnectWordsProps {
106
106
  readonly origin: string;
107
107
  }
108
108
 
109
- /** What a person says to their agent to connect it, in words for someone who is not a developer. */
109
+ /**
110
+ * What a person says to their agent to connect it: the words to paste into its chat, as they
111
+ * are, with the way to copy them; and the commands for a person who runs them by hand.
112
+ */
110
113
  export function ConnectWords(props: ConnectWordsProps) {
111
114
  const words = useWords().connect.words;
115
+ const quote = words.quote(props.origin);
112
116
  return (
113
117
  <section className="sc-panel sc-connect" aria-label={words.title}>
114
118
  <h2 className="sc-section-title">{words.title}</h2>
115
- <p>
116
- {words.tellYourAgent} <q>{words.quote(props.origin)}</q> {words.doesTheRest}
117
- </p>
118
- <p>{words.thenApprove}</p>
119
+ <p>{words.tellYourAgent}</p>
120
+ <div className="sc-copyable">
121
+ <pre className="sc-code">{quote}</pre>
122
+ <CopyButton text={quote} size="sm" />
123
+ </div>
124
+ <p>{words.doesTheRest}</p>
119
125
  <p>{words.byHand}</p>
120
126
  <pre className="sc-code">
121
127
  npm install -g @skillcdn/console{"\n"}console login --url {props.origin}
@@ -1,5 +1,6 @@
1
1
  import { useState } from "react";
2
2
  import {
3
+ agentLabel,
3
4
  MAX_BODY_LENGTH,
4
5
  MAX_NOTE_LENGTH,
5
6
  type RestAnswerInput,
@@ -128,7 +129,7 @@ export function DecisionCard(props: DecisionCardProps) {
128
129
  <PersonChip person={decision.raisedBy} />{" "}
129
130
  {decision.run === null
130
131
  ? words.decisions.asked
131
- : words.decisions.askedThrough(decision.run.agent)}{" "}
132
+ : words.decisions.askedThrough(agentLabel(decision.run.agent))}{" "}
132
133
  <Time iso={decision.createdAt} />
133
134
  {task !== undefined && (
134
135
  <>
@@ -1,4 +1,5 @@
1
1
  import { type FormEvent, type ReactNode, useState } from "react";
2
+ import { agentLabel } from "../agent-name.js";
2
3
  import {
3
4
  foldersAbove,
4
5
  isDocumentPath,
@@ -59,7 +60,7 @@ export function DocumentCrumbs(props: {
59
60
  }
60
61
 
61
62
  /** Who wrote a version, and through which agent, as a chip and a word. */
62
- function Writer(props: {
63
+ export function Writer(props: {
63
64
  readonly by: RestDocumentSummary["updatedBy"];
64
65
  readonly agent: string | null;
65
66
  }) {
@@ -68,8 +69,11 @@ function Writer(props: {
68
69
  <>
69
70
  <PersonChip person={props.by} />
70
71
  {props.agent !== null && (
71
- <span className="sc-feed-agent" title={words.docs.asAgentTitle}>
72
- {words.feed.as(props.agent)}
72
+ <span
73
+ className="sc-feed-agent"
74
+ title={agentLabel(props.agent) === props.agent ? words.docs.asAgentTitle : props.agent}
75
+ >
76
+ {words.feed.as(agentLabel(props.agent))}
73
77
  </span>
74
78
  )}
75
79
  </>
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import type { RestEvent } from "../api.js";
2
+ import { agentLabel, type RestEvent } from "../api.js";
3
3
  import { en, type Messages } from "../i18n/en.js";
4
4
  import { useWords } from "../i18n/index.js";
5
5
  import { ShowMore, usePaged } from "./paged.js";
@@ -38,6 +38,14 @@ export function describeEvent(event: RestEvent, words: Messages = en): string {
38
38
  return feed.event.personJoined();
39
39
  case "person.role_changed":
40
40
  return feed.event.roleChanged(login, role(data.role));
41
+ case "person.listed":
42
+ return feed.event.personListed(login);
43
+ case "person.unlisted":
44
+ return feed.event.personUnlisted(login);
45
+ case "person.deactivated":
46
+ return feed.event.personDeactivated(login);
47
+ case "person.reactivated":
48
+ return feed.event.personReactivated(login);
41
49
  case "project.created":
42
50
  return feed.event.projectCreated(project);
43
51
  case "project.updated":
@@ -129,8 +137,13 @@ export function EventFeed(props: EventFeedProps) {
129
137
  <PersonChip person={event.actor} />
130
138
  )}
131
139
  {event.agent !== null && (
132
- <span className="sc-feed-agent" title={words.feed.asTitle}>
133
- {words.feed.as(event.agent)}
140
+ <span
141
+ className="sc-feed-agent"
142
+ title={
143
+ agentLabel(event.agent) === event.agent ? words.feed.asTitle : event.agent
144
+ }
145
+ >
146
+ {words.feed.as(agentLabel(event.agent))}
134
147
  </span>
135
148
  )}
136
149
  </span>