@skillcdn/console 0.3.1 → 0.3.2

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 (116) hide show
  1. package/README.md +2 -2
  2. package/dist/api.d.ts +3 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +3 -3
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +4 -0
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +105 -18
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +9 -7
  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 +13 -11
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/cli/main.js +1 -0
  17. package/dist/cli/main.js.map +1 -1
  18. package/dist/client.d.ts +13 -8
  19. package/dist/client.d.ts.map +1 -1
  20. package/dist/client.js +10 -2
  21. package/dist/client.js.map +1 -1
  22. package/dist/compat.d.ts +28 -6
  23. package/dist/compat.d.ts.map +1 -1
  24. package/dist/compat.js +55 -14
  25. package/dist/compat.js.map +1 -1
  26. package/dist/components/board.d.ts.map +1 -1
  27. package/dist/components/board.js +6 -4
  28. package/dist/components/board.js.map +1 -1
  29. package/dist/components/calendar.d.ts +13 -7
  30. package/dist/components/calendar.d.ts.map +1 -1
  31. package/dist/components/calendar.js +36 -24
  32. package/dist/components/calendar.js.map +1 -1
  33. package/dist/components/picker.d.ts +39 -0
  34. package/dist/components/picker.d.ts.map +1 -0
  35. package/dist/components/picker.js +155 -0
  36. package/dist/components/picker.js.map +1 -0
  37. package/dist/components/task-form.d.ts +18 -4
  38. package/dist/components/task-form.d.ts.map +1 -1
  39. package/dist/components/task-form.js +56 -13
  40. package/dist/components/task-form.js.map +1 -1
  41. package/dist/components/task-list.js +2 -2
  42. package/dist/components/task-list.js.map +1 -1
  43. package/dist/components/task-view.d.ts +7 -1
  44. package/dist/components/task-view.d.ts.map +1 -1
  45. package/dist/components/task-view.js +6 -5
  46. package/dist/components/task-view.js.map +1 -1
  47. package/dist/components/ui.d.ts +7 -0
  48. package/dist/components/ui.d.ts.map +1 -1
  49. package/dist/components/ui.js +11 -0
  50. package/dist/components/ui.js.map +1 -1
  51. package/dist/components/work.d.ts.map +1 -1
  52. package/dist/components/work.js +5 -3
  53. package/dist/components/work.js.map +1 -1
  54. package/dist/console.css +163 -11
  55. package/dist/console.d.ts.map +1 -1
  56. package/dist/console.js +24 -4
  57. package/dist/console.js.map +1 -1
  58. package/dist/data.d.ts +11 -1
  59. package/dist/data.d.ts.map +1 -1
  60. package/dist/data.js +22 -1
  61. package/dist/data.js.map +1 -1
  62. package/dist/i18n/en.d.ts +11 -3
  63. package/dist/i18n/en.d.ts.map +1 -1
  64. package/dist/i18n/en.js +16 -8
  65. package/dist/i18n/en.js.map +1 -1
  66. package/dist/i18n/ko.d.ts.map +1 -1
  67. package/dist/i18n/ko.js +17 -9
  68. package/dist/i18n/ko.js.map +1 -1
  69. package/dist/index.d.ts +3 -2
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +3 -2
  72. package/dist/index.js.map +1 -1
  73. package/dist/limits.d.ts +2 -0
  74. package/dist/limits.d.ts.map +1 -1
  75. package/dist/limits.js +2 -0
  76. package/dist/limits.js.map +1 -1
  77. package/dist/schemas.d.ts +12 -11
  78. package/dist/schemas.d.ts.map +1 -1
  79. package/dist/schemas.js +11 -8
  80. package/dist/schemas.js.map +1 -1
  81. package/dist/vocabulary.d.ts +2 -0
  82. package/dist/vocabulary.d.ts.map +1 -1
  83. package/dist/vocabulary.js +2 -0
  84. package/dist/vocabulary.js.map +1 -1
  85. package/docs/specs/cli.md +9 -9
  86. package/docs/specs/rest.md +14 -14
  87. package/package.json +1 -1
  88. package/skills/building-a-console/SKILL.md +2 -2
  89. package/skills/deploying-a-console/SKILL.md +2 -2
  90. package/skills/deploying-a-console/references/resources.md +1 -1
  91. package/skills/working-the-board/SKILL.md +3 -3
  92. package/skills/working-the-board/references/terms.md +2 -2
  93. package/src/api.ts +5 -0
  94. package/src/cli/cli.ts +131 -18
  95. package/src/cli/format.ts +11 -7
  96. package/src/cli/help.ts +15 -11
  97. package/src/cli/main.ts +1 -0
  98. package/src/client.ts +23 -10
  99. package/src/compat.ts +63 -14
  100. package/src/components/board.tsx +10 -4
  101. package/src/components/calendar.tsx +68 -32
  102. package/src/components/picker.tsx +297 -0
  103. package/src/components/task-form.tsx +149 -63
  104. package/src/components/task-list.tsx +2 -2
  105. package/src/components/task-view.tsx +23 -9
  106. package/src/components/ui.tsx +25 -0
  107. package/src/components/work.tsx +6 -4
  108. package/src/console.tsx +29 -3
  109. package/src/data.ts +44 -1
  110. package/src/i18n/en.ts +16 -8
  111. package/src/i18n/ko.ts +17 -9
  112. package/src/index.ts +16 -1
  113. package/src/limits.ts +2 -0
  114. package/src/schemas.ts +11 -7
  115. package/src/styles/console.css +163 -11
  116. package/src/vocabulary.ts +3 -0
@@ -145,6 +145,31 @@ export function PersonChip(props: { readonly person: RestPerson; readonly size?:
145
145
  );
146
146
  }
147
147
 
148
+ /** How many of a task's workers are shown as pictures before the rest are a count. */
149
+ const WORKERS_SHOWN = 3;
150
+
151
+ /** The people at work on a task (ADR-0037), as pictures side by side with a count of the rest; nothing while nobody is. */
152
+ export function Workers(props: {
153
+ readonly workers: readonly RestPerson[];
154
+ readonly size?: "xs" | "sm";
155
+ /** How many are shown as pictures; the rest are a count. */
156
+ readonly shown?: number;
157
+ }) {
158
+ if (props.workers.length === 0) {
159
+ return null;
160
+ }
161
+ const shown = props.shown ?? WORKERS_SHOWN;
162
+ const rest = props.workers.length - shown;
163
+ return (
164
+ <span className="sc-workers" title={props.workers.map(personName).join(", ")}>
165
+ {props.workers.slice(0, shown).map((worker) => (
166
+ <Avatar key={worker.id} person={worker} size={props.size ?? "sm"} />
167
+ ))}
168
+ {rest > 0 && <span className="sc-workers-more">+{rest}</span>}
169
+ </span>
170
+ );
171
+ }
172
+
148
173
  /** The words of the vocabulary as people read them, in English: the default pack's. */
149
174
  export const STATE_LABELS: Readonly<Record<TaskState, string>> = {
150
175
  idea: "Idea",
@@ -1,13 +1,13 @@
1
1
  import type { RestTask, RestWork, WeekStart } from "../api.js";
2
- import { addDays, today as todayOf } from "../dates.js";
3
- import { useWords } from "../i18n/index.js";
2
+ import { deviceLanguage, firstDayOfWeek, today as todayOf, weekOf } from "../dates.js";
3
+ import { useLanguage, useWords } from "../i18n/index.js";
4
4
  import { Week } from "./calendar.js";
5
5
  import { ProjectMark } from "./projects.js";
6
6
  import { TaskList } from "./task-list.js";
7
7
  import { PersonChip, Time } from "./ui.js";
8
8
 
9
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, and what of theirs was done or
10
+ // top, with the open tasks they work on on its days, and what of theirs was done or
11
11
  // dropped on them, as a project's calendar shows it; below, the open tasks by when they are
12
12
  // due, and the decisions that wait for a person. Takes its data as props and nothing from the
13
13
  // network; the week shown is the address's (ADR-0010).
@@ -35,8 +35,10 @@ export interface WorkListProps {
35
35
  export function WorkList(props: WorkListProps) {
36
36
  const words = useWords();
37
37
  const work = words.work;
38
+ const { tag } = useLanguage();
38
39
  const now = props.today ?? todayOf();
39
- const weekEnd = addDays(now, 6);
40
+ // This week ends with the week's last day, as the workspace begins its weeks (ADR-0039).
41
+ const weekEnd = weekOf(now, firstDayOfWeek(deviceLanguage(tag), props.weekStart)).at(-1) ?? now;
40
42
  const { tasks, decisions } = props.work;
41
43
  const closed = props.work.closed ?? [];
42
44
  const projectOf = new Map([...tasks, ...closed].map((item) => [item.task.id, item.project]));
package/src/console.tsx CHANGED
@@ -600,6 +600,28 @@ function TaskPage(props: {
600
600
  (fetched !== undefined && (fetched.id === route.id || fetched.number === route.number)
601
601
  ? fetched
602
602
  : undefined);
603
+ // The task this one is part of: among the board's, or asked for by itself when it was put away.
604
+ const parentId = task?.parentId ?? null;
605
+ const parentOnBoard =
606
+ parentId === null ? undefined : data.tasks.find((candidate) => candidate.id === parentId);
607
+ const [parentFetched, setParentFetched] = useState<RestTask | undefined>(undefined);
608
+ const parentWanted =
609
+ parentId !== null && parentOnBoard === undefined && data.projectLoaded ? parentId : undefined;
610
+ useEffect(() => {
611
+ if (parentWanted === undefined) {
612
+ return;
613
+ }
614
+ const controller = new AbortController();
615
+ fetchTask(parentWanted, controller.signal)
616
+ .then((found) => {
617
+ if (!controller.signal.aborted) {
618
+ setParentFetched(found);
619
+ }
620
+ })
621
+ .catch(() => undefined);
622
+ return () => controller.abort();
623
+ }, [parentWanted, fetchTask]);
624
+ const parent = parentOnBoard ?? (parentFetched?.id === parentId ? parentFetched : undefined);
603
625
  const [history, setHistory] = useState<readonly RestEvent[] | undefined>(undefined);
604
626
  const [comments, setComments] = useState<readonly RestComment[] | undefined>(undefined);
605
627
  const { taskHistory, taskComments } = data.actions;
@@ -653,6 +675,9 @@ function TaskPage(props: {
653
675
  task={task}
654
676
  people={data.people.filter(inWorkspace)}
655
677
  tasks={data.tasks}
678
+ parent={parent}
679
+ findTasks={data.actions.findTasks}
680
+ findGoals={data.actions.findGoals}
656
681
  decisions={data.decisions}
657
682
  runs={data.runs}
658
683
  comments={comments}
@@ -805,9 +830,9 @@ function ProjectPage(props: {
805
830
  <section className="sc-panel sc-panel-form" aria-label={words.task.newAria}>
806
831
  <TaskForm
807
832
  people={data.people.filter(inWorkspace)}
808
- parents={data.tasks}
833
+ findTasks={data.actions.findTasks}
809
834
  categories={data.categories}
810
- goals={data.goals}
835
+ findGoals={data.actions.findGoals}
811
836
  busy={busy}
812
837
  onSubmit={(input: RestTaskInput) =>
813
838
  void act(async () => {
@@ -907,6 +932,7 @@ function ProjectPage(props: {
907
932
  <Workload
908
933
  tasks={shown}
909
934
  today={now}
935
+ weekStart={weekStartOf(data.me?.workspace)}
910
936
  personHref={(person) =>
911
937
  boardHref(key, person === undefined ? {} : { person: person.id })
912
938
  }
@@ -1796,7 +1822,7 @@ function WorkspaceSettingsPage(props: {
1796
1822
  }
1797
1823
 
1798
1824
  /** A file a person chose, as the client sends it: its name, its bytes and the type the browser says. */
1799
- /** Whether a person is in: who a picker of assignees and of members offers (ADR-0022). */
1825
+ /** Whether a person is in: who a picker of workers and of members offers (ADR-0022). */
1800
1826
  const inWorkspace = (person: { readonly status: "active" | "deactivated" }): boolean =>
1801
1827
  person.status === "active";
1802
1828
 
package/src/data.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  type RestGoal,
25
25
  type RestGoalInput,
26
26
  type RestGoalPatch,
27
+ type RestGoals,
27
28
  type RestListing,
28
29
  type RestListingInput,
29
30
  type RestMe,
@@ -107,6 +108,18 @@ export interface ConsoleActions {
107
108
  page: { readonly limit: number; readonly offset: number },
108
109
  signal?: AbortSignal,
109
110
  ): Promise<RestTasks>;
111
+ /** What words find among the current tasks of the project, a page at a time, for a picker (ADR-0037); the first page of them for no words. */
112
+ findTasks(
113
+ query: string,
114
+ page: { readonly limit: number; readonly offset: number },
115
+ signal?: AbortSignal,
116
+ ): Promise<RestTasks>;
117
+ /** What words find among the open goals of the project, a page at a time, for a picker. */
118
+ findGoals(
119
+ query: string,
120
+ page: { readonly limit: number; readonly offset: number },
121
+ signal?: AbortSignal,
122
+ ): Promise<RestGoals>;
110
123
  raiseDecision(input: RestDecisionInput): Promise<RestDecision>;
111
124
  answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
112
125
  /** Grows a decision's record: its context, or what followed. */
@@ -265,6 +278,32 @@ async function readTasks(
265
278
  return { items };
266
279
  }
267
280
 
281
+ /** How many pages of the roadmap are read before it is shown as it stands. */
282
+ const GOALS_MAX_PAGES = 20;
283
+
284
+ /**
285
+ * The goals of a project, whole: page after page while the server says there is more, since
286
+ * the roadmap, the calendar and the filters draw them all.
287
+ */
288
+ async function readGoals(
289
+ scope: ProjectClient,
290
+ signal: AbortSignal | undefined,
291
+ ): Promise<{ readonly items: RestGoal[] }> {
292
+ const items: RestGoal[] = [];
293
+ for (let page = 0; page < GOALS_MAX_PAGES; page += 1) {
294
+ const result = await scope.goals({ limit: LIST_LIMIT, offset: page * LIST_LIMIT }, signal);
295
+ items.push(...result.items);
296
+ if (!result.more) {
297
+ break;
298
+ }
299
+ }
300
+ return { items };
301
+ }
302
+
303
+ /** The words of a search as the API takes them: none for a blank. */
304
+ const searchWordsOf = (query: string): { readonly q?: string } =>
305
+ query.trim() === "" ? {} : { q: query.trim() };
306
+
268
307
  /** How many pages of the decisions are read before the record is shown as it stands. */
269
308
  const DECISIONS_MAX_PAGES = 20;
270
309
 
@@ -353,7 +392,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
353
392
  scope.get(signal),
354
393
  readTasks(scope, signal),
355
394
  readDecisions(scope, signal),
356
- scope.goals({}, signal),
395
+ readGoals(scope, signal),
357
396
  scope.runs(undefined, signal),
358
397
  scope.members(signal),
359
398
  ]);
@@ -608,6 +647,10 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
608
647
  return archived;
609
648
  },
610
649
  archivedTasks: (page, signal) => scope().tasks({ archived: true, ...page }, signal),
650
+ findTasks: (query, page, signal) =>
651
+ scope().tasks({ archived: false, ...searchWordsOf(query), ...page }, signal),
652
+ findGoals: (query, page, signal) =>
653
+ scope().goals({ open: true, ...searchWordsOf(query), ...page }, signal),
611
654
  async raiseDecision(input) {
612
655
  const decision = await scope().raiseDecision(input);
613
656
  await refreshProject();
package/src/i18n/en.ts CHANGED
@@ -121,6 +121,11 @@ export const en = {
121
121
  next: "Next page",
122
122
  range: (from: number, to: number, of: number) => `${from}-${to} of ${of}`,
123
123
  },
124
+ picker: {
125
+ nothingFound: "Nothing matches",
126
+ searching: "Searching",
127
+ more: "Show more",
128
+ },
124
129
  },
125
130
 
126
131
  nav: {
@@ -304,7 +309,7 @@ export const en = {
304
309
  },
305
310
  partOf: "Part of",
306
311
  owner: "Owner",
307
- assignee: "Assignee",
312
+ workers: "Workers",
308
313
  written: "Written",
309
314
  changed: "Changed",
310
315
  state: "State",
@@ -338,8 +343,11 @@ export const en = {
338
343
  bodyPlaceholder: "What is to be done, in Markdown.",
339
344
  state: "State",
340
345
  priority: "Priority",
341
- assignee: "Assignee",
346
+ workers: "Workers",
347
+ addWorker: "Add a worker",
342
348
  partOf: "Part of",
349
+ searchTasks: "Search by number or title",
350
+ searchGoals: "Search by title",
343
351
  category: "Category",
344
352
  noCategory: "No category",
345
353
  goal: "Goal",
@@ -425,7 +433,7 @@ export const en = {
425
433
  person: "Person",
426
434
  open: "Open",
427
435
  inProgress: "In progress",
428
- dueSoon: "Due in 7 days",
436
+ dueThisWeek: "Due this week",
429
437
  overdue: "Overdue",
430
438
  unassigned: "Nobody's yet",
431
439
  },
@@ -497,8 +505,8 @@ export const en = {
497
505
  loading: "Loading your work",
498
506
  couldNotLoad: "Your work could not be loaded.",
499
507
  empty: {
500
- title: "Nothing is assigned to you",
501
- body: "A task assigned to you in any project you may see shows up here.",
508
+ title: "Nothing is on your plate",
509
+ body: "A task you work on, in any project you may see, shows up here.",
502
510
  },
503
511
  overdue: "Overdue",
504
512
  thisWeek: "Due this week",
@@ -609,7 +617,7 @@ export const en = {
609
617
  title: "the title",
610
618
  body: "the body",
611
619
  priority: "the priority",
612
- assigneeId: "the assignee",
620
+ workers: "the workers",
613
621
  parentId: "what it is part of",
614
622
  links: "the links",
615
623
  name: "the name",
@@ -861,8 +869,8 @@ export const en = {
861
869
  loadingWork: "Loading the work",
862
870
  couldNotLoadWork: "The work could not be loaded.",
863
871
  noWork: {
864
- title: (name: string) => `Nothing is assigned to ${name}`,
865
- body: "An open task assigned to them, in a project you may see, shows up here.",
872
+ title: (name: string) => `Nothing is on ${name}'s plate`,
873
+ body: "An open task they work on, in a project you may see, shows up here.",
866
874
  },
867
875
  notOverseen: {
868
876
  title: "Not yours to see",
package/src/i18n/ko.ts CHANGED
@@ -83,6 +83,11 @@ export const ko: Messages = {
83
83
  next: "다음",
84
84
  range: (from, to, of) => `${of}건 중 ${from}-${to}`,
85
85
  },
86
+ picker: {
87
+ nothingFound: "맞는 것이 없습니다",
88
+ searching: "검색 중",
89
+ more: "더 보기",
90
+ },
86
91
  },
87
92
 
88
93
  nav: {
@@ -264,7 +269,7 @@ export const ko: Messages = {
264
269
  },
265
270
  partOf: "상위 작업:",
266
271
  owner: "담당자",
267
- assignee: "배정",
272
+ workers: "작업자",
268
273
  written: "작성",
269
274
  changed: "변경",
270
275
  state: "상태",
@@ -298,8 +303,11 @@ export const ko: Messages = {
298
303
  bodyPlaceholder: "무엇을 할지, Markdown으로.",
299
304
  state: "상태",
300
305
  priority: "우선순위",
301
- assignee: "배정",
306
+ workers: "작업자",
307
+ addWorker: "작업자 추가",
302
308
  partOf: "상위 작업",
309
+ searchTasks: "번호나 제목으로 검색",
310
+ searchGoals: "제목으로 검색",
303
311
  category: "카테고리",
304
312
  noCategory: "카테고리 없음",
305
313
  goal: "목표",
@@ -385,9 +393,9 @@ export const ko: Messages = {
385
393
  person: "사람",
386
394
  open: "열림",
387
395
  inProgress: "진행 중",
388
- dueSoon: "7일 내 마감",
396
+ dueThisWeek: "이번 주 마감",
389
397
  overdue: "기한 지남",
390
- unassigned: "아직 배정 없음",
398
+ unassigned: "아직 작업자 없음",
391
399
  },
392
400
  },
393
401
 
@@ -456,8 +464,8 @@ export const ko: Messages = {
456
464
  loading: "내 작업을 불러오는 중",
457
465
  couldNotLoad: "내 작업을 불러올 수 없습니다.",
458
466
  empty: {
459
- title: "내게 배정된 작업이 없습니다",
460
- body: "볼 수 있는 어느 프로젝트에서든 내게 배정된 작업이 여기에 나타납니다.",
467
+ title: "내가 맡은 작업이 없습니다",
468
+ body: "볼 수 있는 어느 프로젝트에서든 내가 작업자인 작업이 여기에 나타납니다.",
461
469
  },
462
470
  overdue: "기한 지남",
463
471
  thisWeek: "이번 주 마감",
@@ -567,7 +575,7 @@ export const ko: Messages = {
567
575
  title: "제목",
568
576
  body: "본문",
569
577
  priority: "우선순위",
570
- assigneeId: "배정",
578
+ workers: "작업자",
571
579
  parentId: "상위 작업",
572
580
  links: "링크",
573
581
  name: "이름",
@@ -816,8 +824,8 @@ export const ko: Messages = {
816
824
  loadingWork: "작업을 불러오는 중",
817
825
  couldNotLoadWork: "작업을 불러올 수 없습니다.",
818
826
  noWork: {
819
- title: (name) => `${name}에게 배정된 작업이 없습니다`,
820
- body: "내가 볼 수 있는 프로젝트에서 그 사람에게 배정된 열린 작업이 여기에 나타납니다.",
827
+ title: (name) => `${name}에게 맡겨진 작업이 없습니다`,
828
+ body: "내가 볼 수 있는 프로젝트에서 그 사람이 작업자인 열린 작업이 여기에 나타납니다.",
821
829
  },
822
830
  notOverseen: {
823
831
  title: "볼 수 없는 작업입니다",
package/src/index.ts CHANGED
@@ -102,6 +102,13 @@ export {
102
102
  PeopleList,
103
103
  type PeopleListProps,
104
104
  } from "./components/people.js";
105
+ export {
106
+ PICKER_PAGE_LIMIT,
107
+ Picker,
108
+ type PickerPage,
109
+ type PickerProps,
110
+ type PickerSearch,
111
+ } from "./components/picker.js";
105
112
  export {
106
113
  clampScale,
107
114
  FITTED,
@@ -168,7 +175,14 @@ export {
168
175
  } from "./components/shell.js";
169
176
  export { SignIn, type SignInProps } from "./components/sign-in.js";
170
177
  export { SkillList, type SkillListProps } from "./components/skills.js";
171
- export { TaskForm, type TaskFormProps } from "./components/task-form.js";
178
+ export {
179
+ type GoalSearch,
180
+ goalLabel,
181
+ TaskForm,
182
+ type TaskFormProps,
183
+ type TaskSearch,
184
+ taskLabel,
185
+ } from "./components/task-form.js";
172
186
  export { TaskList, type TaskListProps } from "./components/task-list.js";
173
187
  export { TaskView, type TaskViewProps } from "./components/task-view.js";
174
188
  export {
@@ -204,6 +218,7 @@ export {
204
218
  STATE_LABELS,
205
219
  StateBadge,
206
220
  Time,
221
+ Workers,
207
222
  } from "./components/ui.js";
208
223
  export { WorkList, type WorkListProps } from "./components/work.js";
209
224
  export {
package/src/limits.ts CHANGED
@@ -9,6 +9,8 @@ export const MAX_QUESTION_LENGTH = 500;
9
9
  export const MAX_BODY_LENGTH = 20_000;
10
10
  /** The links of a task: a repository, a pull request, a document. */
11
11
  export const MAX_LINKS = 20;
12
+ /** How many people may be at work on one task at a time (ADR-0037): a crew, not a mailing list. */
13
+ export const MAX_WORKERS = 20;
12
14
  export const MAX_LINK_LABEL_LENGTH = 120;
13
15
  export const MAX_URL_LENGTH = 2048;
14
16
  /** The options of a decision, and how a person words each. */
package/src/schemas.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  MAX_TOKEN_DAYS,
40
40
  MAX_TOKEN_NAME_LENGTH,
41
41
  MAX_URL_LENGTH,
42
+ MAX_WORKERS,
42
43
  MIN_OPTIONS,
43
44
  } from "./limits.js";
44
45
  import { hasForbiddenCodePoint } from "./text.js";
@@ -480,8 +481,10 @@ export const restTaskSchema = z.object({
480
481
  body: z.string(),
481
482
  state: z.enum(TASK_STATES),
482
483
  priority: z.enum(TASK_PRIORITIES),
484
+ /** Whose task it is: one person, whoever wrote it. */
483
485
  owner: restPersonSchema,
484
- assignee: z.nullable(restPersonSchema),
486
+ /** The people at work on it (ADR-0037), in the order they came to it; `[]` while nobody is. */
487
+ workers: z.array(restPersonSchema),
485
488
  /** The task this one is part of, or `null`. */
486
489
  parentId: z.nullable(uuid),
487
490
  /** The category the task is sorted into, as the workspace keeps it, or `null`. */
@@ -530,7 +533,8 @@ export const restTaskInputSchema = z.object({
530
533
  body: z.optional(body),
531
534
  state: z.optional(z.enum(TASK_STATES)),
532
535
  priority: z.optional(z.enum(TASK_PRIORITIES)),
533
- assigneeId: z.optional(z.nullable(uuid)),
536
+ /** The people to be at work on it, each one who may work in the project, in place of those there were; `[]` for nobody. */
537
+ workerIds: z.optional(z.array(uuid).check(z.maxLength(MAX_WORKERS))),
534
538
  parentId: z.optional(z.nullable(uuid)),
535
539
  /** The key of a category of the workspace, or `null` for none. */
536
540
  category: z.optional(z.nullable(categoryKey)),
@@ -549,7 +553,7 @@ export const restTaskPatchSchema = z.object({
549
553
  body: z.optional(body),
550
554
  state: z.optional(z.enum(TASK_STATES)),
551
555
  priority: z.optional(z.enum(TASK_PRIORITIES)),
552
- assigneeId: z.optional(z.nullable(uuid)),
556
+ workerIds: z.optional(z.array(uuid).check(z.maxLength(MAX_WORKERS))),
553
557
  parentId: z.optional(z.nullable(uuid)),
554
558
  category: z.optional(z.nullable(categoryKey)),
555
559
  goalId: z.optional(z.nullable(uuid)),
@@ -596,8 +600,8 @@ export const restGoalSchema = z.object({
596
600
  });
597
601
  export type RestGoal = z.infer<typeof restGoalSchema>;
598
602
 
599
- /** `GET /api/v1/projects/<key>/goals`: the roadmap, the active goals first. */
600
- export const restGoalsSchema = z.object({ items: z.array(restGoalSchema) });
603
+ /** `GET /api/v1/projects/<key>/goals`: a page of the roadmap, the active goals first, and whether there is more after it. */
604
+ export const restGoalsSchema = z.object({ items: z.array(restGoalSchema), more: z.boolean() });
601
605
  export type RestGoals = z.infer<typeof restGoalsSchema>;
602
606
 
603
607
  /** What `POST /api/v1/projects/<key>/goals` is sent: the title, and the rest with defaults. */
@@ -686,8 +690,8 @@ const projectRef = z.object({
686
690
 
687
691
  /**
688
692
  * `GET /api/v1/work`: what is on the asker's plate across the projects they may see (ADR-0019):
689
- * the open tasks assigned to them, and the decisions that wait for a person, each with its
690
- * project; and, for the week on the plate, `closed`, the tasks assigned to them that are done
693
+ * the open tasks they work on, and the decisions that wait for a person, each with its
694
+ * project; and, for the week on the plate, `closed`, the tasks they work on that are done
691
695
  * or dropped and not yet put away. Left out by a console older than it.
692
696
  */
693
697
  export const restWorkSchema = z.object({
@@ -1238,6 +1238,150 @@ table {
1238
1238
  justify-self: start;
1239
1239
  }
1240
1240
 
1241
+ /* The workers of a task on its form: each a chip with the way to take them off, and the way to add one. */
1242
+ .sc-chips {
1243
+ display: flex;
1244
+ flex-wrap: wrap;
1245
+ align-items: center;
1246
+ gap: var(--sc-space-2);
1247
+ }
1248
+
1249
+ .sc-chip {
1250
+ display: inline-flex;
1251
+ align-items: center;
1252
+ gap: var(--sc-space-1);
1253
+ min-height: var(--sc-control-size);
1254
+ padding: 0 var(--sc-space-1) 0 var(--sc-space-2);
1255
+ border: var(--sc-border);
1256
+ border-radius: var(--sc-radius-md);
1257
+ background: var(--sc-color-bg-inset);
1258
+ font-size: var(--sc-text-sm);
1259
+ }
1260
+
1261
+ .sc-chip-remove,
1262
+ .sc-picker-clear {
1263
+ display: inline-grid;
1264
+ flex: none;
1265
+ place-items: center;
1266
+ width: 1.5rem;
1267
+ height: 1.5rem;
1268
+ border: 0;
1269
+ border-radius: var(--sc-radius-sm);
1270
+ background: transparent;
1271
+ color: var(--sc-color-text-muted);
1272
+ font-size: var(--sc-text-md);
1273
+ line-height: 1;
1274
+ cursor: pointer;
1275
+ }
1276
+
1277
+ .sc-chip-remove:hover:not(:disabled),
1278
+ .sc-picker-clear:hover:not(:disabled) {
1279
+ background: var(--sc-color-bg-raised);
1280
+ color: var(--sc-color-text);
1281
+ }
1282
+
1283
+ .sc-chip-remove:focus-visible,
1284
+ .sc-picker-clear:focus-visible {
1285
+ outline: var(--sc-focus-ring);
1286
+ }
1287
+
1288
+ .sc-chips .sc-chip-add {
1289
+ width: auto;
1290
+ min-width: 10rem;
1291
+ }
1292
+
1293
+ /* A picker: the field that searches a listing, its page of results under it, and the choice in its place. */
1294
+ .sc-picker {
1295
+ position: relative;
1296
+ min-width: 0;
1297
+ }
1298
+
1299
+ .sc-picker-chosen {
1300
+ display: flex;
1301
+ align-items: center;
1302
+ gap: var(--sc-space-2);
1303
+ min-height: var(--sc-control-size);
1304
+ padding: var(--sc-space-1) var(--sc-space-1) var(--sc-space-1) var(--sc-space-3);
1305
+ border: 1px solid var(--sc-color-border-strong);
1306
+ border-radius: var(--sc-radius-md);
1307
+ background: var(--sc-color-field);
1308
+ font-size: var(--sc-text-sm);
1309
+ }
1310
+
1311
+ .sc-picker-value {
1312
+ flex: 1;
1313
+ min-width: 0;
1314
+ overflow: hidden;
1315
+ text-overflow: ellipsis;
1316
+ white-space: nowrap;
1317
+ }
1318
+
1319
+ .sc-picker-list {
1320
+ position: absolute;
1321
+ z-index: 40;
1322
+ top: calc(100% + var(--sc-space-1));
1323
+ right: 0;
1324
+ left: 0;
1325
+ max-height: 18rem;
1326
+ margin: 0;
1327
+ padding: var(--sc-space-1);
1328
+ overflow-y: auto;
1329
+ list-style: none;
1330
+ border: var(--sc-border);
1331
+ border-radius: var(--sc-radius-md);
1332
+ background: var(--sc-color-bg-inset);
1333
+ box-shadow: var(--sc-shadow-stage);
1334
+ font-size: var(--sc-text-sm);
1335
+ }
1336
+
1337
+ /* Where the window has no room below the field, and more above, the list stands above it. */
1338
+ .sc-picker-list-above {
1339
+ top: auto;
1340
+ bottom: calc(100% + var(--sc-space-1));
1341
+ }
1342
+
1343
+ .sc-picker-option {
1344
+ padding: var(--sc-space-2);
1345
+ overflow: hidden;
1346
+ border-radius: var(--sc-radius-sm);
1347
+ text-overflow: ellipsis;
1348
+ white-space: nowrap;
1349
+ cursor: pointer;
1350
+ }
1351
+
1352
+ .sc-picker-option-active {
1353
+ background: var(--sc-color-bg-raised);
1354
+ }
1355
+
1356
+ .sc-picker-note {
1357
+ padding: var(--sc-space-2);
1358
+ }
1359
+
1360
+ /* The workers of a task on a card or a row: pictures side by side, and how many more. */
1361
+ .sc-workers {
1362
+ display: inline-flex;
1363
+ align-items: center;
1364
+ }
1365
+
1366
+ .sc-workers > * + * {
1367
+ margin-left: -0.375rem;
1368
+ }
1369
+
1370
+ .sc-workers-more {
1371
+ margin-inline: var(--sc-space-1);
1372
+ color: var(--sc-color-text-muted);
1373
+ font-size: var(--sc-text-xs);
1374
+ font-weight: var(--sc-weight-medium);
1375
+ }
1376
+
1377
+ .sc-worker-list {
1378
+ display: grid;
1379
+ gap: var(--sc-space-1);
1380
+ margin: 0;
1381
+ padding: 0;
1382
+ list-style: none;
1383
+ }
1384
+
1241
1385
  .sc-form-actions {
1242
1386
  display: flex;
1243
1387
  flex-wrap: wrap;
@@ -2906,8 +3050,10 @@ table {
2906
3050
  white-space: nowrap;
2907
3051
  }
2908
3052
 
2909
- /* Lit a little under the pointer by its colour, not by a filter, which would light the card within it too. */
2910
- .sc-calendar-bar:hover {
3053
+ /* The task under the pointer is lit a little by its colour, in every piece of its bar and in
3054
+ every week it crosses, never one piece alone; by its colour and not by a filter, which would
3055
+ light the card within it too. */
3056
+ .sc-calendar-bar-pointed {
2911
3057
  background: color-mix(in srgb, var(--sc-category-fill), white 12%);
2912
3058
  }
2913
3059
 
@@ -2925,6 +3071,18 @@ table {
2925
3071
  border-end-end-radius: var(--sc-radius-full);
2926
3072
  }
2927
3073
 
3074
+ /* The title of a bar with more days in the week runs along the bar as far as its days reach,
3075
+ less the room of its end, instead of stopping at the first day's edge: above every piece,
3076
+ the one under the pointer included, and under the cards; the pointer goes through it to the
3077
+ piece of the day beneath. The days are the first piece's own custom property. */
3078
+ .sc-calendar-task-title-spanning {
3079
+ position: relative;
3080
+ z-index: 25;
3081
+ flex: none;
3082
+ max-width: calc(var(--sc-span, 1) * 100% - 2.25rem);
3083
+ pointer-events: none;
3084
+ }
3085
+
2928
3086
  .sc-calendar-bar.sc-calendar-late {
2929
3087
  border-color: color-mix(in srgb, var(--sc-color-danger) 70%, transparent);
2930
3088
  color: var(--sc-color-danger);
@@ -3365,18 +3523,12 @@ table {
3365
3523
  position: relative;
3366
3524
  }
3367
3525
 
3368
- /* The item under the pointer rises above its neighbours, which would otherwise paint over its card. */
3369
- .sc-calendar-task:hover,
3370
- .sc-calendar-task:focus-visible,
3371
- .sc-calendar-bar:hover,
3372
- .sc-calendar-bar:focus-visible {
3373
- z-index: 20;
3374
- }
3375
-
3526
+ /* The card rises above its neighbours and above a title running along a bar; the item under the
3527
+ pointer itself stays in its layer, so that nothing of it covers such a title. */
3376
3528
  .sc-task-tip {
3377
3529
  position: absolute;
3378
3530
  left: 0;
3379
- z-index: 20;
3531
+ z-index: 30;
3380
3532
  width: max-content;
3381
3533
  max-width: 20rem;
3382
3534
  padding: var(--sc-space-2) var(--sc-space-3);
package/src/vocabulary.ts CHANGED
@@ -30,6 +30,9 @@ export const GOAL_STATES = ["planned", "active", "done", "dropped"] as const;
30
30
 
31
31
  export type GoalState = (typeof GOAL_STATES)[number];
32
32
 
33
+ /** The states a goal is still open in: planned, or being worked towards. */
34
+ export const OPEN_GOAL_STATES: readonly GoalState[] = ["planned", "active"];
35
+
33
36
  /**
34
37
  * The colours a category, or a project's mark, may be shown in: names, so that a custom console
35
38
  * paints them in its own tokens and no value a person typed reaches a stylesheet.