@skillcdn/console 0.1.15 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +13 -5
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/components/calendar.d.ts +35 -2
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +58 -19
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/projects.d.ts +4 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +5 -2
- package/dist/components/projects.js.map +1 -1
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +1 -1
- package/dist/components/task-form.js.map +1 -1
- package/dist/console.css +70 -30
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +1 -1
- package/dist/console.js.map +1 -1
- package/dist/i18n/en.d.ts +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +3 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +3 -2
- package/dist/i18n/ko.js.map +1 -1
- package/dist/rules.js +1 -1
- package/dist/rules.js.map +1 -1
- package/docs/specs/cli.md +1 -1
- package/docs/specs/rest.md +1 -1
- package/package.json +1 -1
- package/src/cli/cli.ts +14 -5
- package/src/cli/help.ts +1 -1
- package/src/components/calendar.tsx +216 -28
- package/src/components/projects.tsx +6 -6
- package/src/components/task-form.tsx +1 -0
- package/src/console.tsx +1 -0
- package/src/i18n/en.ts +3 -2
- package/src/i18n/ko.ts +3 -2
- package/src/rules.ts +1 -1
- package/src/styles/console.css +70 -30
package/dist/rules.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,+FAA+F;AAC/F,2FAA2F;AAC3F,4FAA4F;AAC5F,2FAA2F;AAC3F,0FAA0F;AAC1F,mFAAmF;AAEnF,uGAAuG;AACvG,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC;AAElC,wCAAwC;AACxC,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAEnC;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,kCAAkC;IAClC,EAAE;IACF,mUAAmU;IACnU,EAAE;IACF,2BAA2B;IAC3B,EAAE;IACF,2fAA2f;IAC3f,4KAA4K;IAC5K,EAAE;IACF,8BAA8B;IAC9B,EAAE;IACF,uxBAAuxB;IACvxB,EAAE;IACF,yBAAyB;IACzB,EAAE;IACF,sRAAsR;IACtR,EAAE;IACF,wBAAwB;IACxB,EAAE;IACF,mZAAmZ;IACnZ,EAAE;IACF,yBAAyB;IACzB,EAAE;IACF,kTAAkT;IAClT,EAAE;IACF,YAAY;IACZ,EAAE;IACF,
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,+FAA+F;AAC/F,2FAA2F;AAC3F,4FAA4F;AAC5F,2FAA2F;AAC3F,0FAA0F;AAC1F,mFAAmF;AAEnF,uGAAuG;AACvG,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC;AAElC,wCAAwC;AACxC,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC;AAEnC;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,kCAAkC;IAClC,EAAE;IACF,mUAAmU;IACnU,EAAE;IACF,2BAA2B;IAC3B,EAAE;IACF,2fAA2f;IAC3f,4KAA4K;IAC5K,EAAE;IACF,8BAA8B;IAC9B,EAAE;IACF,uxBAAuxB;IACvxB,EAAE;IACF,yBAAyB;IACzB,EAAE;IACF,sRAAsR;IACtR,EAAE;IACF,wBAAwB;IACxB,EAAE;IACF,mZAAmZ;IACnZ,EAAE;IACF,yBAAyB;IACzB,EAAE;IACF,kTAAkT;IAClT,EAAE;IACF,YAAY;IACZ,EAAE;IACF,omBAAomB;IACpmB,EAAE;IACF,uBAAuB;IACvB,EAAE;IACF,qaAAqa;IACra,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,sGAAsG;AACtG,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,yBAAyB;IACzB,EAAE;IACF,6DAA6D;IAC7D,EAAE;IACF,yBAAyB;IACzB,EAAE;IACF,yBAAyB;IACzB,EAAE;IACF,cAAc;IACd,EAAE;IACF,yBAAyB;IACzB,EAAE;IACF,YAAY;IACZ,EAAE;CACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,eAAe,CAAC,IAAI,EAAE,CAAC;AAChD,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,MAAM,0BAA0B,GAAG,YAAY,CAAC;AAEvD,6GAA6G;AAC7G,MAAM,cAAc,GAAG,qCAAqC,CAAC;AAE7D,2GAA2G;AAC3G,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QAC3F,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,qCAAqC;IACvC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,QAAmC;IACzE,MAAM,IAAI,GAAG,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;IAC1E,OAAO,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC3D,CAAC"}
|
package/docs/specs/cli.md
CHANGED
|
@@ -46,7 +46,7 @@ The board is a project's ([ADR-0008](../adr/0008-a-workspace-holds-projects-and-
|
|
|
46
46
|
| `skills` | `GET .../skills` | the project's skills, or the organization's: each with its description and the URI an agent loads it by through its own SkillCDN connection; or why there are none to show; where a person connects an agent that has no connection to the address, and where the console's own skills are on this machine, when the command knows |
|
|
47
47
|
| `rules` | `GET .../rules`, or `GET /api/v1/rules` without a project named, and `GET /api/v1/me` | the rules that hold, as they read for the person (`{language}` is the language they chose, or the task's) ([ADR-0023](../adr/0023-the-rules-of-a-board-are-pages-the-workspace-and-its-projects-keep-read-first-by-every-agent.md)): the workspace's, the console's own until an administrator writes the organization's, with its version and who wrote it when written; then this project's own, or that it adds none |
|
|
48
48
|
| `rules write --body <markdown>\|--file <path>\|-\|none [--workspace] [--base <version>]` | `PUT .../docs/rules` or `POST .../docs/rules/archive`; with `--workspace`, `PUT /api/v1/rules` or `POST /api/v1/rules/archive` | the version written, that the console's own rules sent as they are wrote nothing, or that the rules are put away; for an owner of the project, or an administrator for the workspace's |
|
|
49
|
-
| `take <number\|id> [--agent <name>]` | `GET .../tasks/<ref>`, then `POST .../runs`, then `GET .../rules
|
|
49
|
+
| `take <number\|id> [--agent <name>]` | `GET .../tasks/<ref>`, then `POST .../runs`, then `GET .../rules`, `GET /api/v1/me` and `GET .../tasks/<ref>` again | the run that began, the task in full as it stands taken, the rules that hold as they read for the person, and what to do next; a run open on the task is taken over, and that is said when it happened ([ADR-0020](../adr/0020-the-console-does-not-claim-an-agent-is-at-work-a-run-says-when-it-was-last-heard-from-and-a-new-run-takes-over.md)) |
|
|
50
50
|
| `report <markdown>`, `report --file <path>`, `report -` | `POST .../runs/<id>/reports` | how many reports the run carries |
|
|
51
51
|
| `hand-in <https url \| file path> [--label <words>]` | `POST .../runs/<id>/artifacts` for a link; `POST .../runs/<id>/files` for a file, sent as a form with its name and its media type by extension | how many artifacts |
|
|
52
52
|
| `ask "<question>" --option "<label>" ... [--body <markdown>\|--file <path>] [--wait <seconds>]` | `POST .../decisions` with `runId`, then `GET .../decisions/<id>?wait=` | the decision raised, its options; then the answer, or that it still waits |
|
package/docs/specs/rest.md
CHANGED
|
@@ -155,7 +155,7 @@ The project, as the asker sees it. A patch, for an owner, signed in or with a to
|
|
|
155
155
|
|
|
156
156
|
### `POST /api/v1/projects/<key>/icon`, `DELETE .../icon` and `GET .../icon`
|
|
157
157
|
|
|
158
|
-
The project's own image for its mark, in place of the symbol on the colour. `POST`, for an owner, takes a form with a part called `file`: a PNG, a JPEG, a GIF or a WebP of at most `MAX_PROJECT_ICON_BYTES`, known by its first bytes and never by what the form says of it (`project.invalid_icon`; `request.too_large`). The bytes are kept by their hash, the project is answered with its `icon`, and the board hears `project.updated` with `fields: ["icon"]`; the same image again writes nothing. `DELETE` takes the image away and answers the project. `GET`, for whoever may see the project, answers the bytes with their type, the hash as `ETag` and `Cache-Control: private, max-age=86400`, or `304` to a matching `If-None-Match`; a project without an image is `project.no_icon`. The pages read it at `projectIconPath(key, icon)`, which carries the hash as a version.
|
|
158
|
+
The project's own image for its mark, in place of the symbol on the colour. `POST`, for an owner, takes a form with a part called `file`: a PNG, a JPEG, a GIF or a WebP of at most `MAX_PROJECT_ICON_BYTES`, known by its first bytes and never by what the form says of it (`project.invalid_icon`; `request.too_large`). The bytes are kept by their hash, the project is answered with its `icon`, and the board hears `project.updated` with `fields: ["icon"]`; the same image again writes nothing. `DELETE` takes the image away and answers the project. `GET`, for whoever may see the project, answers the bytes with their type, the hash as `ETag` and `Cache-Control: private, max-age=86400, immutable`, since the address the pages read it at carries the hash, or `304` to a matching `If-None-Match`; a project without an image is `project.no_icon`. The pages read it at `projectIconPath(key, icon)`, which carries the hash as a version.
|
|
159
159
|
|
|
160
160
|
### `GET /api/v1/projects/<key>/members` and `POST /api/v1/projects/<key>/members`
|
|
161
161
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillcdn/console",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "What a custom SkillCDN console is built from: the API client and schemas, the components, the composition of the default console, and the console command agents work the board with.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/skillcdn/console#readme",
|
package/src/cli/cli.ts
CHANGED
|
@@ -974,17 +974,26 @@ const take: Command = async (args, io) => {
|
|
|
974
974
|
...(agent === undefined ? {} : { agent }),
|
|
975
975
|
});
|
|
976
976
|
// The rules come with the run, as they read for the person: everything from here acts
|
|
977
|
-
// within them (ADR-0023).
|
|
978
|
-
const [ruling, me] = await Promise.all([
|
|
977
|
+
// within them (ADR-0023); the task is read again, as it stands now that it is taken.
|
|
978
|
+
const [ruling, me, taken] = await Promise.all([
|
|
979
|
+
project.rules(),
|
|
980
|
+
session.client.me(),
|
|
981
|
+
project.task(ref),
|
|
982
|
+
]);
|
|
983
|
+
// Taking a task with a run open on it ends that run (ADR-0020): said when it happened.
|
|
984
|
+
const tookOver =
|
|
985
|
+
found.openRuns > 0
|
|
986
|
+
? " A run that was open on it is over now: the board cannot know whether its agent still ran, so taking it was deciding."
|
|
987
|
+
: "";
|
|
979
988
|
const rules = {
|
|
980
989
|
workspace: forPerson(ruling.workspace, me.language),
|
|
981
990
|
project: forPerson(ruling.project, me.language),
|
|
982
991
|
};
|
|
983
|
-
answer(io, session, { run, task:
|
|
992
|
+
answer(io, session, { run, task: taken, rules }, () =>
|
|
984
993
|
[
|
|
985
|
-
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs
|
|
994
|
+
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}`,
|
|
986
995
|
"",
|
|
987
|
-
formatTask(
|
|
996
|
+
formatTask(taken),
|
|
988
997
|
"",
|
|
989
998
|
formatRules(rules),
|
|
990
999
|
"",
|
package/src/cli/help.ts
CHANGED
|
@@ -126,7 +126,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
126
126
|
about:
|
|
127
127
|
"One task in full, with its plan (category, goal, start and due days), what happened to it (when it was started and done, which the board keeps itself), the decisions about it and the runs on it; or a new task written down; or a task changed, only in what the flags say, where none clears a field (priorities: low, normal, high, urgent).",
|
|
128
128
|
limits: [
|
|
129
|
-
`title: one line, up to ${MAX_TITLE_LENGTH} characters`,
|
|
129
|
+
`title: one line, up to ${MAX_TITLE_LENGTH} characters, saying what is to be done on its own; what it refers to goes in the body`,
|
|
130
130
|
`body: ${MARKDOWN}`,
|
|
131
131
|
...TASK_FIELD_LIMITS,
|
|
132
132
|
],
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
1
|
+
import { type ReactNode, type SyntheticEvent, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
OPEN_TASK_STATES,
|
|
4
|
+
type RestCategory,
|
|
5
|
+
type RestGoal,
|
|
6
|
+
type RestPerson,
|
|
7
|
+
type RestTask,
|
|
8
|
+
} from "../api.js";
|
|
3
9
|
import {
|
|
4
10
|
addDays,
|
|
5
11
|
addMonths,
|
|
6
12
|
daysBetween,
|
|
7
13
|
firstDayOfWeek,
|
|
14
|
+
formatDay,
|
|
8
15
|
formatDays,
|
|
9
16
|
formatMonth,
|
|
10
17
|
formatWeekday,
|
|
@@ -16,15 +23,16 @@ import {
|
|
|
16
23
|
weekOf,
|
|
17
24
|
} from "../dates.js";
|
|
18
25
|
import { useLanguage, useWords } from "../i18n/index.js";
|
|
19
|
-
import { Avatar, cx, PersonChip, personName } from "./ui.js";
|
|
26
|
+
import { Avatar, CategoryBadge, cx, PersonChip, personName, StateBadge, Time } from "./ui.js";
|
|
20
27
|
|
|
21
28
|
// The calendar of a project (ADR-0019): the month as a grid, or one week on its own. A task
|
|
22
29
|
// planned from a start to a due day lies across its days as a bar, in a lane of its own within
|
|
23
30
|
// each week so that the bars of one week never cross; a task with one day alone, a due day or
|
|
24
31
|
// a start, sits on that day as a dot. On a project's calendar the person who has a task stands
|
|
25
32
|
// before its title, since the days hold several people's work. Who has what says whether the
|
|
26
|
-
// work is on its days.
|
|
27
|
-
//
|
|
33
|
+
// work is on its days. The task under the pointer is shown in full within its item, with the
|
|
34
|
+
// facts its own page lists, above or below it where there is more room. Takes its data as props
|
|
35
|
+
// and nothing from the network; the month or the week shown is the address's (ADR-0010).
|
|
28
36
|
|
|
29
37
|
const isOpen = (task: RestTask): boolean => OPEN_TASK_STATES.includes(task.state);
|
|
30
38
|
|
|
@@ -40,6 +48,8 @@ export interface CalendarProps {
|
|
|
40
48
|
readonly today?: string | undefined;
|
|
41
49
|
/** The workspace's categories, for the legend to say what the colours mean. */
|
|
42
50
|
readonly categories?: readonly RestCategory[] | undefined;
|
|
51
|
+
/** The project's goals, so that the task under the pointer is shown with its goal's title. */
|
|
52
|
+
readonly goals?: readonly RestGoal[] | undefined;
|
|
43
53
|
readonly taskHref: (task: RestTask) => string;
|
|
44
54
|
/** Where the calendar is on another month. */
|
|
45
55
|
readonly monthHref: (month: string) => string;
|
|
@@ -131,6 +141,174 @@ const tone = (task: RestTask, now: string): string =>
|
|
|
131
141
|
isOpen(task) && task.dueOn !== null && task.dueOn < now && "sc-calendar-late",
|
|
132
142
|
);
|
|
133
143
|
|
|
144
|
+
/** Where the card of the item under the pointer goes: the side with more room, kept within the window. */
|
|
145
|
+
export interface TipPlace {
|
|
146
|
+
readonly side: "above" | "below";
|
|
147
|
+
/** How far the card moves from the item's left edge, in pixels, to stay within the window: zero or less. */
|
|
148
|
+
readonly shift: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** What the card may grow to, in pixels: 20rem at the usual root size, as the style says. */
|
|
152
|
+
const TIP_WIDTH = 320;
|
|
153
|
+
/** What stays between the card and the window's edge, in pixels. */
|
|
154
|
+
const TIP_MARGIN = 8;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Places the card beside an item, from the item's box and the window's size, both in pixels from
|
|
158
|
+
* the window's top left corner: above the item where the room above is at least the room below,
|
|
159
|
+
* else below it, and moved left where it would otherwise leave the window on the right.
|
|
160
|
+
*/
|
|
161
|
+
export function placeTip(
|
|
162
|
+
item: { readonly top: number; readonly bottom: number; readonly left: number },
|
|
163
|
+
window: { readonly width: number; readonly height: number },
|
|
164
|
+
): TipPlace {
|
|
165
|
+
const side = item.top >= window.height - item.bottom ? "above" : "below";
|
|
166
|
+
const left = Math.max(TIP_MARGIN, Math.min(item.left, window.width - TIP_WIDTH - TIP_MARGIN));
|
|
167
|
+
return { side, shift: Math.min(0, left - item.left) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The item the pointer is on: a task on a day, since a bar has an item on each of its days, and where its card goes. */
|
|
171
|
+
interface Pointed {
|
|
172
|
+
readonly task: RestTask;
|
|
173
|
+
readonly day: string;
|
|
174
|
+
readonly place: TipPlace;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The item under the pointer, measured as the pointer arrives, since only the browser knows where it is. */
|
|
178
|
+
function usePointed(): [
|
|
179
|
+
Pointed | null,
|
|
180
|
+
(task: RestTask, day: string, item: HTMLElement) => void,
|
|
181
|
+
() => void,
|
|
182
|
+
] {
|
|
183
|
+
const [pointed, setPointed] = useState<Pointed | null>(null);
|
|
184
|
+
const point = (task: RestTask, day: string, item: HTMLElement): void =>
|
|
185
|
+
setPointed({
|
|
186
|
+
task,
|
|
187
|
+
day,
|
|
188
|
+
place: placeTip(item.getBoundingClientRect(), {
|
|
189
|
+
width: window.innerWidth,
|
|
190
|
+
height: window.innerHeight,
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
const leave = (): void => setPointed(null);
|
|
194
|
+
return [pointed, point, leave];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface TaskTipProps {
|
|
198
|
+
readonly task: RestTask;
|
|
199
|
+
/** The project's goals, so that the task's goal is named by its title; without them the row is left out. */
|
|
200
|
+
readonly goals?: readonly RestGoal[] | undefined;
|
|
201
|
+
readonly place: TipPlace;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The task under the pointer in full: its number and its title, then the facts its own page lists
|
|
206
|
+
* beside the text, those that are set. Lies within the item it belongs to, above or below it, and
|
|
207
|
+
* out of the accessible tree: the item's text is the title, and the facts are on the task's page.
|
|
208
|
+
*/
|
|
209
|
+
export function TaskTip(props: TaskTipProps) {
|
|
210
|
+
const words = useWords();
|
|
211
|
+
const { tag } = useLanguage();
|
|
212
|
+
const { task, place } = props;
|
|
213
|
+
const goal = props.goals?.find((candidate) => candidate.id === task.goalId);
|
|
214
|
+
const day = (value: string): string => formatDay(value, tag);
|
|
215
|
+
return (
|
|
216
|
+
<div
|
|
217
|
+
className={cx(
|
|
218
|
+
"sc-task-tip",
|
|
219
|
+
place.side === "above" ? "sc-task-tip-above" : "sc-task-tip-below",
|
|
220
|
+
)}
|
|
221
|
+
style={{ left: place.shift }}
|
|
222
|
+
aria-hidden="true"
|
|
223
|
+
>
|
|
224
|
+
<p className="sc-task-tip-title">
|
|
225
|
+
#{task.number} {task.title}
|
|
226
|
+
</p>
|
|
227
|
+
<dl className="sc-task-tip-facts">
|
|
228
|
+
<div>
|
|
229
|
+
<dt>{words.task.state}</dt>
|
|
230
|
+
<dd>
|
|
231
|
+
<StateBadge state={task.state} />
|
|
232
|
+
</dd>
|
|
233
|
+
</div>
|
|
234
|
+
<div>
|
|
235
|
+
<dt>{words.task.form.priority}</dt>
|
|
236
|
+
<dd>{words.vocabulary.priority[task.priority]}</dd>
|
|
237
|
+
</div>
|
|
238
|
+
{task.category !== null && (
|
|
239
|
+
<div>
|
|
240
|
+
<dt>{words.task.category}</dt>
|
|
241
|
+
<dd>
|
|
242
|
+
<CategoryBadge category={task.category} />
|
|
243
|
+
</dd>
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
{goal !== undefined && (
|
|
247
|
+
<div>
|
|
248
|
+
<dt>{words.task.goal}</dt>
|
|
249
|
+
<dd>{goal.title}</dd>
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
<div>
|
|
253
|
+
<dt>{words.task.owner}</dt>
|
|
254
|
+
<dd>
|
|
255
|
+
<PersonChip person={task.owner} />
|
|
256
|
+
</dd>
|
|
257
|
+
</div>
|
|
258
|
+
<div>
|
|
259
|
+
<dt>{words.task.assignee}</dt>
|
|
260
|
+
<dd>
|
|
261
|
+
{task.assignee === null ? (
|
|
262
|
+
<span className="sc-muted">{words.common.nobody}</span>
|
|
263
|
+
) : (
|
|
264
|
+
<PersonChip person={task.assignee} />
|
|
265
|
+
)}
|
|
266
|
+
</dd>
|
|
267
|
+
</div>
|
|
268
|
+
{(task.startsOn !== null || task.dueOn !== null) && (
|
|
269
|
+
<div>
|
|
270
|
+
<dt>{words.task.plan}</dt>
|
|
271
|
+
<dd>
|
|
272
|
+
{words.task.planned(
|
|
273
|
+
task.startsOn === null ? undefined : day(task.startsOn),
|
|
274
|
+
task.dueOn === null ? undefined : day(task.dueOn),
|
|
275
|
+
)}
|
|
276
|
+
</dd>
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
{task.startedAt !== null && (
|
|
280
|
+
<div>
|
|
281
|
+
<dt>{words.task.started}</dt>
|
|
282
|
+
<dd>
|
|
283
|
+
<Time iso={task.startedAt} />
|
|
284
|
+
</dd>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
{task.doneAt !== null && (
|
|
288
|
+
<div>
|
|
289
|
+
<dt>{words.task.done}</dt>
|
|
290
|
+
<dd>
|
|
291
|
+
<Time iso={task.doneAt} />
|
|
292
|
+
</dd>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
<div>
|
|
296
|
+
<dt>{words.task.written}</dt>
|
|
297
|
+
<dd>
|
|
298
|
+
<Time iso={task.createdAt} />
|
|
299
|
+
</dd>
|
|
300
|
+
</div>
|
|
301
|
+
<div>
|
|
302
|
+
<dt>{words.task.changed}</dt>
|
|
303
|
+
<dd>
|
|
304
|
+
<Time iso={task.updatedAt} />
|
|
305
|
+
</dd>
|
|
306
|
+
</div>
|
|
307
|
+
</dl>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
134
312
|
interface WeekRowProps {
|
|
135
313
|
readonly week: readonly GridDay[];
|
|
136
314
|
readonly segments: readonly WeekSegment[];
|
|
@@ -142,26 +320,30 @@ interface WeekRowProps {
|
|
|
142
320
|
readonly markOf?: ((task: RestTask) => ReactNode) | undefined;
|
|
143
321
|
/** Whether the person who has a task stands before its title, where the days hold several people's work. */
|
|
144
322
|
readonly people?: boolean | undefined;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
*/
|
|
152
|
-
|
|
153
|
-
const item = event.currentTarget;
|
|
154
|
-
const title = item.querySelector(".sc-calendar-task-title");
|
|
155
|
-
item.toggleAttribute("data-clipped", title === null || title.scrollWidth > title.clientWidth);
|
|
323
|
+
/** The item under the pointer, shown in full within that item, or none. */
|
|
324
|
+
readonly pointed: Pointed | null;
|
|
325
|
+
/** The project's goals, so that the item under the pointer names its goal; none across projects. */
|
|
326
|
+
readonly goals?: readonly RestGoal[] | undefined;
|
|
327
|
+
/** The pointer, or the focus, arrived on a task's item on a day. */
|
|
328
|
+
readonly onPoint: (task: RestTask, day: string, item: HTMLElement) => void;
|
|
329
|
+
/** It left. */
|
|
330
|
+
readonly onLeave: () => void;
|
|
156
331
|
}
|
|
157
332
|
|
|
158
333
|
/** One week as a row of cells: the bars across the days in their lanes, then the tasks on each day. */
|
|
159
334
|
function WeekRow(props: WeekRowProps) {
|
|
160
335
|
const { week, segments, now } = props;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
336
|
+
const pointing = (task: RestTask, day: string) => ({
|
|
337
|
+
onMouseEnter: (event: SyntheticEvent<HTMLElement>) =>
|
|
338
|
+
props.onPoint(task, day, event.currentTarget),
|
|
339
|
+
onMouseLeave: props.onLeave,
|
|
340
|
+
onFocus: (event: SyntheticEvent<HTMLElement>) => props.onPoint(task, day, event.currentTarget),
|
|
341
|
+
onBlur: props.onLeave,
|
|
342
|
+
});
|
|
343
|
+
const tipIn = (task: RestTask, day: string): ReactNode =>
|
|
344
|
+
props.pointed !== null && props.pointed.task.id === task.id && props.pointed.day === day ? (
|
|
345
|
+
<TaskTip task={task} goals={props.goals} place={props.pointed.place} />
|
|
346
|
+
) : null;
|
|
165
347
|
const who = (task: RestTask): ReactNode =>
|
|
166
348
|
props.people === true && task.assignee !== null ? (
|
|
167
349
|
<Avatar person={task.assignee} size="xs" />
|
|
@@ -199,12 +381,9 @@ function WeekRow(props: WeekRowProps) {
|
|
|
199
381
|
segment.from === column && segment.begins && "sc-calendar-bar-begins",
|
|
200
382
|
segment.to === column && segment.ends && "sc-calendar-bar-ends",
|
|
201
383
|
tone(task, now),
|
|
202
|
-
"sc-tip",
|
|
203
384
|
)}
|
|
204
385
|
href={props.taskHref(task)}
|
|
205
|
-
|
|
206
|
-
onMouseEnter={reveal}
|
|
207
|
-
onFocus={reveal}
|
|
386
|
+
{...pointing(task, cell.day)}
|
|
208
387
|
>
|
|
209
388
|
{segment.from === column && (
|
|
210
389
|
<>
|
|
@@ -213,6 +392,7 @@ function WeekRow(props: WeekRowProps) {
|
|
|
213
392
|
<span className="sc-calendar-task-title">{task.title}</span>
|
|
214
393
|
</>
|
|
215
394
|
)}
|
|
395
|
+
{tipIn(task, cell.day)}
|
|
216
396
|
</a>
|
|
217
397
|
);
|
|
218
398
|
})}
|
|
@@ -221,11 +401,9 @@ function WeekRow(props: WeekRowProps) {
|
|
|
221
401
|
return (
|
|
222
402
|
<a
|
|
223
403
|
key={task.id}
|
|
224
|
-
className={cx("sc-calendar-task", tone(task, now)
|
|
404
|
+
className={cx("sc-calendar-task", tone(task, now))}
|
|
225
405
|
href={props.taskHref(task)}
|
|
226
|
-
|
|
227
|
-
onMouseEnter={reveal}
|
|
228
|
-
onFocus={reveal}
|
|
406
|
+
{...pointing(task, cell.day)}
|
|
229
407
|
>
|
|
230
408
|
{mark === undefined || mark === null ? (
|
|
231
409
|
<span
|
|
@@ -239,6 +417,7 @@ function WeekRow(props: WeekRowProps) {
|
|
|
239
417
|
)}
|
|
240
418
|
{who(task)}
|
|
241
419
|
<span className="sc-calendar-task-title">{task.title}</span>
|
|
420
|
+
{tipIn(task, cell.day)}
|
|
242
421
|
</a>
|
|
243
422
|
);
|
|
244
423
|
})}
|
|
@@ -251,6 +430,7 @@ function WeekRow(props: WeekRowProps) {
|
|
|
251
430
|
export function Calendar(props: CalendarProps) {
|
|
252
431
|
const words = useWords().calendar;
|
|
253
432
|
const { tag } = useLanguage();
|
|
433
|
+
const [pointed, point, leave] = usePointed();
|
|
254
434
|
const now = props.today ?? todayOf();
|
|
255
435
|
const firstDay = firstDayOfWeek(tag);
|
|
256
436
|
const weeks = monthGrid(props.month, firstDay);
|
|
@@ -310,6 +490,10 @@ export function Calendar(props: CalendarProps) {
|
|
|
310
490
|
on={on}
|
|
311
491
|
now={now}
|
|
312
492
|
taskHref={props.taskHref}
|
|
493
|
+
pointed={pointed}
|
|
494
|
+
goals={props.goals}
|
|
495
|
+
onPoint={point}
|
|
496
|
+
onLeave={leave}
|
|
313
497
|
people
|
|
314
498
|
/>
|
|
315
499
|
))}
|
|
@@ -349,6 +533,7 @@ export interface WeekProps {
|
|
|
349
533
|
export function Week(props: WeekProps) {
|
|
350
534
|
const words = useWords().calendar.week;
|
|
351
535
|
const { tag } = useLanguage();
|
|
536
|
+
const [pointed, point, leave] = usePointed();
|
|
352
537
|
const now = props.today ?? todayOf();
|
|
353
538
|
const firstDay = firstDayOfWeek(tag);
|
|
354
539
|
const days = weekOf(props.day ?? now, firstDay);
|
|
@@ -400,6 +585,9 @@ export function Week(props: WeekProps) {
|
|
|
400
585
|
now={now}
|
|
401
586
|
taskHref={props.taskHref}
|
|
402
587
|
markOf={props.markOf}
|
|
588
|
+
pointed={pointed}
|
|
589
|
+
onPoint={point}
|
|
590
|
+
onLeave={leave}
|
|
403
591
|
/>
|
|
404
592
|
</tbody>
|
|
405
593
|
</table>
|
|
@@ -25,7 +25,10 @@ import {
|
|
|
25
25
|
import { useWords } from "../i18n/index.js";
|
|
26
26
|
import { Badge, Button, cx, PersonChip, personName, Time } from "./ui.js";
|
|
27
27
|
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* A project's mark: its symbol on its colour, so that projects tell apart at a glance; its own
|
|
30
|
+
* image over the symbol where it has one, so that the mark is never empty while the image loads.
|
|
31
|
+
*/
|
|
29
32
|
export function ProjectMark(props: {
|
|
30
33
|
readonly project: {
|
|
31
34
|
readonly key: string;
|
|
@@ -53,11 +56,8 @@ export function ProjectMark(props: {
|
|
|
53
56
|
)}
|
|
54
57
|
aria-hidden="true"
|
|
55
58
|
>
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
) : (
|
|
59
|
-
<img className="sc-mark-image" src={src} alt="" />
|
|
60
|
-
)}
|
|
59
|
+
{projectSymbolOf(props.project)}
|
|
60
|
+
{src !== undefined && <img className="sc-mark-image" src={src} alt="" />}
|
|
61
61
|
</span>
|
|
62
62
|
);
|
|
63
63
|
}
|
package/src/console.tsx
CHANGED
package/src/i18n/en.ts
CHANGED
|
@@ -131,7 +131,7 @@ export const en = {
|
|
|
131
131
|
},
|
|
132
132
|
|
|
133
133
|
signIn: {
|
|
134
|
-
lead: "
|
|
134
|
+
lead: "Join an organization made of AI agents.",
|
|
135
135
|
continueWith: (provider: string) => `Continue with ${provider}`,
|
|
136
136
|
nobody: {
|
|
137
137
|
title: "Nobody can sign in here yet",
|
|
@@ -269,6 +269,7 @@ export const en = {
|
|
|
269
269
|
done: "Done",
|
|
270
270
|
form: {
|
|
271
271
|
title: "Title",
|
|
272
|
+
titlePlaceholder: "What is to be done, in plain words that stand on their own.",
|
|
272
273
|
body: "Body",
|
|
273
274
|
bodyPlaceholder: "What is to be done, in Markdown.",
|
|
274
275
|
state: "State",
|
|
@@ -299,7 +300,7 @@ export const en = {
|
|
|
299
300
|
body: "It may belong to another project, or the link is wrong.",
|
|
300
301
|
},
|
|
301
302
|
empty: {
|
|
302
|
-
title: "
|
|
303
|
+
title: "No decision waits for a person",
|
|
303
304
|
body: "A decision raised from a task, or from here, shows up on this page.",
|
|
304
305
|
},
|
|
305
306
|
waiting: "Waiting",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -101,7 +101,7 @@ export const ko: Messages = {
|
|
|
101
101
|
},
|
|
102
102
|
|
|
103
103
|
signIn: {
|
|
104
|
-
lead: "
|
|
104
|
+
lead: "AI 에이전트로 이뤄진 조직에 합류하세요.",
|
|
105
105
|
continueWith: (provider) => `${provider}로 계속`,
|
|
106
106
|
nobody: {
|
|
107
107
|
title: "아직 아무도 로그인할 수 없습니다",
|
|
@@ -238,6 +238,7 @@ export const ko: Messages = {
|
|
|
238
238
|
done: "완료",
|
|
239
239
|
form: {
|
|
240
240
|
title: "제목",
|
|
241
|
+
titlePlaceholder: "무엇을 할지, 제목만 보고도 알 수 있게.",
|
|
241
242
|
body: "본문",
|
|
242
243
|
bodyPlaceholder: "무엇을 할지, Markdown으로.",
|
|
243
244
|
state: "상태",
|
|
@@ -268,7 +269,7 @@ export const ko: Messages = {
|
|
|
268
269
|
body: "다른 프로젝트의 것이거나, 링크가 잘못됐습니다.",
|
|
269
270
|
},
|
|
270
271
|
empty: {
|
|
271
|
-
title: "사람을 기다리는
|
|
272
|
+
title: "사람을 기다리는 결정이 없습니다",
|
|
272
273
|
body: "작업에서, 또는 여기서 요청한 결정이 이 페이지에 나타납니다.",
|
|
273
274
|
},
|
|
274
275
|
waiting: "대기 중",
|
package/src/rules.ts
CHANGED
|
@@ -44,7 +44,7 @@ export const REFERENCE_RULES = [
|
|
|
44
44
|
"",
|
|
45
45
|
"## Reports",
|
|
46
46
|
"",
|
|
47
|
-
"Report at the milestones, not every step: what was found, what was done, what is next, in words for people; a problem as a problem, with what it blocks. What people read on the board, the reports, the questions and their options, the tasks and the pages, is written in {language}. The result goes in a last report or is handed in as a link or a file, with the check that ran and its result; a check that did not run is said to be not run.",
|
|
47
|
+
"Report at the milestones, not every step: what was found, what was done, what is next, in words for people; a problem as a problem, with what it blocks. What people read on the board, the reports, the questions and their options, the tasks and the pages, is written in {language}. A task's title says what is to be done, in plain words that stand on their own; what it refers to, a milestone, a commit, a page, goes in the body, linked where it can be. The result goes in a last report or is handed in as a link or a file, with the check that ran and its result; a check that did not run is said to be not run.",
|
|
48
48
|
"",
|
|
49
49
|
"## What never happens",
|
|
50
50
|
"",
|