@skillcdn/console 0.1.18 → 0.1.19

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 (121) hide show
  1. package/README.md +1 -1
  2. package/SKILLCDN.md +1 -1
  3. package/dist/api.d.ts +1 -1
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +1 -1
  6. package/dist/api.js.map +1 -1
  7. package/dist/cli/cli.d.ts +1 -1
  8. package/dist/cli/cli.d.ts.map +1 -1
  9. package/dist/cli/cli.js +353 -39
  10. package/dist/cli/cli.js.map +1 -1
  11. package/dist/cli/credentials.d.ts +102 -3
  12. package/dist/cli/credentials.d.ts.map +1 -1
  13. package/dist/cli/credentials.js +177 -8
  14. package/dist/cli/credentials.js.map +1 -1
  15. package/dist/cli/directory.d.ts +6 -0
  16. package/dist/cli/directory.d.ts.map +1 -1
  17. package/dist/cli/directory.js +37 -6
  18. package/dist/cli/directory.js.map +1 -1
  19. package/dist/cli/format.d.ts +10 -1
  20. package/dist/cli/format.d.ts.map +1 -1
  21. package/dist/cli/format.js +106 -2
  22. package/dist/cli/format.js.map +1 -1
  23. package/dist/cli/help.d.ts.map +1 -1
  24. package/dist/cli/help.js +42 -20
  25. package/dist/cli/help.js.map +1 -1
  26. package/dist/client.d.ts +21 -1
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +14 -1
  29. package/dist/client.js.map +1 -1
  30. package/dist/components/board.d.ts +9 -1
  31. package/dist/components/board.d.ts.map +1 -1
  32. package/dist/components/board.js +82 -5
  33. package/dist/components/board.js.map +1 -1
  34. package/dist/components/comments.d.ts.map +1 -1
  35. package/dist/components/comments.js +12 -4
  36. package/dist/components/comments.js.map +1 -1
  37. package/dist/components/event-feed.d.ts.map +1 -1
  38. package/dist/components/event-feed.js +4 -0
  39. package/dist/components/event-feed.js.map +1 -1
  40. package/dist/components/goals.d.ts +17 -1
  41. package/dist/components/goals.d.ts.map +1 -1
  42. package/dist/components/goals.js +34 -9
  43. package/dist/components/goals.js.map +1 -1
  44. package/dist/components/markdown.d.ts +6 -0
  45. package/dist/components/markdown.d.ts.map +1 -1
  46. package/dist/components/markdown.js +7 -0
  47. package/dist/components/markdown.js.map +1 -1
  48. package/dist/components/pictures.d.ts +68 -0
  49. package/dist/components/pictures.d.ts.map +1 -0
  50. package/dist/components/pictures.js +213 -0
  51. package/dist/components/pictures.js.map +1 -0
  52. package/dist/components/projects.d.ts +12 -0
  53. package/dist/components/projects.d.ts.map +1 -1
  54. package/dist/components/projects.js +15 -0
  55. package/dist/components/projects.js.map +1 -1
  56. package/dist/components/task-view.d.ts +3 -0
  57. package/dist/components/task-view.d.ts.map +1 -1
  58. package/dist/components/task-view.js +6 -4
  59. package/dist/components/task-view.js.map +1 -1
  60. package/dist/console.css +282 -3
  61. package/dist/console.d.ts.map +1 -1
  62. package/dist/console.js +124 -10
  63. package/dist/console.js.map +1 -1
  64. package/dist/data.d.ts +16 -1
  65. package/dist/data.d.ts.map +1 -1
  66. package/dist/data.js +47 -2
  67. package/dist/data.js.map +1 -1
  68. package/dist/i18n/en.d.ts +52 -0
  69. package/dist/i18n/en.d.ts.map +1 -1
  70. package/dist/i18n/en.js +50 -2
  71. package/dist/i18n/en.js.map +1 -1
  72. package/dist/i18n/ko.d.ts.map +1 -1
  73. package/dist/i18n/ko.js +50 -2
  74. package/dist/i18n/ko.js.map +1 -1
  75. package/dist/index.d.ts +5 -4
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +5 -4
  78. package/dist/index.js.map +1 -1
  79. package/dist/router.d.ts +25 -6
  80. package/dist/router.d.ts.map +1 -1
  81. package/dist/router.js +38 -7
  82. package/dist/router.js.map +1 -1
  83. package/dist/rules.js +5 -5
  84. package/dist/rules.js.map +1 -1
  85. package/dist/schemas.d.ts +158 -0
  86. package/dist/schemas.d.ts.map +1 -1
  87. package/dist/schemas.js +78 -0
  88. package/dist/schemas.js.map +1 -1
  89. package/dist/vocabulary.d.ts +2 -2
  90. package/dist/vocabulary.d.ts.map +1 -1
  91. package/dist/vocabulary.js +3 -0
  92. package/dist/vocabulary.js.map +1 -1
  93. package/docs/specs/cli.md +17 -9
  94. package/docs/specs/rest.md +22 -4
  95. package/package.json +1 -1
  96. package/skills/working-the-board/SKILL.md +10 -4
  97. package/src/api.ts +13 -0
  98. package/src/cli/cli.ts +441 -42
  99. package/src/cli/credentials.ts +276 -14
  100. package/src/cli/directory.ts +43 -6
  101. package/src/cli/format.ts +138 -2
  102. package/src/cli/help.ts +44 -20
  103. package/src/client.ts +60 -0
  104. package/src/components/board.tsx +154 -8
  105. package/src/components/comments.tsx +20 -10
  106. package/src/components/event-feed.tsx +4 -0
  107. package/src/components/goals.tsx +173 -106
  108. package/src/components/markdown.tsx +19 -0
  109. package/src/components/pictures.tsx +389 -0
  110. package/src/components/projects.tsx +68 -0
  111. package/src/components/task-view.tsx +43 -2
  112. package/src/console.tsx +259 -4
  113. package/src/data.ts +72 -1
  114. package/src/i18n/en.ts +53 -2
  115. package/src/i18n/ko.ts +53 -2
  116. package/src/index.ts +27 -0
  117. package/src/router.ts +57 -9
  118. package/src/rules.ts +5 -5
  119. package/src/schemas.ts +97 -0
  120. package/src/styles/console.css +282 -3
  121. package/src/vocabulary.ts +3 -0
@@ -10,13 +10,15 @@ import {
10
10
  type RestTask,
11
11
  TASK_STATES,
12
12
  } from "../api.js";
13
- import { formatDays } from "../dates.js";
13
+ import { formatDays, monthOf } from "../dates.js";
14
14
  import { useLanguage, useWords } from "../i18n/index.js";
15
+ import { GOAL_TASK_VIEWS, type GoalTaskView } from "../router.js";
15
16
  import { Markdown } from "./markdown.js";
16
17
  import { TaskList } from "./task-list.js";
17
18
  import {
18
19
  Button,
19
20
  CategoryBadge,
21
+ cx,
20
22
  Day,
21
23
  DueBadge,
22
24
  GoalStateBadge,
@@ -27,9 +29,10 @@ import {
27
29
 
28
30
  // The roadmap of a project (ADR-0019): its goals, each planned in days as a task is, a start
29
31
  // and a target, sorted into a category of the workspace (ADR-0026), with how far its tasks
30
- // are, read from the board as it stands. The list, one goal with its tasks, and the form an
31
- // owner sets one with. Each takes its data as props and nothing from the network; what people
32
- // wrote is shown as text, never as HTML.
32
+ // are, read from the board as it stands. The list, one goal with its tasks (as the board of
33
+ // them, the list by state, or the calendar, the first two given by the composition from the
34
+ // board's own parts), and the form an owner sets one with. Each takes its data as props and
35
+ // nothing from the network; what people wrote is shown as text, never as HTML.
33
36
 
34
37
  /** A goal's day on the calendar's terms: reached or let go, it is a plain fact; else it may be late. */
35
38
  const dueState = (goal: RestGoal) =>
@@ -208,9 +211,23 @@ export function GoalForm(props: GoalFormProps) {
208
211
  );
209
212
  }
210
213
 
214
+ /**
215
+ * The month a goal's calendar opens on: the month it starts while that is still to come, the
216
+ * month it is meant to be reached by once that is past, and this month otherwise.
217
+ */
218
+ export function goalMonth(goal: RestGoal, today: string): string {
219
+ if (goal.startsOn !== null && today < goal.startsOn) {
220
+ return monthOf(goal.startsOn);
221
+ }
222
+ if (goal.dueOn !== null && today > goal.dueOn) {
223
+ return monthOf(goal.dueOn);
224
+ }
225
+ return monthOf(today);
226
+ }
227
+
211
228
  export interface GoalViewProps {
212
229
  readonly goal: RestGoal;
213
- /** The tasks of the board; those that work towards the goal are shown, by state. */
230
+ /** The tasks of the board; those that work towards the goal are shown. */
214
231
  readonly tasks: readonly RestTask[];
215
232
  readonly taskHref: (task: RestTask) => string;
216
233
  /** Where a document of the project is read, by its path, for the links in what was written. */
@@ -221,6 +238,16 @@ export interface GoalViewProps {
221
238
  /** Called when an owner moves the goal to another state from here. */
222
239
  readonly onMove?: ((goal: RestGoal, state: GoalState) => void) | undefined;
223
240
  readonly busy?: boolean | undefined;
241
+ /** Which view of the goal's tasks is shown: the list when left out. */
242
+ readonly view?: GoalTaskView | undefined;
243
+ /** Where each view is; left out, the views cannot be switched from here. */
244
+ readonly viewHref?: ((view: GoalTaskView) => string) | undefined;
245
+ /** The board of the goal's tasks, built by the composition from the board's own parts; the list stands in without it. */
246
+ readonly board?: ReactNode;
247
+ /** The calendar of the goal's tasks, the same way. */
248
+ readonly calendar?: ReactNode;
249
+ /** Called to put every done or dropped task of the goal away (ADR-0029); left out, it is not offered. */
250
+ readonly onArchiveTasks?: (() => void) | undefined;
224
251
  }
225
252
 
226
253
  export function GoalView(props: GoalViewProps) {
@@ -228,116 +255,156 @@ export function GoalView(props: GoalViewProps) {
228
255
  const roadmap = words.roadmap;
229
256
  const { goal } = props;
230
257
  const towards = props.tasks.filter((task) => task.goalId === goal.id);
258
+ const view: GoalTaskView = props.view ?? "list";
259
+ const { viewHref } = props;
260
+ const done = goal.taskCounts.done;
261
+ const total = Object.values(goal.taskCounts).reduce((sum, n) => sum + n, 0);
262
+ const list =
263
+ towards.length === 0 ? (
264
+ <p className="sc-muted">{roadmap.noTasks}</p>
265
+ ) : (
266
+ TASK_STATES.map((state) => {
267
+ const inState = towards.filter((task) => task.state === state);
268
+ return inState.length === 0 ? null : (
269
+ <div key={state} className="sc-goal-tasks">
270
+ <h3 className="sc-goal-tasks-state">
271
+ {words.vocabulary.state[state]}{" "}
272
+ <span className="sc-column-count">{inState.length}</span>
273
+ </h3>
274
+ <TaskList tasks={inState} taskHref={props.taskHref} today={props.today} />
275
+ </div>
276
+ );
277
+ })
278
+ );
279
+ const shown =
280
+ view === "board" && props.board !== undefined
281
+ ? props.board
282
+ : view === "calendar" && props.calendar !== undefined
283
+ ? props.calendar
284
+ : list;
231
285
  return (
232
- <div className="sc-task sc-goal-view">
233
- <header className="sc-task-header">
234
- <h1 className="sc-task-title">{goal.title}</h1>
235
- <div className="sc-task-badges">
236
- {goal.category !== null && <CategoryBadge category={goal.category} />}
237
- <GoalStateBadge state={goal.state} />
238
- {goal.dueOn !== null && (
239
- <DueBadge dueOn={goal.dueOn} state={dueState(goal)} today={props.today} />
240
- )}
286
+ <div className="sc-goal-page">
287
+ <header className="sc-goal-header">
288
+ <div className="sc-task-header">
289
+ <h1 className="sc-task-title">{goal.title}</h1>
290
+ <div className="sc-task-badges">
291
+ {goal.category !== null && <CategoryBadge category={goal.category} />}
292
+ <GoalStateBadge state={goal.state} />
293
+ {goal.dueOn !== null && (
294
+ <DueBadge dueOn={goal.dueOn} state={dueState(goal)} today={props.today} />
295
+ )}
296
+ </div>
241
297
  </div>
242
- </header>
243
- <div className="sc-task-body">
244
- <div className="sc-task-main">
298
+ <div className="sc-goal-progress">
245
299
  <ProgressBar counts={goal.taskCounts} />
246
- {goal.body.length > 0 ? (
247
- <Markdown source={goal.body} docHref={props.docHref} />
248
- ) : (
249
- <p className="sc-muted">{roadmap.nothingMore}</p>
250
- )}
251
- <section className="sc-task-section" aria-label={roadmap.tasks}>
252
- <h2 className="sc-section-title">{roadmap.tasks}</h2>
253
- {towards.length === 0 ? (
254
- <p className="sc-muted">{roadmap.noTasks}</p>
255
- ) : (
256
- TASK_STATES.map((state) => {
257
- const inState = towards.filter((task) => task.state === state);
258
- return inState.length === 0 ? null : (
259
- <div key={state} className="sc-goal-tasks">
260
- <h3 className="sc-goal-tasks-state">
261
- {words.vocabulary.state[state]}{" "}
262
- <span className="sc-column-count">{inState.length}</span>
263
- </h3>
264
- <TaskList tasks={inState} taskHref={props.taskHref} today={props.today} />
265
- </div>
266
- );
267
- })
268
- )}
269
- </section>
300
+ <span className="sc-muted">{roadmap.progress(done, total)}</span>
270
301
  </div>
271
- <aside className="sc-task-aside" aria-label={words.task.facts}>
272
- <dl className="sc-facts">
273
- <div>
274
- <dt>{roadmap.state}</dt>
275
- <dd>
276
- {props.onMove === undefined ? (
277
- words.vocabulary.goalState[goal.state]
278
- ) : (
279
- <select
280
- className="sc-input"
281
- aria-label={roadmap.state}
282
- value={goal.state}
283
- disabled={props.busy === true}
284
- onChange={(event) => props.onMove?.(goal, event.target.value as GoalState)}
285
- >
286
- {GOAL_STATES.map((state) => (
287
- <option key={state} value={state}>
288
- {words.vocabulary.goalState[state]}
289
- </option>
290
- ))}
291
- </select>
292
- )}
293
- </dd>
294
- </div>
295
- <div>
296
- <dt>{roadmap.category}</dt>
297
- <dd>
298
- {goal.category === null ? (
299
- <span className="sc-muted">{roadmap.noCategory}</span>
300
- ) : (
301
- <CategoryBadge category={goal.category} />
302
- )}
303
- </dd>
304
- </div>
305
- <div>
306
- <dt>{roadmap.starts}</dt>
307
- <dd>
308
- {goal.startsOn === null ? (
309
- <span className="sc-muted">{roadmap.noStart}</span>
310
- ) : (
311
- <Day day={goal.startsOn} />
312
- )}
313
- </dd>
314
- </div>
315
- <div>
316
- <dt>{roadmap.target}</dt>
317
- <dd>
318
- {goal.dueOn === null ? (
319
- <span className="sc-muted">{roadmap.noTarget}</span>
320
- ) : (
321
- <Day day={goal.dueOn} />
322
- )}
323
- </dd>
324
- </div>
325
- <div>
326
- <dt>{roadmap.setBy}</dt>
327
- <dd>
328
- <PersonChip person={goal.createdBy} /> <Time iso={goal.createdAt} />
329
- </dd>
330
- </div>
331
- </dl>
302
+ <dl className="sc-facts-row">
303
+ <div>
304
+ <dt>{roadmap.state}</dt>
305
+ <dd>
306
+ {props.onMove === undefined ? (
307
+ words.vocabulary.goalState[goal.state]
308
+ ) : (
309
+ <select
310
+ className="sc-input"
311
+ aria-label={roadmap.state}
312
+ value={goal.state}
313
+ disabled={props.busy === true}
314
+ onChange={(event) => props.onMove?.(goal, event.target.value as GoalState)}
315
+ >
316
+ {GOAL_STATES.map((state) => (
317
+ <option key={state} value={state}>
318
+ {words.vocabulary.goalState[state]}
319
+ </option>
320
+ ))}
321
+ </select>
322
+ )}
323
+ </dd>
324
+ </div>
325
+ <div>
326
+ <dt>{roadmap.category}</dt>
327
+ <dd>
328
+ {goal.category === null ? (
329
+ <span className="sc-muted">{roadmap.noCategory}</span>
330
+ ) : (
331
+ <CategoryBadge category={goal.category} />
332
+ )}
333
+ </dd>
334
+ </div>
335
+ <div>
336
+ <dt>{roadmap.starts}</dt>
337
+ <dd>
338
+ {goal.startsOn === null ? (
339
+ <span className="sc-muted">{roadmap.noStart}</span>
340
+ ) : (
341
+ <Day day={goal.startsOn} />
342
+ )}
343
+ </dd>
344
+ </div>
345
+ <div>
346
+ <dt>{roadmap.target}</dt>
347
+ <dd>
348
+ {goal.dueOn === null ? (
349
+ <span className="sc-muted">{roadmap.noTarget}</span>
350
+ ) : (
351
+ <Day day={goal.dueOn} />
352
+ )}
353
+ </dd>
354
+ </div>
355
+ <div>
356
+ <dt>{roadmap.setBy}</dt>
357
+ <dd>
358
+ <PersonChip person={goal.createdBy} /> <Time iso={goal.createdAt} />
359
+ </dd>
360
+ </div>
332
361
  {props.editHref !== undefined && (
333
- <div className="sc-task-actions">
334
- <a className="sc-button sc-button-secondary" href={props.editHref}>
362
+ <div>
363
+ <a className="sc-button sc-button-secondary sc-button-sm" href={props.editHref}>
335
364
  {words.common.edit}
336
365
  </a>
337
366
  </div>
338
367
  )}
339
- </aside>
340
- </div>
368
+ {props.onArchiveTasks !== undefined && (
369
+ <div>
370
+ <Button
371
+ variant="secondary"
372
+ size="sm"
373
+ disabled={props.busy === true}
374
+ onClick={props.onArchiveTasks}
375
+ >
376
+ {roadmap.archiveTasks}
377
+ </Button>
378
+ </div>
379
+ )}
380
+ </dl>
381
+ {goal.body.length > 0 && (
382
+ <details className="sc-goal-about">
383
+ <summary className="sc-goal-about-summary">{roadmap.about}</summary>
384
+ <Markdown source={goal.body} docHref={props.docHref} />
385
+ </details>
386
+ )}
387
+ </header>
388
+ {viewHref !== undefined && (
389
+ <nav className="sc-views" aria-label={roadmap.view}>
390
+ {GOAL_TASK_VIEWS.map((candidate) => (
391
+ <a
392
+ key={candidate}
393
+ className={cx("sc-view", candidate === view && "sc-view-current")}
394
+ href={viewHref(candidate)}
395
+ aria-current={candidate === view ? "page" : undefined}
396
+ >
397
+ {roadmap.views[candidate]}
398
+ </a>
399
+ ))}
400
+ </nav>
401
+ )}
402
+ <section
403
+ className={cx("sc-goal-tasks-view", `sc-goal-view-${view}`)}
404
+ aria-label={roadmap.tasks}
405
+ >
406
+ {shown}
407
+ </section>
341
408
  </div>
342
409
  );
343
410
  }
@@ -1,6 +1,7 @@
1
1
  import ReactMarkdown from "react-markdown";
2
2
  import remarkGfm from "remark-gfm";
3
3
  import { isDocumentPath } from "../documents.js";
4
+ import { type Picture, PictureThumb } from "./pictures.js";
4
5
 
5
6
  // What people and agents write is untrusted. It is rendered as React elements, never as HTML:
6
7
  // raw HTML in the source stays text, links are limited to a few schemes and open elsewhere,
@@ -20,6 +21,11 @@ export interface MarkdownProps {
20
21
  * is neither one of those nor over https stays text.
21
22
  */
22
23
  readonly fileSrc?: ((source: string) => string | undefined) | undefined;
24
+ /**
25
+ * Called when a picture that came with the text is pressed, to show it large over the page;
26
+ * left out, such a picture is only seen where it is.
27
+ */
28
+ readonly onOpenPicture?: ((picture: Picture, opener: HTMLElement) => void) | undefined;
23
29
  }
24
30
 
25
31
  export function Markdown(props: MarkdownProps) {
@@ -60,6 +66,19 @@ export function Markdown(props: MarkdownProps) {
60
66
  if (url === undefined) {
61
67
  return <span className="sc-prose-image-text">{alt ?? ""}</span>;
62
68
  }
69
+ if (
70
+ named !== undefined &&
71
+ props.onOpenPicture !== undefined &&
72
+ typeof src === "string"
73
+ ) {
74
+ const name = src.startsWith("./") ? src.slice(2) : src;
75
+ return (
76
+ <PictureThumb
77
+ picture={{ src: named, alt: alt ?? "", name }}
78
+ onOpen={props.onOpenPicture}
79
+ />
80
+ );
81
+ }
63
82
  return (
64
83
  <img
65
84
  src={url}