@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.
- package/README.md +6 -6
- package/dist/api.d.ts +6 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +303 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +11 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +87 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +70 -14
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +44 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -4
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +24 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +66 -5
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +54 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +156 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/categories.d.ts +13 -0
- package/dist/components/categories.d.ts.map +1 -0
- package/dist/components/categories.js +52 -0
- package/dist/components/categories.js.map +1 -0
- package/dist/components/decision-list.d.ts +10 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +16 -1
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +23 -7
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +35 -0
- package/dist/components/goals.d.ts.map +1 -0
- package/dist/components/goals.js +45 -0
- package/dist/components/goals.js.map +1 -0
- package/dist/components/paged.d.ts +14 -0
- package/dist/components/paged.d.ts.map +1 -0
- package/dist/components/paged.js +28 -0
- package/dist/components/paged.js.map +1 -0
- package/dist/components/projects.d.ts +22 -2
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +35 -7
- package/dist/components/projects.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +2 -2
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts +2 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +1 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-form.d.ts +5 -1
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +13 -1
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +16 -0
- package/dist/components/task-list.d.ts.map +1 -0
- package/dist/components/task-list.js +16 -0
- package/dist/components/task-list.js.map +1 -0
- package/dist/components/task-view.d.ts +9 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +10 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +30 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +60 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +10 -0
- package/dist/components/work.d.ts.map +1 -0
- package/dist/components/work.js +33 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +658 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +145 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +19 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +51 -4
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +39 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +120 -0
- package/dist/dates.js.map +1 -0
- package/dist/i18n/en.d.ts +172 -5
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +183 -6
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +178 -6
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +11 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -0
- package/dist/limits.js.map +1 -1
- package/dist/marks.d.ts +12 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/marks.js +31 -0
- package/dist/marks.js.map +1 -0
- package/dist/router.d.ts +51 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +80 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +14 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +17 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +489 -3
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +150 -9
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +31 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +17 -8
- package/docs/specs/rest.md +48 -17
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +42 -0
- package/src/cli/cli.ts +362 -18
- package/src/cli/format.ts +96 -6
- package/src/cli/help.ts +77 -15
- package/src/client.ts +98 -7
- package/src/components/board.tsx +173 -13
- package/src/components/calendar.tsx +367 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +67 -5
- package/src/components/event-feed.tsx +46 -27
- package/src/components/goals.tsx +264 -0
- package/src/components/paged.tsx +39 -0
- package/src/components/projects.tsx +128 -10
- package/src/components/runs.tsx +7 -1
- package/src/components/shell.tsx +5 -2
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +64 -0
- package/src/components/task-view.tsx +75 -4
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +93 -0
- package/src/console.tsx +376 -9
- package/src/data.ts +83 -12
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +193 -6
- package/src/i18n/ko.ts +184 -6
- package/src/index.ts +62 -1
- package/src/limits.ts +11 -0
- package/src/marks.ts +40 -0
- package/src/router.ts +131 -6
- package/src/routes.ts +19 -0
- package/src/schemas.ts +187 -7
- package/src/styles/console.css +658 -0
- 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] =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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: "
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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",
|