@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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/cli.d.ts.map +1 -1
  3. package/dist/cli/cli.js +13 -5
  4. package/dist/cli/cli.js.map +1 -1
  5. package/dist/cli/help.js +1 -1
  6. package/dist/cli/help.js.map +1 -1
  7. package/dist/components/calendar.d.ts +35 -2
  8. package/dist/components/calendar.d.ts.map +1 -1
  9. package/dist/components/calendar.js +58 -19
  10. package/dist/components/calendar.js.map +1 -1
  11. package/dist/components/projects.d.ts +4 -1
  12. package/dist/components/projects.d.ts.map +1 -1
  13. package/dist/components/projects.js +5 -2
  14. package/dist/components/projects.js.map +1 -1
  15. package/dist/components/task-form.d.ts.map +1 -1
  16. package/dist/components/task-form.js +1 -1
  17. package/dist/components/task-form.js.map +1 -1
  18. package/dist/console.css +70 -30
  19. package/dist/console.d.ts.map +1 -1
  20. package/dist/console.js +1 -1
  21. package/dist/console.js.map +1 -1
  22. package/dist/i18n/en.d.ts +1 -0
  23. package/dist/i18n/en.d.ts.map +1 -1
  24. package/dist/i18n/en.js +3 -2
  25. package/dist/i18n/en.js.map +1 -1
  26. package/dist/i18n/ko.d.ts.map +1 -1
  27. package/dist/i18n/ko.js +3 -2
  28. package/dist/i18n/ko.js.map +1 -1
  29. package/dist/rules.js +1 -1
  30. package/dist/rules.js.map +1 -1
  31. package/docs/specs/cli.md +1 -1
  32. package/docs/specs/rest.md +1 -1
  33. package/package.json +1 -1
  34. package/src/cli/cli.ts +14 -5
  35. package/src/cli/help.ts +1 -1
  36. package/src/components/calendar.tsx +216 -28
  37. package/src/components/projects.tsx +6 -6
  38. package/src/components/task-form.tsx +1 -0
  39. package/src/console.tsx +1 -0
  40. package/src/i18n/en.ts +3 -2
  41. package/src/i18n/ko.ts +3 -2
  42. package/src/rules.ts +1 -1
  43. 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,wbAAwb;IACxb,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"}
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` and `GET /api/v1/me` | the run that began, the task in full, the rules that hold as they read for the person, and what to do next; a run open on the task is taken over ([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)) |
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 |
@@ -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.15",
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([project.rules(), session.client.me()]);
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: found, rules }, () =>
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. A run that was open on it is over now: the board cannot know whether its agent still ran, so taking it was deciding.`,
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(found),
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 { ReactNode, SyntheticEvent } from "react";
2
- import { OPEN_TASK_STATES, type RestCategory, type RestPerson, type RestTask } from "../api.js";
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. Takes its data as props and nothing from the network; the month or the
27
- // week shown is the address's (ADR-0010).
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
- * Marks an item whose title its line cut short, or which shows none (a bar on its later days),
149
- * so that the tip it carries in `data-tip` is shown at once while the pointer is on it. Measured
150
- * as the pointer arrives, since only the browser knows where the line ended.
151
- */
152
- function reveal(event: SyntheticEvent<HTMLElement>): void {
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 tip = (task: RestTask): string =>
162
- props.people === true && task.assignee !== null
163
- ? `#${task.number} ${task.title} \u00B7 ${personName(task.assignee)}`
164
- : `#${task.number} ${task.title}`;
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
- data-tip={tip(task)}
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), "sc-tip")}
404
+ className={cx("sc-calendar-task", tone(task, now))}
225
405
  href={props.taskHref(task)}
226
- data-tip={tip(task)}
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
- /** A project's mark: its symbol on its colour, so that projects tell apart at a glance. */
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
- {src === undefined ? (
57
- projectSymbolOf(props.project)
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
  }
@@ -96,6 +96,7 @@ export function TaskForm(props: TaskFormProps) {
96
96
  <input
97
97
  className="sc-input"
98
98
  value={title}
99
+ placeholder={form.titlePlaceholder}
99
100
  maxLength={MAX_TITLE_LENGTH}
100
101
  required
101
102
  onChange={(event) => setTitle(event.target.value)}
package/src/console.tsx CHANGED
@@ -677,6 +677,7 @@ function ProjectPage(props: {
677
677
  month={month}
678
678
  today={now}
679
679
  categories={data.categories}
680
+ goals={data.goals}
680
681
  taskHref={href}
681
682
  monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
682
683
  />
package/src/i18n/en.ts CHANGED
@@ -131,7 +131,7 @@ export const en = {
131
131
  },
132
132
 
133
133
  signIn: {
134
- lead: "The board of the work, the agents at it, what they did, and the decisions that wait for a person.",
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: "Nothing waits for a person",
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
  "",