@skillcdn/console 0.1.9 → 0.1.11

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 (162) hide show
  1. package/README.md +6 -6
  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 +303 -18
  8. package/dist/cli/cli.js.map +1 -1
  9. package/dist/cli/format.d.ts +11 -1
  10. package/dist/cli/format.d.ts.map +1 -1
  11. package/dist/cli/format.js +87 -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 +70 -14
  15. package/dist/cli/help.js.map +1 -1
  16. package/dist/client.d.ts +44 -8
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +32 -4
  19. package/dist/client.js.map +1 -1
  20. package/dist/components/board.d.ts +24 -1
  21. package/dist/components/board.d.ts.map +1 -1
  22. package/dist/components/board.js +66 -5
  23. package/dist/components/board.js.map +1 -1
  24. package/dist/components/calendar.d.ts +54 -0
  25. package/dist/components/calendar.d.ts.map +1 -0
  26. package/dist/components/calendar.js +156 -0
  27. package/dist/components/calendar.js.map +1 -0
  28. package/dist/components/categories.d.ts +13 -0
  29. package/dist/components/categories.d.ts.map +1 -0
  30. package/dist/components/categories.js +52 -0
  31. package/dist/components/categories.js.map +1 -0
  32. package/dist/components/decision-list.d.ts +10 -0
  33. package/dist/components/decision-list.d.ts.map +1 -1
  34. package/dist/components/decision-list.js +16 -1
  35. package/dist/components/decision-list.js.map +1 -1
  36. package/dist/components/event-feed.d.ts.map +1 -1
  37. package/dist/components/event-feed.js +23 -7
  38. package/dist/components/event-feed.js.map +1 -1
  39. package/dist/components/goals.d.ts +35 -0
  40. package/dist/components/goals.d.ts.map +1 -0
  41. package/dist/components/goals.js +45 -0
  42. package/dist/components/goals.js.map +1 -0
  43. package/dist/components/paged.d.ts +14 -0
  44. package/dist/components/paged.d.ts.map +1 -0
  45. package/dist/components/paged.js +28 -0
  46. package/dist/components/paged.js.map +1 -0
  47. package/dist/components/projects.d.ts +22 -2
  48. package/dist/components/projects.d.ts.map +1 -1
  49. package/dist/components/projects.js +35 -7
  50. package/dist/components/projects.js.map +1 -1
  51. package/dist/components/runs.d.ts.map +1 -1
  52. package/dist/components/runs.js +2 -2
  53. package/dist/components/runs.js.map +1 -1
  54. package/dist/components/shell.d.ts +2 -1
  55. package/dist/components/shell.d.ts.map +1 -1
  56. package/dist/components/shell.js +1 -1
  57. package/dist/components/shell.js.map +1 -1
  58. package/dist/components/task-form.d.ts +5 -1
  59. package/dist/components/task-form.d.ts.map +1 -1
  60. package/dist/components/task-form.js +13 -1
  61. package/dist/components/task-form.js.map +1 -1
  62. package/dist/components/task-list.d.ts +16 -0
  63. package/dist/components/task-list.d.ts.map +1 -0
  64. package/dist/components/task-list.js +16 -0
  65. package/dist/components/task-list.js.map +1 -0
  66. package/dist/components/task-view.d.ts +9 -1
  67. package/dist/components/task-view.d.ts.map +1 -1
  68. package/dist/components/task-view.js +10 -4
  69. package/dist/components/task-view.js.map +1 -1
  70. package/dist/components/ui.d.ts +30 -1
  71. package/dist/components/ui.d.ts.map +1 -1
  72. package/dist/components/ui.js +60 -0
  73. package/dist/components/ui.js.map +1 -1
  74. package/dist/components/work.d.ts +10 -0
  75. package/dist/components/work.d.ts.map +1 -0
  76. package/dist/components/work.js +33 -0
  77. package/dist/components/work.js.map +1 -0
  78. package/dist/console.css +658 -0
  79. package/dist/console.d.ts +8 -0
  80. package/dist/console.d.ts.map +1 -1
  81. package/dist/console.js +145 -13
  82. package/dist/console.js.map +1 -1
  83. package/dist/data.d.ts +19 -3
  84. package/dist/data.d.ts.map +1 -1
  85. package/dist/data.js +51 -4
  86. package/dist/data.js.map +1 -1
  87. package/dist/dates.d.ts +39 -0
  88. package/dist/dates.d.ts.map +1 -0
  89. package/dist/dates.js +120 -0
  90. package/dist/dates.js.map +1 -0
  91. package/dist/i18n/en.d.ts +172 -5
  92. package/dist/i18n/en.d.ts.map +1 -1
  93. package/dist/i18n/en.js +183 -6
  94. package/dist/i18n/en.js.map +1 -1
  95. package/dist/i18n/ko.d.ts.map +1 -1
  96. package/dist/i18n/ko.js +178 -6
  97. package/dist/i18n/ko.js.map +1 -1
  98. package/dist/index.d.ts +12 -5
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +12 -5
  101. package/dist/index.js.map +1 -1
  102. package/dist/limits.d.ts +11 -0
  103. package/dist/limits.d.ts.map +1 -1
  104. package/dist/limits.js +11 -0
  105. package/dist/limits.js.map +1 -1
  106. package/dist/marks.d.ts +12 -0
  107. package/dist/marks.d.ts.map +1 -0
  108. package/dist/marks.js +31 -0
  109. package/dist/marks.js.map +1 -0
  110. package/dist/router.d.ts +51 -1
  111. package/dist/router.d.ts.map +1 -1
  112. package/dist/router.js +80 -1
  113. package/dist/router.js.map +1 -1
  114. package/dist/routes.d.ts +14 -1
  115. package/dist/routes.d.ts.map +1 -1
  116. package/dist/routes.js +17 -0
  117. package/dist/routes.js.map +1 -1
  118. package/dist/schemas.d.ts +489 -3
  119. package/dist/schemas.d.ts.map +1 -1
  120. package/dist/schemas.js +150 -9
  121. package/dist/schemas.js.map +1 -1
  122. package/dist/vocabulary.d.ts +21 -1
  123. package/dist/vocabulary.d.ts.map +1 -1
  124. package/dist/vocabulary.js +31 -0
  125. package/dist/vocabulary.js.map +1 -1
  126. package/docs/specs/cli.md +17 -8
  127. package/docs/specs/rest.md +48 -17
  128. package/package.json +1 -1
  129. package/skills/working-the-board/SKILL.md +7 -6
  130. package/src/api.ts +42 -0
  131. package/src/cli/cli.ts +362 -18
  132. package/src/cli/format.ts +96 -6
  133. package/src/cli/help.ts +77 -15
  134. package/src/client.ts +98 -7
  135. package/src/components/board.tsx +173 -13
  136. package/src/components/calendar.tsx +367 -0
  137. package/src/components/categories.tsx +209 -0
  138. package/src/components/decision-list.tsx +67 -5
  139. package/src/components/event-feed.tsx +46 -27
  140. package/src/components/goals.tsx +264 -0
  141. package/src/components/paged.tsx +39 -0
  142. package/src/components/projects.tsx +128 -10
  143. package/src/components/runs.tsx +7 -1
  144. package/src/components/shell.tsx +5 -2
  145. package/src/components/task-form.tsx +78 -1
  146. package/src/components/task-list.tsx +64 -0
  147. package/src/components/task-view.tsx +75 -4
  148. package/src/components/ui.tsx +116 -1
  149. package/src/components/work.tsx +93 -0
  150. package/src/console.tsx +376 -9
  151. package/src/data.ts +83 -12
  152. package/src/dates.ts +143 -0
  153. package/src/i18n/en.ts +193 -6
  154. package/src/i18n/ko.ts +184 -6
  155. package/src/index.ts +62 -1
  156. package/src/limits.ts +11 -0
  157. package/src/marks.ts +40 -0
  158. package/src/router.ts +131 -6
  159. package/src/routes.ts +19 -0
  160. package/src/schemas.ts +187 -7
  161. package/src/styles/console.css +658 -0
  162. package/src/vocabulary.ts +43 -0
package/src/data.ts CHANGED
@@ -6,6 +6,9 @@ import {
6
6
  EVENT_KINDS,
7
7
  type FileUpload,
8
8
  type RestAnswerInput,
9
+ type RestCategory,
10
+ type RestCategoryInput,
11
+ type RestCategoryPatch,
9
12
  type RestConnectRequest,
10
13
  type RestDecision,
11
14
  type RestDecisionInput,
@@ -14,6 +17,9 @@ import {
14
17
  type RestDocumentInput,
15
18
  type RestDocuments,
16
19
  type RestEvent,
20
+ type RestGoal,
21
+ type RestGoalInput,
22
+ type RestGoalPatch,
17
23
  type RestMe,
18
24
  type RestMember,
19
25
  type RestMemberInput,
@@ -35,6 +41,7 @@ import {
35
41
  type RestTokens,
36
42
  type RestVersion,
37
43
  type RestVersions,
44
+ type RestWork,
38
45
  restEventSchema,
39
46
  } from "./api.js";
40
47
 
@@ -51,10 +58,13 @@ const FEED_MAX_PAGES = 20;
51
58
  const RELOAD_DELAY_MS = 150;
52
59
 
53
60
  export interface ConsoleActions {
54
- /** Makes a project, whose owner the person becomes. */
55
- createProject(input: RestProjectInput): Promise<RestProject>;
61
+ /** Makes a project, whose owner the person becomes, with an image for its mark when one is given. */
62
+ createProject(input: RestProjectInput, icon?: FileUpload): Promise<RestProject>;
56
63
  /** Changes the settings of the project the page is on; for an owner. */
57
64
  updateProject(patch: RestProjectPatch): Promise<RestProject>;
65
+ /** Sets the image for the mark of the project the page is on, or takes it away; for an owner. */
66
+ setProjectIcon(input: FileUpload): Promise<RestProject>;
67
+ removeProjectIcon(): Promise<RestProject>;
58
68
  addMember(input: RestMemberInput): Promise<RestMember>;
59
69
  updateMember(personId: string, patch: RestMemberPatch): Promise<RestMember>;
60
70
  removeMember(personId: string): Promise<void>;
@@ -64,6 +74,15 @@ export interface ConsoleActions {
64
74
  answerDecision(id: string, input: RestAnswerInput): Promise<RestDecision>;
65
75
  /** Grows a decision's record: its context, or what followed. */
66
76
  updateDecision(id: string, patch: RestDecisionPatch): Promise<RestDecision>;
77
+ /** Sets a goal on the roadmap of the project the page is on; for an owner. */
78
+ createGoal(input: RestGoalInput): Promise<RestGoal>;
79
+ updateGoal(id: string, patch: RestGoalPatch): Promise<RestGoal>;
80
+ /** What is on the person's plate across their projects, read when the page asks. */
81
+ work(signal?: AbortSignal): Promise<RestWork>;
82
+ /** The categories tasks are sorted into: kept by an administrator. */
83
+ createCategory(input: RestCategoryInput): Promise<RestCategory>;
84
+ updateCategory(key: string, patch: RestCategoryPatch): Promise<RestCategory>;
85
+ removeCategory(key: string): Promise<void>;
67
86
  /** A folder's pages and folders, or the pages a search finds, in the project the page is on. */
68
87
  documents(filter?: DocumentFilter, signal?: AbortSignal): Promise<RestDocuments>;
69
88
  document(path: string, signal?: AbortSignal): Promise<RestDocument>;
@@ -104,6 +123,8 @@ export interface ConsoleData {
104
123
  /** The projects the person may see, by name. */
105
124
  readonly projects: readonly RestProject[];
106
125
  readonly people: readonly RestPerson[];
126
+ /** The categories tasks are sorted into, by name. */
127
+ readonly categories: readonly RestCategory[];
107
128
  /** The tokens of whoever is signed in, newest first. */
108
129
  readonly tokens: readonly RestToken[];
109
130
  /** The project the page is on, once loaded; `undefined` while loading, or when the page is on none. */
@@ -116,6 +137,8 @@ export interface ConsoleData {
116
137
  readonly live: boolean;
117
138
  readonly tasks: readonly RestTask[];
118
139
  readonly decisions: readonly RestDecision[];
140
+ /** The roadmap of the project: its goals, the active first. */
141
+ readonly goals: readonly RestGoal[];
119
142
  /** The runs of the project, newest first. */
120
143
  readonly runs: readonly RestRun[];
121
144
  /** The project's events, oldest first. */
@@ -161,6 +184,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
161
184
  const [loaded, setLoaded] = useState(false);
162
185
  const [projects, setProjects] = useState<readonly RestProject[]>([]);
163
186
  const [people, setPeople] = useState<readonly RestPerson[]>([]);
187
+ const [categories, setCategories] = useState<readonly RestCategory[]>([]);
164
188
  const [tokens, setTokens] = useState<readonly RestToken[]>([]);
165
189
  const [project, setProject] = useState<RestProject | undefined>(undefined);
166
190
  const [projectLoaded, setProjectLoaded] = useState(false);
@@ -168,6 +192,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
168
192
  const [live, setLive] = useState(false);
169
193
  const [tasks, setTasks] = useState<readonly RestTask[]>([]);
170
194
  const [decisions, setDecisions] = useState<readonly RestDecision[]>([]);
195
+ const [goals, setGoals] = useState<readonly RestGoal[]>([]);
171
196
  const [runs, setRuns] = useState<readonly RestRun[]>([]);
172
197
  const [events, setEvents] = useState<readonly RestEvent[]>([]);
173
198
  const [members, setMembers] = useState<readonly RestMember[]>([]);
@@ -181,15 +206,17 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
181
206
  /** Loads the workspace's lists again. What fails stays as it was. */
182
207
  const refreshWorkspace = useCallback(
183
208
  async (signal?: AbortSignal) => {
184
- const [nextProjects, nextPeople] = await Promise.all([
209
+ const [nextProjects, nextPeople, nextCategories] = await Promise.all([
185
210
  client.projects(signal),
186
211
  client.people(signal),
212
+ client.categories(signal),
187
213
  ]);
188
214
  if (signal?.aborted === true) {
189
215
  return;
190
216
  }
191
217
  setProjects(nextProjects.items);
192
218
  setPeople(nextPeople.items);
219
+ setCategories(nextCategories.items);
193
220
  },
194
221
  [client],
195
222
  );
@@ -201,19 +228,22 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
201
228
  return;
202
229
  }
203
230
  const scope = client.project(projectKey);
204
- const [nextProject, nextTasks, nextDecisions, nextRuns, nextMembers] = await Promise.all([
205
- scope.get(signal),
206
- scope.tasks(undefined, signal),
207
- scope.decisions(undefined, signal),
208
- scope.runs(undefined, signal),
209
- scope.members(signal),
210
- ]);
231
+ const [nextProject, nextTasks, nextDecisions, nextGoals, nextRuns, nextMembers] =
232
+ await Promise.all([
233
+ scope.get(signal),
234
+ scope.tasks(undefined, signal),
235
+ scope.decisions(undefined, signal),
236
+ scope.goals(signal),
237
+ scope.runs(undefined, signal),
238
+ scope.members(signal),
239
+ ]);
211
240
  if (signal?.aborted === true) {
212
241
  return;
213
242
  }
214
243
  setProject(nextProject);
215
244
  setTasks(nextTasks.items);
216
245
  setDecisions(nextDecisions.items);
246
+ setGoals(nextGoals.items);
217
247
  setRuns(nextRuns.items);
218
248
  setMembers(nextMembers.items);
219
249
  },
@@ -274,6 +304,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
274
304
  setProjectError(undefined);
275
305
  setTasks([]);
276
306
  setDecisions([]);
307
+ setGoals([]);
277
308
  setRuns([]);
278
309
  setEvents([]);
279
310
  setMembers([]);
@@ -359,10 +390,11 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
359
390
  return client.project(projectKey);
360
391
  };
361
392
  return {
362
- async createProject(input) {
393
+ async createProject(input, icon) {
363
394
  const made = await client.createProject(input);
395
+ const marked = icon === undefined ? made : await client.project(made.key).setIcon(icon);
364
396
  await refreshWorkspace();
365
- return made;
397
+ return marked;
366
398
  },
367
399
  async updateProject(patch) {
368
400
  const changed = await scope().update(patch);
@@ -370,6 +402,18 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
370
402
  await refreshWorkspace();
371
403
  return changed;
372
404
  },
405
+ async setProjectIcon(input) {
406
+ const changed = await scope().setIcon(input);
407
+ setProject(changed);
408
+ await refreshWorkspace();
409
+ return changed;
410
+ },
411
+ async removeProjectIcon() {
412
+ const changed = await scope().removeIcon();
413
+ setProject(changed);
414
+ await refreshWorkspace();
415
+ return changed;
416
+ },
373
417
  async addMember(input) {
374
418
  const member = await scope().addMember(input);
375
419
  await refreshProject();
@@ -411,6 +455,31 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
411
455
  );
412
456
  return decision;
413
457
  },
458
+ async createGoal(input) {
459
+ const goal = await scope().createGoal(input);
460
+ await refreshProject();
461
+ return goal;
462
+ },
463
+ async updateGoal(id, patch) {
464
+ const goal = await scope().updateGoal(id, patch);
465
+ setGoals((current) => current.map((candidate) => (candidate.id === id ? goal : candidate)));
466
+ return goal;
467
+ },
468
+ work: (signal) => client.work(signal),
469
+ async createCategory(input) {
470
+ const category = await client.createCategory(input);
471
+ setCategories((await client.categories()).items);
472
+ return category;
473
+ },
474
+ async updateCategory(key, patch) {
475
+ const category = await client.updateCategory(key, patch);
476
+ setCategories((await client.categories()).items);
477
+ return category;
478
+ },
479
+ async removeCategory(key) {
480
+ await client.removeCategory(key);
481
+ setCategories((current) => current.filter((category) => category.key !== key));
482
+ },
414
483
  documents: (filter, signal) => scope().documents(filter, signal),
415
484
  document: (path, signal) => scope().document(path, signal),
416
485
  writeDocument: (path, input) => scope().writeDocument(path, input),
@@ -476,6 +545,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
476
545
  loaded,
477
546
  projects,
478
547
  people,
548
+ categories,
479
549
  tokens,
480
550
  project,
481
551
  projectLoaded,
@@ -483,6 +553,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
483
553
  live,
484
554
  tasks,
485
555
  decisions,
556
+ goals,
486
557
  runs,
487
558
  events,
488
559
  members,
package/src/dates.ts ADDED
@@ -0,0 +1,143 @@
1
+ // Days, as the plan is made in (ADR-0019): `2026-10-20`, read and written as the strings they
2
+ // are, so that a deadline is the same day wherever the people are. The browser's own clock says
3
+ // which day is today; nothing here looks at it unless asked.
4
+
5
+ const DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
6
+
7
+ /** Whether a string is a day as the API spells one. */
8
+ export function isDay(value: string | null | undefined): value is string {
9
+ return value !== null && value !== undefined && DAY.test(value);
10
+ }
11
+
12
+ /** A day from a local date: the date the browser's clock shows, not the UTC one. */
13
+ export function dayOf(date: Date): string {
14
+ const year = String(date.getFullYear()).padStart(4, "0");
15
+ const month = String(date.getMonth() + 1).padStart(2, "0");
16
+ const day = String(date.getDate()).padStart(2, "0");
17
+ return `${year}-${month}-${day}`;
18
+ }
19
+
20
+ /** Today, by the browser's clock. */
21
+ export function today(now: Date = new Date()): string {
22
+ return dayOf(now);
23
+ }
24
+
25
+ /** A local date at noon of the day, so that a daylight-saving change never moves it a day. */
26
+ export function dateOf(day: string): Date {
27
+ const match = DAY.exec(day);
28
+ if (match === null) {
29
+ return new Date(Number.NaN);
30
+ }
31
+ return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12);
32
+ }
33
+
34
+ /** The day `count` days after `day`; negative counts go back. */
35
+ export function addDays(day: string, count: number): string {
36
+ const date = dateOf(day);
37
+ date.setDate(date.getDate() + count);
38
+ return dayOf(date);
39
+ }
40
+
41
+ /** How many days `to` is after `from`; negative when it is before. */
42
+ export function daysBetween(from: string, to: string): number {
43
+ const millis = dateOf(to).getTime() - dateOf(from).getTime();
44
+ return Math.round(millis / 86_400_000);
45
+ }
46
+
47
+ /** The month a day is in: `2026-10`. */
48
+ export function monthOf(day: string): string {
49
+ return day.slice(0, 7);
50
+ }
51
+
52
+ /** Whether a string is a month as the calendar's address spells one. */
53
+ export function isMonth(value: string | null | undefined): value is string {
54
+ return value !== null && value !== undefined && /^\d{4}-(0[1-9]|1[0-2])$/.test(value);
55
+ }
56
+
57
+ /** The month `count` months after `month`; negative counts go back. */
58
+ export function addMonths(month: string, count: number): string {
59
+ const year = Number(month.slice(0, 4));
60
+ const index = Number(month.slice(5, 7)) - 1 + count;
61
+ const date = new Date(year, index, 1, 12);
62
+ return `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}`;
63
+ }
64
+
65
+ /** The weekday a week begins on where the language is spoken: 1 for Monday, 7 for Sunday. */
66
+ export function firstDayOfWeek(language: string): number {
67
+ try {
68
+ const locale = new Intl.Locale(language) as Intl.Locale & {
69
+ readonly weekInfo?: { readonly firstDay: number };
70
+ readonly getWeekInfo?: () => { readonly firstDay: number };
71
+ };
72
+ const info = locale.getWeekInfo?.() ?? locale.weekInfo;
73
+ if (info !== undefined && info.firstDay >= 1 && info.firstDay <= 7) {
74
+ return info.firstDay;
75
+ }
76
+ } catch {
77
+ // A tag the browser does not know: the week begins on Monday.
78
+ }
79
+ return 1;
80
+ }
81
+
82
+ /** One cell of a month's grid: the day, and whether it is of the month shown or a neighbour's. */
83
+ export interface GridDay {
84
+ readonly day: string;
85
+ readonly inMonth: boolean;
86
+ }
87
+
88
+ /**
89
+ * The weeks of a month as a calendar shows them: full weeks, beginning on `firstDay` (1 for
90
+ * Monday, 7 for Sunday), with the neighbouring months' days filling the first and the last.
91
+ */
92
+ export function monthGrid(month: string, firstDay = 1): GridDay[][] {
93
+ const first = `${month}-01`;
94
+ const start = dateOf(first);
95
+ // getDay: 0 for Sunday; the grid counts Monday as 1 and Sunday as 7.
96
+ const weekday = start.getDay() === 0 ? 7 : start.getDay();
97
+ const back = (weekday - firstDay + 7) % 7;
98
+ let cursor = addDays(first, -back);
99
+ const weeks: GridDay[][] = [];
100
+ do {
101
+ const week: GridDay[] = [];
102
+ for (let column = 0; column < 7; column += 1) {
103
+ week.push({ day: cursor, inMonth: monthOf(cursor) === month });
104
+ cursor = addDays(cursor, 1);
105
+ }
106
+ weeks.push(week);
107
+ } while (monthOf(cursor) === month);
108
+ return weeks;
109
+ }
110
+
111
+ /** The weekdays in the order a week is shown, from `firstDay`, as the grid counts them. */
112
+ export function weekdays(firstDay = 1): number[] {
113
+ return Array.from({ length: 7 }, (_, index) => ((firstDay - 1 + index) % 7) + 1);
114
+ }
115
+
116
+ /** A day as people read one in a language: the month and the day, with the year when it is not this year's. */
117
+ export function formatDay(day: string, language?: string, now: Date = new Date()): string {
118
+ const date = dateOf(day);
119
+ if (Number.isNaN(date.getTime())) {
120
+ return day;
121
+ }
122
+ return date.toLocaleDateString(language, {
123
+ month: "short",
124
+ day: "numeric",
125
+ ...(date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
126
+ });
127
+ }
128
+
129
+ /** A month as people read one: the month and the year. */
130
+ export function formatMonth(month: string, language?: string): string {
131
+ const date = dateOf(`${month}-01`);
132
+ if (Number.isNaN(date.getTime())) {
133
+ return month;
134
+ }
135
+ return date.toLocaleDateString(language, { month: "long", year: "numeric" });
136
+ }
137
+
138
+ /** A weekday as people read one, short, as the grid counts them: 1 for Monday, 7 for Sunday. */
139
+ export function formatWeekday(weekday: number, language?: string): string {
140
+ // The week of 2024-01-01 began on a Monday.
141
+ const date = new Date(2024, 0, weekday, 12);
142
+ return date.toLocaleDateString(language, { weekday: "short" });
143
+ }
package/src/i18n/en.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { SignInFailure } from "../routes.js";
2
2
  import type {
3
+ CategoryColor,
4
+ GoalState,
3
5
  PersonRole,
4
6
  ProjectRole,
5
7
  ProjectVisibility,
@@ -41,12 +43,28 @@ export const en = {
41
43
  private: "Only those listed",
42
44
  } satisfies Record<ProjectVisibility, string>,
43
45
  runStatus: {
44
- running: "At work",
46
+ running: "Open",
45
47
  waiting: "Waiting for a decision",
46
48
  finished: "Finished",
47
49
  failed: "Failed",
48
50
  abandoned: "Abandoned",
49
51
  } satisfies Record<RunStatus, string>,
52
+ goalState: {
53
+ planned: "Planned",
54
+ active: "Active",
55
+ done: "Reached",
56
+ dropped: "Dropped",
57
+ } satisfies Record<GoalState, string>,
58
+ categoryColor: {
59
+ gray: "Gray",
60
+ blue: "Blue",
61
+ green: "Green",
62
+ orange: "Orange",
63
+ pink: "Pink",
64
+ purple: "Purple",
65
+ teal: "Teal",
66
+ yellow: "Yellow",
67
+ } satisfies Record<CategoryColor, string>,
50
68
  },
51
69
 
52
70
  common: {
@@ -77,13 +95,18 @@ export const en = {
77
95
  nobody: "Nobody",
78
96
  nothing: "Nothing",
79
97
  language: "Language",
98
+ showMore: (n: number, of: number) => `Show ${n} more of ${of}`,
80
99
  },
81
100
 
82
101
  nav: {
83
102
  projects: "Projects",
103
+ work: "My work",
84
104
  people: "People",
85
105
  agents: "Agents",
106
+ categories: "Categories",
86
107
  board: "Board",
108
+ roadmap: "Roadmap",
109
+ calendar: "Calendar",
87
110
  docs: "Docs",
88
111
  decisions: "Decisions",
89
112
  feed: "Feed",
@@ -128,7 +151,6 @@ export const en = {
128
151
  list: "Projects",
129
152
  private: "Private",
130
153
  decisionsWaiting: (n: number) => `${count(n, "decision", "decisions")} waiting`,
131
- agentsAtWork: (n: number) => `${count(n, "agent", "agents")} at work`,
132
154
  form: {
133
155
  aria: "New project",
134
156
  name: "Name",
@@ -140,6 +162,11 @@ export const en = {
140
162
  whoIsMember: "Who is a member",
141
163
  skillsAddress: "Skills address",
142
164
  skillsAddressPlaceholder: "/gh/<owner>/<repo>, or empty for the organization's",
165
+ symbol: "Mark",
166
+ image: "Image",
167
+ imageRemove: "Remove the image",
168
+ color: "Colour",
169
+ colorAuto: "Chosen from the key",
143
170
  make: "Make the project",
144
171
  },
145
172
  members: {
@@ -185,9 +212,24 @@ export const en = {
185
212
  moveTo: "Move to",
186
213
  decisions: (n: number) => count(n, "decision", "decisions"),
187
214
  decisionsTitle: "Decisions waiting for a person",
188
- agentAtWork: "agent at work",
189
- agentAtWorkTitle: "An agent is at work on it",
190
- links: (n: number) => count(n, "link", "links"),
215
+ filters: {
216
+ aria: "Narrow the board",
217
+ everyone: "Everyone",
218
+ mine: "Mine",
219
+ anyCategory: "Any category",
220
+ noCategory: "No category",
221
+ anyGoal: "Any goal",
222
+ noGoal: "No goal",
223
+ clear: "Show everything",
224
+ showing: (n: number, of: number) => `${n} of ${count(of, "task", "tasks")}`,
225
+ },
226
+ due: {
227
+ overdue: (days: number) => `${count(days, "day", "days")} overdue`,
228
+ today: "Due today",
229
+ tomorrow: "Due tomorrow",
230
+ inDays: (days: number) => `Due in ${count(days, "day", "days")}`,
231
+ on: (day: string) => `Due ${day}`,
232
+ },
191
233
  },
192
234
 
193
235
  task: {
@@ -207,12 +249,24 @@ export const en = {
207
249
  raiseDecision: "Raise a decision",
208
250
  nothingMore: "Nothing more was written about it.",
209
251
  links: "Links",
210
- agentsAtWork: "Agents at work",
211
252
  runs: "Runs",
212
253
  decisions: "Decisions",
213
254
  history: "What happened",
214
255
  subtasks: "Subtasks",
215
256
  partOfIt: "Part of it",
257
+ category: "Category",
258
+ goal: "Goal",
259
+ plan: "Plan",
260
+ planned: (from: string | undefined, to: string | undefined) =>
261
+ from !== undefined && to !== undefined
262
+ ? `${from} to ${to}`
263
+ : from !== undefined
264
+ ? `from ${from}`
265
+ : to !== undefined
266
+ ? `by ${to}`
267
+ : "",
268
+ started: "Started",
269
+ done: "Done",
216
270
  form: {
217
271
  title: "Title",
218
272
  body: "Body",
@@ -221,6 +275,12 @@ export const en = {
221
275
  priority: "Priority",
222
276
  assignee: "Assignee",
223
277
  partOf: "Part of",
278
+ category: "Category",
279
+ noCategory: "No category",
280
+ goal: "Goal",
281
+ noGoal: "No goal",
282
+ startsOn: "Starts on",
283
+ dueOn: "Due on",
224
284
  links: "Links",
225
285
  label: "Label",
226
286
  addLink: "Add a link",
@@ -244,6 +304,10 @@ export const en = {
244
304
  },
245
305
  waiting: "Waiting",
246
306
  answered: "Answered",
307
+ record: "The record",
308
+ recordLead:
309
+ "Every decision answered, newest first: what was asked, what was chosen, by whom. Open one for its context, its rationale and what followed.",
310
+ openRecord: "Open",
247
311
  waitingForPerson: "Waiting for a person.",
248
312
  options: "Options",
249
313
  notePlaceholder: "A word about it, if any",
@@ -271,6 +335,113 @@ export const en = {
271
335
  },
272
336
  },
273
337
 
338
+ calendar: {
339
+ title: "Calendar",
340
+ lead: "The tasks on their days: planned from a start to a due day, a task lies across them; due on a day alone, it sits on it. One with no day waits at the bottom to be given one.",
341
+ grid: "The month",
342
+ legend: { title: "Category colours" },
343
+ startsOnly: "Starts that day, with no due day yet",
344
+ previous: "The month before",
345
+ next: "The month after",
346
+ thisMonth: "This month",
347
+ nothingDue: "Nothing is due this month.",
348
+ overdue: "Overdue",
349
+ nothingOverdue: "Nothing is overdue.",
350
+ unscheduled: "No day set",
351
+ allScheduled: "Every open task has a day.",
352
+ workload: {
353
+ title: "Who has what",
354
+ lead: "The open tasks of each person, and what of them is late or due within the week.",
355
+ person: "Person",
356
+ open: "Open",
357
+ inProgress: "In progress",
358
+ dueSoon: "Due in 7 days",
359
+ overdue: "Overdue",
360
+ unassigned: "Nobody's yet",
361
+ },
362
+ },
363
+
364
+ roadmap: {
365
+ title: "Roadmap",
366
+ lead: "What the project works towards: each goal with the day it is meant to be reached by, and how far its tasks are. Progress is read from the board as it stands.",
367
+ setGoal: "Set a goal",
368
+ setGoalAria: "Set a goal",
369
+ editAria: "Change the goal",
370
+ one: "Goal",
371
+ all: "All goals",
372
+ empty: {
373
+ title: "No goal is set yet",
374
+ body: "An owner sets the first one: what to reach, and by when. Tasks are then written towards it.",
375
+ },
376
+ noSuch: {
377
+ title: "No such goal",
378
+ body: "It may belong to another project, or the link is wrong.",
379
+ },
380
+ onlyOwner: {
381
+ title: "Only an owner sets the roadmap",
382
+ body: "Ask one of the project's owners, or a workspace administrator.",
383
+ },
384
+ progress: (done: number, of: number) => `${done} of ${count(of, "task", "tasks")} done`,
385
+ inProgress: (n: number) => `${n} in progress`,
386
+ noTasks: "No task works towards it yet.",
387
+ tasks: "Tasks",
388
+ target: "Target",
389
+ noTarget: "No target day",
390
+ setBy: "Set by",
391
+ state: "State",
392
+ nothingMore: "Nothing more was written about it.",
393
+ form: {
394
+ title: "Title",
395
+ body: "What reaching it means",
396
+ bodyPlaceholder: "What is true once the goal is reached, in Markdown.",
397
+ state: "State",
398
+ dueOn: "Target day",
399
+ set: "Set the goal",
400
+ },
401
+ },
402
+
403
+ work: {
404
+ title: "My work",
405
+ lead: "What is on your plate across your projects: the open tasks assigned to you, by the day they are due, and the decisions that wait for a person.",
406
+ loading: "Loading your work",
407
+ couldNotLoad: "Your work could not be loaded.",
408
+ empty: {
409
+ title: "Nothing is assigned to you",
410
+ body: "A task assigned to you in any project you may see shows up here.",
411
+ },
412
+ overdue: "Overdue",
413
+ thisWeek: "Due this week",
414
+ later: "Later",
415
+ noDay: "No day set",
416
+ decisions: "Decisions waiting for a person",
417
+ noDecisions: "No decision waits.",
418
+ inProject: (project: string) => `in ${project}`,
419
+ },
420
+
421
+ categories: {
422
+ title: "Categories",
423
+ lead: "The kinds of work tasks are sorted into, the same in every project. A task carries one, the board is narrowed by one, and an administrator keeps the list.",
424
+ onlyAdmin: "An administrator adds, changes and removes them; everyone else reads them here.",
425
+ list: "Categories",
426
+ add: "Add a category",
427
+ addAria: "Add a category",
428
+ empty: {
429
+ title: "No categories",
430
+ body: "An administrator adds the first one here.",
431
+ },
432
+ inUse: "A category tasks are sorted into cannot be removed until they are moved out of it.",
433
+ form: {
434
+ name: "Name",
435
+ key: "Key",
436
+ keyHint:
437
+ "Lowercase letters, digits and hyphens: what the command and the filters say. It does not change.",
438
+ color: "Colour",
439
+ add: "Add",
440
+ },
441
+ nameOf: (key: string) => `Name of ${key}`,
442
+ colorOf: (key: string) => `Colour of ${key}`,
443
+ },
444
+
274
445
  feed: {
275
446
  title: "What happened",
276
447
  list: "What happened",
@@ -284,6 +455,8 @@ export const en = {
284
455
  something: "something",
285
456
  aQuestion: "a question",
286
457
  aFile: "a file",
458
+ aGoal: "a goal",
459
+ aCategory: "a category",
287
460
  thePage: (name: string) => `the page ${name}`.trimEnd(),
288
461
  role: {
289
462
  admin: "an administrator",
@@ -303,6 +476,12 @@ export const en = {
303
476
  visibility: "who is a member",
304
477
  skillsAddress: "the skills address",
305
478
  outcome: "what followed",
479
+ category: "the category",
480
+ goalId: "the goal",
481
+ startsOn: "the start",
482
+ dueOn: "the due date",
483
+ state: "the state",
484
+ color: "the colour",
306
485
  } as Readonly<Record<string, string>>,
307
486
  event: {
308
487
  personJoined: () => "joined the board",
@@ -315,6 +494,12 @@ export const en = {
315
494
  memberChanged: (login: string, role: string, project: string) =>
316
495
  `made ${login} ${role} of ${project}`,
317
496
  memberRemoved: (login: string, project: string) => `removed ${login} from ${project}`,
497
+ categoryCreated: (category: string) => `made the category ${category}`,
498
+ categoryUpdated: (fields: string, category: string) =>
499
+ `changed ${fields} of the category ${category}`,
500
+ categoryRemoved: (category: string) => `removed the category ${category}`,
501
+ goalCreated: (goal: string) => `set the goal ${goal}`,
502
+ goalUpdated: (fields: string, goal: string) => `changed ${fields} of the goal ${goal}`,
318
503
  taskCreated: (task: string) => `wrote ${task}`,
319
504
  taskMoved: (task: string, from: string, to: string) => `moved ${task} from ${from} to ${to}`,
320
505
  taskUpdated: (fields: string, task: string) => `changed ${fields} of ${task}`,
@@ -326,6 +511,7 @@ export const en = {
326
511
  runReported: (task: string, excerpt: string | undefined) =>
327
512
  `reported on ${task}${excerpt === undefined ? "" : `: ${excerpt}`}`,
328
513
  runHandedIn: (label: string, task: string) => `handed in ${label} on ${task}`,
514
+ runTakenOver: (task: string, agent: string) => `took over ${task} from ${agent}`,
329
515
  runFinished: (task: string) => `finished ${task}`,
330
516
  runFailed: (task: string) => `failed on ${task}`,
331
517
  runAbandoned: (task: string) => `gave up on ${task}`,
@@ -341,6 +527,7 @@ export const en = {
341
527
  for: "for",
342
528
  since: "since",
343
529
  until: "until",
530
+ lastHeard: "last heard",
344
531
  markAbandoned: "Mark abandoned",
345
532
  waitingForDecision: "Waiting for a decision",
346
533
  answerIt: "answer it",