@skillcdn/console 0.1.10 → 0.1.12

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 (145) hide show
  1. package/README.md +6 -5
  2. package/dist/api.d.ts +6 -5
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +5 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts.map +1 -1
  7. package/dist/cli/cli.js +49 -9
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +2 -0
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +7 -6
  12. package/dist/cli/format.js.map +1 -1
  13. package/dist/cli/help.d.ts.map +1 -1
  14. package/dist/cli/help.js +10 -9
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +14 -5
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +13 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/components/board.d.ts +1 -4
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +6 -13
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/calendar.d.ts +21 -1
  25. package/dist/components/calendar.d.ts.map +1 -1
  26. package/dist/components/calendar.js +69 -9
  27. package/dist/components/calendar.js.map +1 -1
  28. package/dist/components/decision-list.d.ts.map +1 -1
  29. package/dist/components/decision-list.js +11 -9
  30. package/dist/components/decision-list.js.map +1 -1
  31. package/dist/components/event-feed.d.ts.map +1 -1
  32. package/dist/components/event-feed.js +11 -7
  33. package/dist/components/event-feed.js.map +1 -1
  34. package/dist/components/paged.d.ts +14 -0
  35. package/dist/components/paged.d.ts.map +1 -0
  36. package/dist/components/paged.js +28 -0
  37. package/dist/components/paged.js.map +1 -0
  38. package/dist/components/people.js +2 -2
  39. package/dist/components/people.js.map +1 -1
  40. package/dist/components/projects.d.ts +22 -2
  41. package/dist/components/projects.d.ts.map +1 -1
  42. package/dist/components/projects.js +37 -9
  43. package/dist/components/projects.js.map +1 -1
  44. package/dist/components/runs.d.ts.map +1 -1
  45. package/dist/components/runs.js +3 -3
  46. package/dist/components/runs.js.map +1 -1
  47. package/dist/components/shell.d.ts +2 -1
  48. package/dist/components/shell.d.ts.map +1 -1
  49. package/dist/components/shell.js +3 -3
  50. package/dist/components/shell.js.map +1 -1
  51. package/dist/components/task-form.js +2 -2
  52. package/dist/components/task-form.js.map +1 -1
  53. package/dist/components/task-list.d.ts +1 -1
  54. package/dist/components/task-list.d.ts.map +1 -1
  55. package/dist/components/task-list.js +6 -4
  56. package/dist/components/task-list.js.map +1 -1
  57. package/dist/components/task-view.js +1 -1
  58. package/dist/components/task-view.js.map +1 -1
  59. package/dist/components/ui.d.ts +7 -2
  60. package/dist/components/ui.d.ts.map +1 -1
  61. package/dist/components/ui.js +9 -4
  62. package/dist/components/ui.js.map +1 -1
  63. package/dist/components/work.d.ts.map +1 -1
  64. package/dist/components/work.js +7 -3
  65. package/dist/components/work.js.map +1 -1
  66. package/dist/console.css +149 -2
  67. package/dist/console.d.ts +38 -4
  68. package/dist/console.d.ts.map +1 -1
  69. package/dist/console.js +84 -23
  70. package/dist/console.js.map +1 -1
  71. package/dist/data.d.ts +5 -2
  72. package/dist/data.d.ts.map +1 -1
  73. package/dist/data.js +15 -2
  74. package/dist/data.js.map +1 -1
  75. package/dist/i18n/en.d.ts +17 -11
  76. package/dist/i18n/en.d.ts.map +1 -1
  77. package/dist/i18n/en.js +18 -14
  78. package/dist/i18n/en.js.map +1 -1
  79. package/dist/i18n/ko.d.ts.map +1 -1
  80. package/dist/i18n/ko.js +18 -14
  81. package/dist/i18n/ko.js.map +1 -1
  82. package/dist/index.d.ts +6 -5
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +6 -5
  85. package/dist/index.js.map +1 -1
  86. package/dist/limits.d.ts +6 -0
  87. package/dist/limits.d.ts.map +1 -1
  88. package/dist/limits.js +6 -0
  89. package/dist/limits.js.map +1 -1
  90. package/dist/marks.d.ts +12 -0
  91. package/dist/marks.d.ts.map +1 -0
  92. package/dist/marks.js +31 -0
  93. package/dist/marks.js.map +1 -0
  94. package/dist/router.d.ts +19 -3
  95. package/dist/router.d.ts.map +1 -1
  96. package/dist/router.js +28 -5
  97. package/dist/router.js.map +1 -1
  98. package/dist/routes.d.ts +7 -0
  99. package/dist/routes.d.ts.map +1 -1
  100. package/dist/routes.js +8 -0
  101. package/dist/routes.js.map +1 -1
  102. package/dist/schemas.d.ts +123 -3
  103. package/dist/schemas.d.ts.map +1 -1
  104. package/dist/schemas.js +42 -10
  105. package/dist/schemas.js.map +1 -1
  106. package/dist/vocabulary.d.ts +10 -4
  107. package/dist/vocabulary.d.ts.map +1 -1
  108. package/dist/vocabulary.js +8 -3
  109. package/dist/vocabulary.js.map +1 -1
  110. package/docs/specs/cli.md +7 -5
  111. package/docs/specs/rest.md +18 -12
  112. package/package.json +1 -1
  113. package/skills/building-a-console/SKILL.md +2 -2
  114. package/skills/working-the-board/SKILL.md +1 -1
  115. package/src/api.ts +12 -0
  116. package/src/cli/cli.ts +51 -8
  117. package/src/cli/format.ts +8 -6
  118. package/src/cli/help.ts +10 -10
  119. package/src/client.ts +29 -6
  120. package/src/components/board.tsx +6 -30
  121. package/src/components/calendar.tsx +168 -56
  122. package/src/components/decision-list.tsx +38 -33
  123. package/src/components/event-feed.tsx +34 -27
  124. package/src/components/paged.tsx +39 -0
  125. package/src/components/people.tsx +5 -5
  126. package/src/components/projects.tsx +132 -12
  127. package/src/components/runs.tsx +9 -3
  128. package/src/components/shell.tsx +8 -5
  129. package/src/components/task-form.tsx +2 -2
  130. package/src/components/task-list.tsx +35 -32
  131. package/src/components/task-view.tsx +1 -1
  132. package/src/components/ui.tsx +14 -5
  133. package/src/components/work.tsx +12 -2
  134. package/src/console.tsx +158 -25
  135. package/src/data.ts +20 -4
  136. package/src/i18n/en.ts +19 -14
  137. package/src/i18n/ko.ts +19 -14
  138. package/src/index.ts +9 -0
  139. package/src/limits.ts +6 -0
  140. package/src/marks.ts +40 -0
  141. package/src/router.ts +45 -7
  142. package/src/routes.ts +9 -0
  143. package/src/schemas.ts +48 -8
  144. package/src/styles/console.css +149 -2
  145. package/src/vocabulary.ts +14 -4
package/src/index.ts CHANGED
@@ -16,9 +16,11 @@ export {
16
16
  export {
17
17
  Calendar,
18
18
  type CalendarProps,
19
+ type WeekSegment,
19
20
  Workload,
20
21
  type WorkloadProps,
21
22
  type WorkloadRow,
23
+ weekSegments,
22
24
  workloadOf,
23
25
  } from "./components/calendar.js";
24
26
  export { CategoryList, type CategoryListProps } from "./components/categories.js";
@@ -64,6 +66,7 @@ export {
64
66
  type GoalViewProps,
65
67
  } from "./components/goals.js";
66
68
  export { Markdown, type MarkdownProps } from "./components/markdown.js";
69
+ export { PAGE_SIZE, ShowMore, usePaged } from "./components/paged.js";
67
70
  export { PeopleList, type PeopleListProps } from "./components/people.js";
68
71
  export {
69
72
  keyOf,
@@ -74,6 +77,7 @@ export {
74
77
  type ProjectFormProps,
75
78
  ProjectList,
76
79
  type ProjectListProps,
80
+ ProjectMark,
77
81
  ProjectRoleBadge,
78
82
  VISIBILITY_LABELS,
79
83
  } from "./components/projects.js";
@@ -123,6 +127,7 @@ export {
123
127
  PRIORITY_LABELS,
124
128
  PriorityBadge,
125
129
  ProgressBar,
130
+ personName,
126
131
  ROLE_LABELS,
127
132
  RoleBadge,
128
133
  Spinner,
@@ -136,7 +141,10 @@ export {
136
141
  type ConsoleApp,
137
142
  type ConsoleComponents,
138
143
  type ConsoleConfig,
144
+ type CustomPage,
145
+ type CustomPageContext,
139
146
  createConsole,
147
+ customPageLabel,
140
148
  DEFAULT_COMPONENTS,
141
149
  } from "./console.js";
142
150
  export { type ConsoleActions, type ConsoleData, useConsoleData } from "./data.js";
@@ -176,6 +184,7 @@ export {
176
184
  boardHref,
177
185
  calendarHref,
178
186
  connectHref,
187
+ customPageHref,
179
188
  type DocsView,
180
189
  decisionHref,
181
190
  docHref,
package/src/limits.ts CHANGED
@@ -64,3 +64,9 @@ export const MAX_CATEGORY_KEY_LENGTH = 40;
64
64
  export const MAX_CATEGORY_NAME_LENGTH = 60;
65
65
  /** How many categories one workspace may have: a vocabulary, not a tag cloud. */
66
66
  export const MAX_CATEGORIES = 50;
67
+ /** A project's mark: a letter or two, or a symbol, shown on its colour; counted in code units. */
68
+ export const MAX_PROJECT_SYMBOL_LENGTH = 8;
69
+ /** How large a project's own image for its mark may be, in bytes. */
70
+ export const MAX_PROJECT_ICON_BYTES = 512 * 1024;
71
+ /** How many items a listing answers with when none is asked for, and the most one may ask for. */
72
+ export const DEFAULT_PAGE_LIMIT = 50;
package/src/marks.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { COLOR_NAMES, type ColorName } from "./vocabulary.js";
2
+
3
+ // A project's mark: a symbol on a colour, so that projects tell apart at a glance. A project
4
+ // that names neither gets a colour from its key, the same everywhere and every time, and the
5
+ // first letter of its name.
6
+
7
+ /** A small, well-spread hash of a string: enough to pick a colour, nothing more. */
8
+ function hashOf(value: string): number {
9
+ let hash = 2166136261;
10
+ for (const char of value) {
11
+ hash ^= char.codePointAt(0) ?? 0;
12
+ hash = Math.imul(hash, 16777619) >>> 0;
13
+ }
14
+ return hash;
15
+ }
16
+
17
+ /** The colour of a project's mark: the one it names, else one chosen from its key. */
18
+ export function projectColorOf(project: {
19
+ readonly key: string;
20
+ readonly color: ColorName | null;
21
+ }): ColorName {
22
+ if (project.color !== null) {
23
+ return project.color;
24
+ }
25
+ // Gray is for what has no colour; a project chosen one gets one of the rest.
26
+ const choices = COLOR_NAMES.filter((name) => name !== "gray");
27
+ return choices[hashOf(project.key) % choices.length] ?? "gray";
28
+ }
29
+
30
+ /** The symbol of a project's mark: the one it names, else the first letter of its name. */
31
+ export function projectSymbolOf(project: {
32
+ readonly name: string;
33
+ readonly symbol: string | null;
34
+ }): string {
35
+ if (project.symbol !== null && project.symbol.length > 0) {
36
+ return project.symbol;
37
+ }
38
+ const first = [...project.name.trim()][0];
39
+ return first === undefined ? "?" : first.toUpperCase();
40
+ }
package/src/router.ts CHANGED
@@ -52,8 +52,13 @@ export type Route =
52
52
  | { readonly name: "decisions"; readonly project: string }
53
53
  | { readonly name: "new-decision"; readonly project: string }
54
54
  | { readonly name: "decision"; readonly project: string; readonly id: string }
55
- /** The calendar of a project: the month shown, or this one when the address names none. */
56
- | { readonly name: "calendar"; readonly project: string; readonly month: string | undefined }
55
+ /** The calendar of a project: the month shown, or this one when the address names none, narrowed as the board is. */
56
+ | {
57
+ readonly name: "calendar";
58
+ readonly project: string;
59
+ readonly month: string | undefined;
60
+ readonly filter: BoardFilter;
61
+ }
57
62
  | { readonly name: "roadmap"; readonly project: string }
58
63
  | { readonly name: "new-goal"; readonly project: string }
59
64
  | {
@@ -79,6 +84,14 @@ export type Route =
79
84
  | { readonly name: "connect"; readonly code: string | undefined }
80
85
  /** An address that moved: the page takes the person to `to` in its place. */
81
86
  | { readonly name: "moved"; readonly to: string }
87
+ /** A page a console adds of its own (`createConsole({ pages })`): on the workspace, or on a project. */
88
+ | { readonly name: "custom"; readonly level: "workspace"; readonly page: string }
89
+ | {
90
+ readonly name: "custom";
91
+ readonly level: "project";
92
+ readonly project: string;
93
+ readonly page: string;
94
+ }
82
95
  | { readonly name: "not-found" };
83
96
 
84
97
  /** The pages of a project, after its key. */
@@ -107,6 +120,8 @@ export const PATHS = {
107
120
  newToken: "/agents/new",
108
121
  /** The categories tasks are sorted into, kept by an administrator. */
109
122
  categories: "/categories",
123
+ /** Where the pages a console adds of its own are: `/pages/<key>`, and `/projects/<key>/pages/<page>` on a project. */
124
+ pages: "/pages",
110
125
  /** Where an agent's connection is approved (ADR-0011): `/connect/<code>`, or the code typed. */
111
126
  connect: "/connect",
112
127
  } as const;
@@ -218,6 +233,12 @@ export function matchRoute(pathname: string, search = ""): Route {
218
233
  ? { name: "person-agents", id }
219
234
  : NOT_FOUND;
220
235
  }
236
+ if (path.startsWith(`${PATHS.pages}/`)) {
237
+ const [page, ...more] = path.slice(PATHS.pages.length + 1).split("/");
238
+ return page !== undefined && PROJECT_KEY.test(page) && more.length === 0
239
+ ? { name: "custom", level: "workspace", page }
240
+ : NOT_FOUND;
241
+ }
221
242
  if (!path.startsWith(`${PATHS.project}/`)) {
222
243
  return NOT_FOUND;
223
244
  }
@@ -237,6 +258,11 @@ export function matchRoute(pathname: string, search = ""): Route {
237
258
  const view = docsViewOf(params, docPath);
238
259
  return view === undefined ? NOT_FOUND : { name: "docs", project, path: docPath, view };
239
260
  }
261
+ if (page === "pages") {
262
+ return third !== undefined && PROJECT_KEY.test(third) && rest.length === 0
263
+ ? { name: "custom", level: "project", project, page: third }
264
+ : NOT_FOUND;
265
+ }
240
266
  if (rest.length > 0) {
241
267
  return NOT_FOUND;
242
268
  }
@@ -280,7 +306,7 @@ export function matchRoute(pathname: string, search = ""): Route {
280
306
  if (month !== null && !isMonth(month)) {
281
307
  return NOT_FOUND;
282
308
  }
283
- return { name: "calendar", project, month: month ?? undefined };
309
+ return { name: "calendar", project, month: month ?? undefined, filter: boardFilterOf(params) };
284
310
  }
285
311
  if (page === "roadmap") {
286
312
  return { name: "roadmap", project };
@@ -307,6 +333,12 @@ export function projectHref(key: string, page?: ProjectPage): string {
307
333
 
308
334
  /** Where a project's board is, narrowed as the filter says; the plain board for an empty one. */
309
335
  export function boardHref(key: string, filter: BoardFilter = {}): string {
336
+ const parts = filterParts(filter);
337
+ return parts.length === 0 ? projectHref(key) : `${projectHref(key)}?${parts.join("&")}`;
338
+ }
339
+
340
+ /** The filter as the query carries it, a part per narrowing; nothing for the plain board. */
341
+ function filterParts(filter: BoardFilter): string[] {
310
342
  const parts: string[] = [];
311
343
  // An empty value means "none": it keeps its sign; "mine" is a flag and has no value.
312
344
  if (filter.category !== undefined) {
@@ -321,7 +353,7 @@ export function boardHref(key: string, filter: BoardFilter = {}): string {
321
353
  if (filter.mine === true) {
322
354
  parts.push("mine");
323
355
  }
324
- return parts.length === 0 ? projectHref(key) : `${projectHref(key)}?${parts.join("&")}`;
356
+ return parts;
325
357
  }
326
358
 
327
359
  /** Where a task is written: the form. */
@@ -345,10 +377,16 @@ export function decisionHref(key: string, id: string): string {
345
377
  return `${PATHS.project}/${key}/decisions/${id}`;
346
378
  }
347
379
 
348
- /** Where a project's calendar is, on a month, or on this one when none is named. */
349
- export function calendarHref(key: string, month?: string): string {
380
+ /** Where a project's calendar is, on a month, or on this one when none is named, narrowed as the filter says. */
381
+ export function calendarHref(key: string, month?: string, filter: BoardFilter = {}): string {
350
382
  const base = projectHref(key, "calendar");
351
- return month === undefined ? base : `${base}?month=${month}`;
383
+ const parts = [...(month === undefined ? [] : [`month=${month}`]), ...filterParts(filter)];
384
+ return parts.length === 0 ? base : `${base}?${parts.join("&")}`;
385
+ }
386
+
387
+ /** Where a page a console adds of its own is: on the workspace, or on a project when its key is given. */
388
+ export function customPageHref(page: string, project?: string): string {
389
+ return project === undefined ? `${PATHS.pages}/${page}` : `${projectHref(project)}/pages/${page}`;
352
390
  }
353
391
 
354
392
  /** Where a goal is set: the form. */
package/src/routes.ts CHANGED
@@ -114,3 +114,12 @@ export function projectPath(key: string, collection?: ProjectCollection, id?: st
114
114
  ? `${base}/${collection}`
115
115
  : `${base}/${collection}/${encodeURIComponent(id)}`;
116
116
  }
117
+
118
+ /**
119
+ * Where a project's own image for its mark is read, with the hash as a version where the image
120
+ * is known, so that a browser's copy is asked for again only when the image changes.
121
+ */
122
+ export function projectIconPath(key: string, icon?: { readonly sha256: string } | null): string {
123
+ const base = `${projectPath(key)}/icon`;
124
+ return icon === undefined || icon === null ? base : `${base}?v=${icon.sha256.slice(0, 16)}`;
125
+ }
package/src/schemas.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  MAX_PROJECT_DESCRIPTION_LENGTH,
22
22
  MAX_PROJECT_KEY_LENGTH,
23
23
  MAX_PROJECT_NAME_LENGTH,
24
+ MAX_PROJECT_SYMBOL_LENGTH,
24
25
  MAX_QUESTION_LENGTH,
25
26
  MAX_SECRET_LENGTH,
26
27
  MAX_SKILLS_ADDRESS_LENGTH,
@@ -35,10 +36,12 @@ import { hasForbiddenCodePoint } from "./text.js";
35
36
  import {
36
37
  ARTIFACT_KINDS,
37
38
  CATEGORY_COLORS,
39
+ COLOR_NAMES,
38
40
  EVENT_KINDS,
39
41
  GOAL_STATES,
40
42
  LINK_SOURCES,
41
43
  PERSON_ROLES,
44
+ PROJECT_ICON_TYPES,
42
45
  PROJECT_ROLES,
43
46
  PROJECT_VISIBILITIES,
44
47
  PROVIDER_KEYS,
@@ -160,7 +163,19 @@ const projectKey = z.string().check(
160
163
  /** The address of a project's skills, as the standard spells one; the console checks it as one. */
161
164
  const skillsAddress = line(MAX_SKILLS_ADDRESS_LENGTH);
162
165
 
166
+ /** A project's mark: a letter or two, or a symbol, with no space in it; shown on the project's colour. */
167
+ const projectSymbol = line(MAX_PROJECT_SYMBOL_LENGTH).check(
168
+ z.refine((value) => !/\s/.test(value), "must have no space in it"),
169
+ );
170
+
163
171
  /** A project of the workspace, as the asker sees it: with what they are in it. */
172
+ /** A project's own image for its mark: the hash it is kept by, which `projectIconPath` reads it with, and its kind. */
173
+ export const restProjectIconSchema = z.object({
174
+ sha256: z.string().check(z.regex(/^[0-9a-f]{64}$/)),
175
+ contentType: z.enum(PROJECT_ICON_TYPES),
176
+ });
177
+ export type RestProjectIcon = z.infer<typeof restProjectIconSchema>;
178
+
164
179
  export const restProjectSchema = z.object({
165
180
  id: uuid,
166
181
  key: projectKey,
@@ -170,6 +185,12 @@ export const restProjectSchema = z.object({
170
185
  visibility: z.enum(PROJECT_VISIBILITIES),
171
186
  /** The address of the project's skills at the SkillCDN deployment, canonical, or `null` for the organization's. */
172
187
  skillsAddress: z.nullable(z.string()),
188
+ /** The colour of the project's mark, one of the names, or `null` for one chosen from its key. */
189
+ color: z.nullable(z.enum(COLOR_NAMES)),
190
+ /** The project's mark, a letter or two or a symbol, or `null` for the first letter of its name. */
191
+ symbol: z.nullable(z.string()),
192
+ /** The project's own image for its mark, shown in place of the symbol, or `null` for none. */
193
+ icon: z.nullable(restProjectIconSchema),
173
194
  /** What the asker is in it: an owner configures the project, a member works on it. */
174
195
  role: z.enum(PROJECT_ROLES),
175
196
  /** How many decisions in it wait for a person. */
@@ -194,6 +215,9 @@ export const restProjectInputSchema = z.object({
194
215
  visibility: z.optional(z.enum(PROJECT_VISIBILITIES)),
195
216
  /** Left out or `null`: the organization's skills. */
196
217
  skillsAddress: z.optional(z.nullable(skillsAddress)),
218
+ /** Left out or `null`: a colour chosen from the key, and the first letter of the name. */
219
+ color: z.optional(z.nullable(z.enum(COLOR_NAMES))),
220
+ symbol: z.optional(z.nullable(projectSymbol)),
197
221
  });
198
222
  export type RestProjectInput = z.infer<typeof restProjectInputSchema>;
199
223
 
@@ -203,6 +227,8 @@ export const restProjectPatchSchema = z.object({
203
227
  description: z.optional(paragraphs(MAX_PROJECT_DESCRIPTION_LENGTH)),
204
228
  visibility: z.optional(z.enum(PROJECT_VISIBILITIES)),
205
229
  skillsAddress: z.optional(z.nullable(skillsAddress)),
230
+ color: z.optional(z.nullable(z.enum(COLOR_NAMES))),
231
+ symbol: z.optional(z.nullable(projectSymbol)),
206
232
  });
207
233
  export type RestProjectPatch = z.infer<typeof restProjectPatchSchema>;
208
234
 
@@ -313,8 +339,8 @@ export const restTaskSchema = z.object({
313
339
  });
314
340
  export type RestTask = z.infer<typeof restTaskSchema>;
315
341
 
316
- /** `GET /api/v1/projects/<key>/tasks`: the board, newest first. */
317
- export const restTasksSchema = z.object({ items: z.array(restTaskSchema) });
342
+ /** `GET /api/v1/projects/<key>/tasks`: a page of the board, newest first, and whether there is more after it. */
343
+ export const restTasksSchema = z.object({ items: z.array(restTaskSchema), more: z.boolean() });
318
344
  export type RestTasks = z.infer<typeof restTasksSchema>;
319
345
 
320
346
  const taskLinkInput = z.object({
@@ -449,12 +475,21 @@ export const restDecisionSchema = z.object({
449
475
  });
450
476
  export type RestDecision = z.infer<typeof restDecisionSchema>;
451
477
 
452
- /** `GET /api/v1/projects/<key>/decisions`: the ones that wait first, newest first within each. */
453
- export const restDecisionsSchema = z.object({ items: z.array(restDecisionSchema) });
478
+ /** `GET /api/v1/projects/<key>/decisions`: a page, the ones that wait first, newest first within each, and whether there is more. */
479
+ export const restDecisionsSchema = z.object({
480
+ items: z.array(restDecisionSchema),
481
+ more: z.boolean(),
482
+ });
454
483
  export type RestDecisions = z.infer<typeof restDecisionsSchema>;
455
484
 
456
- /** The project a thing on a person's plate belongs to, named for the page to say and link it. */
457
- const projectRef = z.object({ key: projectKey, name: z.string() });
485
+ /** The project a thing on a person's plate belongs to, named and marked for the page to say, link and show it. */
486
+ const projectRef = z.object({
487
+ key: projectKey,
488
+ name: z.string(),
489
+ color: z.nullable(z.enum(COLOR_NAMES)),
490
+ symbol: z.nullable(z.string()),
491
+ icon: z.nullable(restProjectIconSchema),
492
+ });
458
493
 
459
494
  /**
460
495
  * `GET /api/v1/work`: what is on the asker's plate across the projects they may see (ADR-0019):
@@ -598,6 +633,11 @@ export const restRunSchema = z.object({
598
633
  status: z.enum(RUN_STATUSES),
599
634
  startedAt: instant,
600
635
  endedAt: z.nullable(instant),
636
+ /**
637
+ * When the agent was last heard from on this run: when it began, reported, handed in or
638
+ * asked. The board cannot know whether an agent still runs; this is what it knows.
639
+ */
640
+ lastHeardAt: instant,
601
641
  /** What the agent said when the run ended, in Markdown, or `null`. */
602
642
  summary: z.nullable(z.string()),
603
643
  /** Oldest first. */
@@ -608,8 +648,8 @@ export const restRunSchema = z.object({
608
648
  });
609
649
  export type RestRun = z.infer<typeof restRunSchema>;
610
650
 
611
- /** `GET /api/v1/projects/<key>/runs`: the runs, newest first. */
612
- export const restRunsSchema = z.object({ items: z.array(restRunSchema) });
651
+ /** `GET /api/v1/projects/<key>/runs`: a page of the runs, newest first, and whether there is more. */
652
+ export const restRunsSchema = z.object({ items: z.array(restRunSchema), more: z.boolean() });
613
653
  export type RestRuns = z.infer<typeof restRunsSchema>;
614
654
 
615
655
  /** `GET .../events?after=`: what happened after that number, oldest first. */
@@ -1197,6 +1197,7 @@ table {
1197
1197
  flex: 1 1 0;
1198
1198
  grid-auto-flow: column;
1199
1199
  grid-auto-columns: minmax(var(--sc-column-width), 1fr);
1200
+ grid-template-rows: minmax(0, 1fr);
1200
1201
  gap: var(--sc-space-3);
1201
1202
  min-height: 24rem;
1202
1203
  overflow-x: auto;
@@ -1209,6 +1210,8 @@ table {
1209
1210
  display: flex;
1210
1211
  flex-direction: column;
1211
1212
  min-height: 0;
1213
+ /* The body scrolls; the column clips, so that nothing of it reaches the board's own scrolling. */
1214
+ overflow: hidden;
1212
1215
  border-radius: var(--sc-radius-xl);
1213
1216
  scroll-snap-align: start;
1214
1217
  }
@@ -2575,15 +2578,89 @@ table {
2575
2578
  white-space: nowrap;
2576
2579
  }
2577
2580
 
2578
- .sc-calendar-task-over {
2581
+ .sc-calendar-over {
2579
2582
  color: var(--sc-color-text-faint);
2580
2583
  text-decoration: line-through;
2581
2584
  }
2582
2585
 
2583
- .sc-calendar-task-late {
2586
+ .sc-calendar-late {
2584
2587
  color: var(--sc-color-danger);
2585
2588
  }
2586
2589
 
2590
+ /* A task planned across days lies across them: a bar in a lane of its own, in its category's
2591
+ colour, reaching over the gap between the cells so that it reads as one. */
2592
+ .sc-calendar-lane,
2593
+ .sc-calendar-bar {
2594
+ display: flex;
2595
+ align-items: center;
2596
+ height: 1.125rem;
2597
+ margin-top: 2px;
2598
+ }
2599
+
2600
+ .sc-calendar-bar {
2601
+ margin-inline: calc(-1 * var(--sc-space-2) - 0.125rem - 1px);
2602
+ padding-inline: var(--sc-space-2);
2603
+ border-block: 1px solid color-mix(in srgb, var(--sc-category-line) 45%, transparent);
2604
+ background: var(--sc-category-fill);
2605
+ color: var(--sc-category-line);
2606
+ font-size: var(--sc-text-xs);
2607
+ line-height: 1;
2608
+ text-decoration: none;
2609
+ overflow: hidden;
2610
+ white-space: nowrap;
2611
+ }
2612
+
2613
+ .sc-calendar-bar:hover {
2614
+ filter: brightness(1.15);
2615
+ }
2616
+
2617
+ .sc-calendar-bar-begins {
2618
+ margin-left: 0;
2619
+ border-left: 1px solid color-mix(in srgb, var(--sc-category-line) 45%, transparent);
2620
+ border-start-start-radius: var(--sc-radius-full);
2621
+ border-end-start-radius: var(--sc-radius-full);
2622
+ }
2623
+
2624
+ .sc-calendar-bar-ends {
2625
+ margin-right: 0;
2626
+ border-right: 1px solid color-mix(in srgb, var(--sc-category-line) 45%, transparent);
2627
+ border-start-end-radius: var(--sc-radius-full);
2628
+ border-end-end-radius: var(--sc-radius-full);
2629
+ }
2630
+
2631
+ .sc-calendar-bar.sc-calendar-late {
2632
+ border-color: color-mix(in srgb, var(--sc-color-danger) 70%, transparent);
2633
+ color: var(--sc-color-danger);
2634
+ }
2635
+
2636
+ .sc-calendar-bar.sc-calendar-over {
2637
+ opacity: 0.55;
2638
+ }
2639
+
2640
+ .sc-calendar-legend {
2641
+ display: flex;
2642
+ flex-wrap: wrap;
2643
+ gap: var(--sc-space-1) var(--sc-space-4);
2644
+ margin: 0;
2645
+ color: var(--sc-color-text-muted);
2646
+ font-size: var(--sc-text-xs);
2647
+ }
2648
+
2649
+ .sc-calendar-legend div {
2650
+ display: flex;
2651
+ align-items: center;
2652
+ gap: var(--sc-space-1);
2653
+ }
2654
+
2655
+ .sc-calendar-legend dt {
2656
+ display: flex;
2657
+ align-items: center;
2658
+ }
2659
+
2660
+ .sc-calendar-legend dd {
2661
+ margin: 0;
2662
+ }
2663
+
2587
2664
  .sc-calendar-task-start {
2588
2665
  color: var(--sc-color-text-muted);
2589
2666
  }
@@ -2593,6 +2670,69 @@ table {
2593
2670
  color: var(--sc-color-text-faint);
2594
2671
  }
2595
2672
 
2673
+ /* The button under a list shown a page at a time. */
2674
+ .sc-show-more {
2675
+ margin-top: var(--sc-space-3);
2676
+ }
2677
+
2678
+ /* A project's mark: its symbol on its colour. */
2679
+ .sc-mark {
2680
+ display: inline-flex;
2681
+ flex: none;
2682
+ align-items: center;
2683
+ justify-content: center;
2684
+ border-radius: var(--sc-radius-sm);
2685
+ background: var(--sc-category-fill);
2686
+ color: var(--sc-category-line);
2687
+ font-weight: var(--sc-weight-bold);
2688
+ line-height: 1;
2689
+ vertical-align: middle;
2690
+ }
2691
+
2692
+ .sc-mark-sm {
2693
+ width: 1.125rem;
2694
+ height: 1.125rem;
2695
+ margin-right: var(--sc-space-1);
2696
+ font-size: 0.625rem;
2697
+ }
2698
+
2699
+ .sc-mark-md {
2700
+ width: 2rem;
2701
+ height: 2rem;
2702
+ border-radius: var(--sc-radius-md);
2703
+ font-size: var(--sc-text-sm);
2704
+ }
2705
+
2706
+ .sc-mark-lg {
2707
+ width: 3rem;
2708
+ height: 3rem;
2709
+ border-radius: var(--sc-radius-lg);
2710
+ font-size: var(--sc-text-xl);
2711
+ }
2712
+
2713
+ .sc-mark-row {
2714
+ align-items: end;
2715
+ }
2716
+
2717
+ /* A project's own image, in place of the symbol, filling the mark. */
2718
+ .sc-mark-pictured {
2719
+ padding: 0;
2720
+ overflow: hidden;
2721
+ background: var(--sc-color-bg-inset);
2722
+ }
2723
+
2724
+ .sc-mark-image {
2725
+ display: block;
2726
+ width: 100%;
2727
+ height: 100%;
2728
+ object-fit: cover;
2729
+ }
2730
+
2731
+ .sc-input-file {
2732
+ padding-block: calc(var(--sc-space-2) - 2px);
2733
+ font-size: var(--sc-text-sm);
2734
+ }
2735
+
2596
2736
  .sc-calendar-below {
2597
2737
  display: grid;
2598
2738
  align-items: start;
@@ -2921,6 +3061,13 @@ table {
2921
3061
  display: none;
2922
3062
  }
2923
3063
 
3064
+ .sc-calendar-lane,
3065
+ .sc-calendar-bar {
3066
+ height: 4px;
3067
+ padding: 0;
3068
+ border: 0;
3069
+ }
3070
+
2924
3071
  .sc-feed-item {
2925
3072
  grid-template-columns: 1fr;
2926
3073
  gap: var(--sc-space-1);
package/src/vocabulary.ts CHANGED
@@ -31,10 +31,10 @@ export const GOAL_STATES = ["planned", "active", "done", "dropped"] as const;
31
31
  export type GoalState = (typeof GOAL_STATES)[number];
32
32
 
33
33
  /**
34
- * The colours a category may be shown in: names, so that a custom console paints them in its
35
- * own tokens and no value an administrator typed reaches a stylesheet.
34
+ * The colours a category, or a project's mark, may be shown in: names, so that a custom console
35
+ * paints them in its own tokens and no value a person typed reaches a stylesheet.
36
36
  */
37
- export const CATEGORY_COLORS = [
37
+ export const COLOR_NAMES = [
38
38
  "gray",
39
39
  "blue",
40
40
  "green",
@@ -45,7 +45,16 @@ export const CATEGORY_COLORS = [
45
45
  "yellow",
46
46
  ] as const;
47
47
 
48
- export type CategoryColor = (typeof CATEGORY_COLORS)[number];
48
+ export type ColorName = (typeof COLOR_NAMES)[number];
49
+
50
+ /** The colours of the categories: the same names. */
51
+ export const CATEGORY_COLORS = COLOR_NAMES;
52
+
53
+ export type CategoryColor = ColorName;
54
+
55
+ /** The kinds of image a project's own mark may be: what a browser shows in place, and nothing that can run. */
56
+ export const PROJECT_ICON_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] as const;
57
+ export type ProjectIconType = (typeof PROJECT_ICON_TYPES)[number];
49
58
 
50
59
  /** The identity providers people may sign in through: the git host, and Google (Workspace). */
51
60
  export const PROVIDER_KEYS = ["gh", "google"] as const;
@@ -132,6 +141,7 @@ export const EVENT_KINDS = [
132
141
  "run.started",
133
142
  "run.reported",
134
143
  "run.handed_in",
144
+ "run.taken_over",
135
145
  "run.ended",
136
146
  "document.written",
137
147
  "document.archived",