@skillcdn/console 0.1.17 → 0.1.18

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 (100) hide show
  1. package/README.md +3 -3
  2. package/dist/api.d.ts +2 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +1 -0
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +10 -0
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +145 -14
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/format.d.ts +1 -1
  11. package/dist/cli/format.d.ts.map +1 -1
  12. package/dist/cli/format.js +8 -3
  13. package/dist/cli/format.js.map +1 -1
  14. package/dist/cli/help.d.ts.map +1 -1
  15. package/dist/cli/help.js +7 -5
  16. package/dist/cli/help.js.map +1 -1
  17. package/dist/cli/install.d.ts +21 -0
  18. package/dist/cli/install.d.ts.map +1 -0
  19. package/dist/cli/install.js +87 -0
  20. package/dist/cli/install.js.map +1 -0
  21. package/dist/cli/main.js +22 -3
  22. package/dist/cli/main.js.map +1 -1
  23. package/dist/cli/notices.d.ts +13 -0
  24. package/dist/cli/notices.d.ts.map +1 -0
  25. package/dist/cli/notices.js +31 -0
  26. package/dist/cli/notices.js.map +1 -0
  27. package/dist/client.d.ts +15 -2
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +8 -3
  30. package/dist/client.js.map +1 -1
  31. package/dist/compat.d.ts +66 -0
  32. package/dist/compat.d.ts.map +1 -0
  33. package/dist/compat.js +115 -0
  34. package/dist/compat.js.map +1 -0
  35. package/dist/components/calendar.d.ts +3 -1
  36. package/dist/components/calendar.d.ts.map +1 -1
  37. package/dist/components/calendar.js +21 -2
  38. package/dist/components/calendar.js.map +1 -1
  39. package/dist/components/goals.d.ts +3 -1
  40. package/dist/components/goals.d.ts.map +1 -1
  41. package/dist/components/goals.js +23 -11
  42. package/dist/components/goals.js.map +1 -1
  43. package/dist/components/roadmap.d.ts +37 -0
  44. package/dist/components/roadmap.d.ts.map +1 -0
  45. package/dist/components/roadmap.js +76 -0
  46. package/dist/components/roadmap.js.map +1 -0
  47. package/dist/components/tokens.d.ts +2 -0
  48. package/dist/components/tokens.d.ts.map +1 -1
  49. package/dist/components/tokens.js +13 -3
  50. package/dist/components/tokens.js.map +1 -1
  51. package/dist/console.css +204 -0
  52. package/dist/console.d.ts.map +1 -1
  53. package/dist/console.js +8 -7
  54. package/dist/console.js.map +1 -1
  55. package/dist/data.js +1 -1
  56. package/dist/data.js.map +1 -1
  57. package/dist/i18n/en.d.ts +24 -0
  58. package/dist/i18n/en.d.ts.map +1 -1
  59. package/dist/i18n/en.js +24 -0
  60. package/dist/i18n/en.js.map +1 -1
  61. package/dist/i18n/ko.d.ts.map +1 -1
  62. package/dist/i18n/ko.js +24 -0
  63. package/dist/i18n/ko.js.map +1 -1
  64. package/dist/index.d.ts +2 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/router.d.ts +6 -1
  69. package/dist/router.d.ts.map +1 -1
  70. package/dist/router.js +10 -1
  71. package/dist/router.js.map +1 -1
  72. package/dist/schemas.d.ts +42 -0
  73. package/dist/schemas.d.ts.map +1 -1
  74. package/dist/schemas.js +18 -0
  75. package/dist/schemas.js.map +1 -1
  76. package/docs/specs/cli.md +9 -6
  77. package/docs/specs/rest.md +9 -5
  78. package/package.json +3 -2
  79. package/skills/working-the-board/SKILL.md +4 -3
  80. package/src/api.ts +23 -0
  81. package/src/cli/cli.ts +178 -15
  82. package/src/cli/format.ts +8 -3
  83. package/src/cli/help.ts +7 -5
  84. package/src/cli/install.ts +111 -0
  85. package/src/cli/main.ts +23 -3
  86. package/src/cli/notices.ts +48 -0
  87. package/src/client.ts +31 -6
  88. package/src/compat.ts +173 -0
  89. package/src/components/calendar.tsx +41 -1
  90. package/src/components/goals.tsx +86 -7
  91. package/src/components/roadmap.tsx +242 -0
  92. package/src/components/tokens.tsx +38 -1
  93. package/src/console.tsx +19 -0
  94. package/src/data.ts +1 -1
  95. package/src/i18n/en.ts +24 -0
  96. package/src/i18n/ko.ts +24 -0
  97. package/src/index.ts +8 -0
  98. package/src/router.ts +13 -2
  99. package/src/schemas.ts +20 -0
  100. package/src/styles/console.css +204 -0
@@ -0,0 +1,242 @@
1
+ import type { RestCategory, RestGoal } from "../api.js";
2
+ import { addMonths, daysBetween, formatMonth, monthOf, today as todayOf } from "../dates.js";
3
+ import { useLanguage, useWords } from "../i18n/index.js";
4
+ import { cx, GoalStateBadge } from "./ui.js";
5
+
6
+ // The roadmap as months (ADR-0026): the months as columns, and each goal planned in days a bar
7
+ // from its start to its target in the colour of its category, filled as far as its tasks are
8
+ // done, with today a line down the months. A goal with one day alone sits on it as a point; one
9
+ // with no day is named below the months, since a plan without days is on none of them. Takes
10
+ // its data as props and nothing from the network; the months shown are the address's (ADR-0010).
11
+
12
+ /** How many months the roadmap shows at once when the address names no other span. */
13
+ export const DEFAULT_TIMELINE_MONTHS = 6;
14
+
15
+ export interface TimelineBar {
16
+ readonly goal: RestGoal;
17
+ /** Where the bar begins and ends, as shares of the whole span, 0 to 1. */
18
+ readonly from: number;
19
+ readonly to: number;
20
+ /** Whether the goal begins within the span, and whether it ends within it. */
21
+ readonly begins: boolean;
22
+ readonly ends: boolean;
23
+ /** Whether the goal has one day only: a point on it, not a bar. */
24
+ readonly point: boolean;
25
+ }
26
+
27
+ /** The first day of a month, as a day. */
28
+ const firstDayOf = (month: string): string => `${month}-01`;
29
+
30
+ /**
31
+ * The bars of the goals that touch the span of `months` from `from` on, cut to it: from the
32
+ * goal's start, or the span's first day when it began before, to its target day whole, or the
33
+ * span's end when it ends after. A goal with a start or a target alone sits on that day; a goal
34
+ * with neither is left out.
35
+ */
36
+ export function timelineBars(
37
+ goals: readonly RestGoal[],
38
+ from: string,
39
+ months: number,
40
+ ): TimelineBar[] {
41
+ const first = firstDayOf(from);
42
+ const end = firstDayOf(addMonths(from, months));
43
+ const total = daysBetween(first, end);
44
+ if (total <= 0) {
45
+ return [];
46
+ }
47
+ return goals.flatMap((goal) => {
48
+ const startsOn = goal.startsOn ?? goal.dueOn;
49
+ const dueOn = goal.dueOn ?? goal.startsOn;
50
+ if (startsOn === null || dueOn === null || dueOn < first || startsOn >= end) {
51
+ return [];
52
+ }
53
+ const begins = startsOn >= first;
54
+ const ends = dueOn < end;
55
+ const fromDay = begins ? daysBetween(first, startsOn) : 0;
56
+ const toDay = ends ? daysBetween(first, dueOn) + 1 : total;
57
+ return [
58
+ {
59
+ goal,
60
+ from: fromDay / total,
61
+ to: toDay / total,
62
+ begins,
63
+ ends,
64
+ point: startsOn === dueOn,
65
+ },
66
+ ];
67
+ });
68
+ }
69
+
70
+ export interface GoalTimelineProps {
71
+ readonly goals: readonly RestGoal[];
72
+ /** The first month shown: `2026-10`. */
73
+ readonly from: string;
74
+ /** How many months are shown; `DEFAULT_TIMELINE_MONTHS` when left out. */
75
+ readonly months?: number | undefined;
76
+ /** The day to count from; the browser's today when left out. */
77
+ readonly today?: string | undefined;
78
+ /** The workspace's categories, for the legend to say what the colours mean. */
79
+ readonly categories?: readonly RestCategory[] | undefined;
80
+ readonly goalHref: (goal: RestGoal) => string;
81
+ /** Where the roadmap is from another month on. */
82
+ readonly fromHref: (month: string) => string;
83
+ }
84
+
85
+ const percent = (share: number): string => `${(share * 100).toFixed(3)}%`;
86
+
87
+ export function GoalTimeline(props: GoalTimelineProps) {
88
+ const words = useWords().roadmap;
89
+ const { tag } = useLanguage();
90
+ const months = Math.max(1, props.months ?? DEFAULT_TIMELINE_MONTHS);
91
+ const now = props.today ?? todayOf();
92
+ const first = firstDayOf(props.from);
93
+ const end = firstDayOf(addMonths(props.from, months));
94
+ const total = daysBetween(first, end);
95
+ const columns = Array.from({ length: months }, (_, index) => addMonths(props.from, index));
96
+ const shares = columns.map(
97
+ (month) => daysBetween(firstDayOf(month), firstDayOf(addMonths(month, 1))) / total,
98
+ );
99
+ const bars = timelineBars(props.goals, props.from, months);
100
+ const todayAt = now >= first && now < end ? daysBetween(first, now) / total : undefined;
101
+ const undated = props.goals.filter((goal) => goal.startsOn === null && goal.dueOn === null);
102
+ const doneShare = (goal: RestGoal): number => {
103
+ const counts = goal.taskCounts;
104
+ const all = counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
105
+ return all === 0 ? 0 : counts.done / all;
106
+ };
107
+ return (
108
+ <div className="sc-timeline">
109
+ <div className="sc-timeline-head">
110
+ <h2 className="sc-timeline-title">{words.timeline.title}</h2>
111
+ <nav className="sc-calendar-nav" aria-label={words.timeline.title}>
112
+ <a
113
+ className="sc-button sc-button-ghost sc-button-sm"
114
+ href={props.fromHref(addMonths(props.from, -1))}
115
+ aria-label={words.timeline.earlier}
116
+ >
117
+ {"<"}
118
+ </a>
119
+ <a className="sc-button sc-button-ghost sc-button-sm" href={props.fromHref(monthOf(now))}>
120
+ {words.timeline.thisMonth}
121
+ </a>
122
+ <a
123
+ className="sc-button sc-button-ghost sc-button-sm"
124
+ href={props.fromHref(addMonths(props.from, 1))}
125
+ aria-label={words.timeline.later}
126
+ >
127
+ {">"}
128
+ </a>
129
+ </nav>
130
+ </div>
131
+ <div className="sc-timeline-grid">
132
+ <div className="sc-timeline-row sc-timeline-months">
133
+ <div className="sc-timeline-label" aria-hidden="true" />
134
+ <div className="sc-timeline-track">
135
+ {columns.map((month, index) => (
136
+ <span
137
+ key={month}
138
+ className="sc-timeline-month"
139
+ style={{ width: percent(shares[index] ?? 0) }}
140
+ >
141
+ {formatMonth(month, tag)}
142
+ </span>
143
+ ))}
144
+ {todayAt !== undefined && (
145
+ <span
146
+ className="sc-timeline-today sc-timeline-today-mark"
147
+ style={{ left: percent(todayAt) }}
148
+ title={words.timeline.today}
149
+ />
150
+ )}
151
+ </div>
152
+ </div>
153
+ {bars.map(({ goal, from, to, begins, ends, point }) => {
154
+ const counts = goal.taskCounts;
155
+ const all =
156
+ counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
157
+ return (
158
+ <div key={goal.id} className={cx("sc-timeline-row", `sc-goal-${goal.state}`)}>
159
+ <div className="sc-timeline-label">
160
+ <a className="sc-timeline-goal" href={props.goalHref(goal)}>
161
+ {goal.title}
162
+ </a>
163
+ <GoalStateBadge state={goal.state} />
164
+ </div>
165
+ <div className="sc-timeline-track">
166
+ {columns.map((month, index) => (
167
+ <span
168
+ key={month}
169
+ className="sc-timeline-column"
170
+ style={{ width: percent(shares[index] ?? 0) }}
171
+ aria-hidden="true"
172
+ />
173
+ ))}
174
+ {todayAt !== undefined && (
175
+ <span
176
+ className="sc-timeline-today"
177
+ style={{ left: percent(todayAt) }}
178
+ aria-hidden="true"
179
+ />
180
+ )}
181
+ <a
182
+ className={cx(
183
+ point ? "sc-timeline-point" : "sc-timeline-bar",
184
+ `sc-category-${goal.category?.color ?? "gray"}`,
185
+ begins && "sc-timeline-bar-begins",
186
+ ends && "sc-timeline-bar-ends",
187
+ )}
188
+ style={
189
+ point
190
+ ? { left: percent(from) }
191
+ : { left: percent(from), width: percent(to - from) }
192
+ }
193
+ href={props.goalHref(goal)}
194
+ aria-label={`${goal.title}: ${words.progress(counts.done, all)}`}
195
+ >
196
+ {!point && (
197
+ <>
198
+ <span
199
+ className="sc-timeline-fill"
200
+ style={{ width: percent(doneShare(goal)) }}
201
+ aria-hidden="true"
202
+ />
203
+ <span className="sc-timeline-words">
204
+ {counts.done}/{all}
205
+ </span>
206
+ </>
207
+ )}
208
+ </a>
209
+ </div>
210
+ </div>
211
+ );
212
+ })}
213
+ </div>
214
+ {bars.length === 0 && <p className="sc-muted">{words.timeline.nothing}</p>}
215
+ {(props.categories ?? []).length > 0 && (
216
+ <dl className="sc-calendar-legend" aria-label={words.timeline.legend}>
217
+ {(props.categories ?? []).map((category) => (
218
+ <div key={category.key}>
219
+ <dt>
220
+ <span className={cx("sc-calendar-dot", `sc-category-${category.color}`)} />
221
+ </dt>
222
+ <dd>{category.name}</dd>
223
+ </div>
224
+ ))}
225
+ </dl>
226
+ )}
227
+ {undated.length > 0 && (
228
+ <p className="sc-timeline-undated">
229
+ <span className="sc-muted">{words.timeline.noDays}: </span>
230
+ {undated.map((goal, index) => (
231
+ <span key={goal.id}>
232
+ {index > 0 && ", "}
233
+ <a className="sc-timeline-goal" href={props.goalHref(goal)}>
234
+ {goal.title}
235
+ </a>
236
+ </span>
237
+ ))}
238
+ </p>
239
+ )}
240
+ </div>
241
+ );
242
+ }
@@ -1,12 +1,15 @@
1
1
  import { type FormEvent, type ReactNode, useState } from "react";
2
2
  import {
3
+ type ClientVerdict,
3
4
  DEFAULT_TOKEN_DAYS,
5
+ judgeClient,
4
6
  MAX_TOKEN_NAME_LENGTH,
7
+ parseVersion,
5
8
  type RestToken,
6
9
  type RestTokenInput,
7
10
  } from "../api.js";
8
11
  import { useWords } from "../i18n/index.js";
9
- import { Button, Callout, CopyButton, Time } from "./ui.js";
12
+ import { Badge, Button, Callout, CopyButton, Time } from "./ui.js";
10
13
 
11
14
  // The tokens a person holds for their agents, scripts and consoles of their own, shown as the
12
15
  // agents that hold them: the list, the way to make one by hand, and the one just made, whose
@@ -36,6 +39,22 @@ export interface TokenListProps {
36
39
  readonly onRevoke?: ((token: RestToken) => void) | undefined;
37
40
  readonly busy?: boolean | undefined;
38
41
  readonly empty?: ReactNode;
42
+ /** The console's version (ADR-0025), so that the version of the command an agent runs is said to be behind or ahead; left out, the version alone is shown. */
43
+ readonly consoleVersion?: string | undefined;
44
+ }
45
+
46
+ /** What a command's version is to the console's: nothing to say when they go together, else which way they differ. */
47
+ function verdictOf(
48
+ client: string,
49
+ consoleVersion: string | undefined,
50
+ ): Exclude<ClientVerdict, "current"> | undefined {
51
+ const command = parseVersion(client);
52
+ const server = parseVersion(consoleVersion);
53
+ if (command === undefined || server === undefined) {
54
+ return undefined;
55
+ }
56
+ const { verdict } = judgeClient(command, server);
57
+ return verdict === "current" ? undefined : verdict;
39
58
  }
40
59
 
41
60
  export function TokenList(props: TokenListProps) {
@@ -53,6 +72,24 @@ export function TokenList(props: TokenListProps) {
53
72
  <span>
54
73
  {words.connected} <Time iso={token.createdAt} />
55
74
  </span>
75
+ {token.client !== null && (
76
+ <span>
77
+ {words.command(token.client)}
78
+ {((verdict) =>
79
+ verdict === undefined ? null : (
80
+ <>
81
+ {" "}
82
+ <Badge
83
+ tone={
84
+ verdict === "outdated" || verdict === "too_new" ? "danger" : "warning"
85
+ }
86
+ >
87
+ {words.commandVerdict[verdict]}
88
+ </Badge>
89
+ </>
90
+ ))(verdictOf(token.client, props.consoleVersion))}
91
+ </span>
92
+ )}
56
93
  <span>
57
94
  {token.expiresAt === null ? (
58
95
  words.doesNotExpire
package/src/console.tsx CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  type ProjectListProps,
54
54
  ProjectMark,
55
55
  } from "./components/projects.js";
56
+ import { GoalTimeline } from "./components/roadmap.js";
56
57
  import { RulesForm, RulesView } from "./components/rules.js";
57
58
  import {
58
59
  type Brand,
@@ -106,6 +107,7 @@ import {
106
107
  projectHref,
107
108
  projectSettingsHref,
108
109
  type Route,
110
+ roadmapHref,
109
111
  settingsHref,
110
112
  signInFailureOf,
111
113
  taskHref,
@@ -702,6 +704,7 @@ function ProjectPage(props: {
702
704
  categories={data.categories}
703
705
  goals={data.goals}
704
706
  taskHref={href}
707
+ goalHref={(goal) => goalHref(key, goal.id)}
705
708
  monthHref={(wanted) => calendarHref(key, wanted, route.filter)}
706
709
  />
707
710
  <div className="sc-calendar-below">
@@ -765,6 +768,7 @@ function ProjectPage(props: {
765
768
  {making && (
766
769
  <section className="sc-panel sc-panel-form" aria-label={roadmap.setGoalAria}>
767
770
  <GoalForm
771
+ categories={data.categories}
768
772
  busy={busy}
769
773
  onSubmit={(input) =>
770
774
  void act(async () => {
@@ -776,6 +780,18 @@ function ProjectPage(props: {
776
780
  />
777
781
  </section>
778
782
  )}
783
+ {data.goals.length > 0 && (
784
+ <GoalTimeline
785
+ goals={data.goals}
786
+ from={
787
+ route.name === "roadmap" && route.from !== undefined ? route.from : monthOf(today())
788
+ }
789
+ today={today()}
790
+ categories={data.categories}
791
+ goalHref={(goal) => goalHref(key, goal.id)}
792
+ fromHref={(month) => roadmapHref(key, month)}
793
+ />
794
+ )}
779
795
  <components.GoalList
780
796
  goals={data.goals}
781
797
  goalHref={(goal) => goalHref(key, goal.id)}
@@ -820,6 +836,7 @@ function ProjectPage(props: {
820
836
  <section className="sc-panel sc-panel-form" aria-label={roadmap.editAria}>
821
837
  <GoalForm
822
838
  goal={goal}
839
+ categories={data.categories}
823
840
  busy={busy}
824
841
  onSubmit={(input) =>
825
842
  void act(async () => {
@@ -1451,6 +1468,7 @@ function PersonAgentsPage(props: {
1451
1468
  <components.TokenList
1452
1469
  tokens={tokens}
1453
1470
  busy={busy}
1471
+ consoleVersion={data.me?.console?.version}
1454
1472
  onRevoke={(token) =>
1455
1473
  void act(async () => {
1456
1474
  await data.actions.revokePersonToken(id, token.id);
@@ -1763,6 +1781,7 @@ function WorkspacePage(props: {
1763
1781
  <components.TokenList
1764
1782
  tokens={data.tokens}
1765
1783
  busy={busy}
1784
+ consoleVersion={data.me?.console?.version}
1766
1785
  onRevoke={(token) => void act(() => data.actions.revokeToken(token.id))}
1767
1786
  empty={<EmptyState title={agents.empty.title} body={agents.empty.body} />}
1768
1787
  />
package/src/data.ts CHANGED
@@ -267,7 +267,7 @@ export function useConsoleData(client: ConsoleClient, projectKey: string | undef
267
267
  scope.get(signal),
268
268
  scope.tasks(undefined, signal),
269
269
  scope.decisions(undefined, signal),
270
- scope.goals(signal),
270
+ scope.goals({}, signal),
271
271
  scope.runs(undefined, signal),
272
272
  scope.members(signal),
273
273
  ]);
package/src/i18n/en.ts CHANGED
@@ -385,16 +385,33 @@ export const en = {
385
385
  inProgress: (n: number) => `${n} in progress`,
386
386
  noTasks: "No task works towards it yet.",
387
387
  tasks: "Tasks",
388
+ category: "Category",
389
+ noCategory: "No category",
390
+ starts: "Starts",
391
+ noStart: "No start day",
388
392
  target: "Target",
389
393
  noTarget: "No target day",
390
394
  setBy: "Set by",
391
395
  state: "State",
392
396
  nothingMore: "Nothing more was written about it.",
397
+ timeline: {
398
+ title: "The months",
399
+ legend: "Category colours",
400
+ earlier: "A month earlier",
401
+ later: "A month later",
402
+ thisMonth: "This month",
403
+ today: "Today",
404
+ nothing: "No goal is planned in these months.",
405
+ noDays: "No day set",
406
+ },
393
407
  form: {
394
408
  title: "Title",
395
409
  body: "What reaching it means",
396
410
  bodyPlaceholder: "What is true once the goal is reached, in Markdown.",
397
411
  state: "State",
412
+ category: "Category",
413
+ noCategory: "No category",
414
+ startsOn: "Start day",
398
415
  dueOn: "Target day",
399
416
  set: "Set the goal",
400
417
  },
@@ -734,6 +751,13 @@ export const en = {
734
751
  goodUntil: "Good until",
735
752
  notActive: "Not active yet",
736
753
  lastActive: "Last active",
754
+ command: (version: string) => `console ${version}`,
755
+ commandVerdict: {
756
+ behind: "an update is available",
757
+ ahead: "newer than this console",
758
+ outdated: "must be updated",
759
+ too_new: "too new for this console",
760
+ },
737
761
  disconnect: "Disconnect",
738
762
  form: {
739
763
  name: "Name",
package/src/i18n/ko.ts CHANGED
@@ -354,16 +354,33 @@ export const ko: Messages = {
354
354
  inProgress: (n) => `${n}개 진행 중`,
355
355
  noTasks: "아직 이 목표를 향한 작업이 없습니다.",
356
356
  tasks: "작업",
357
+ category: "카테고리",
358
+ noCategory: "카테고리 없음",
359
+ starts: "시작일",
360
+ noStart: "시작일 없음",
357
361
  target: "목표일",
358
362
  noTarget: "목표일 없음",
359
363
  setBy: "세운 사람",
360
364
  state: "상태",
361
365
  nothingMore: "더 적힌 내용이 없습니다.",
366
+ timeline: {
367
+ title: "달별 로드맵",
368
+ legend: "카테고리 색",
369
+ earlier: "한 달 앞으로",
370
+ later: "한 달 뒤로",
371
+ thisMonth: "이번 달",
372
+ today: "오늘",
373
+ nothing: "이 기간에 잡힌 목표가 없습니다.",
374
+ noDays: "날짜 없음",
375
+ },
362
376
  form: {
363
377
  title: "제목",
364
378
  body: "달성의 의미",
365
379
  bodyPlaceholder: "목표를 이루면 무엇이 참이 되는지, Markdown으로.",
366
380
  state: "상태",
381
+ category: "카테고리",
382
+ noCategory: "카테고리 없음",
383
+ startsOn: "시작일",
367
384
  dueOn: "목표일",
368
385
  set: "목표 세우기",
369
386
  },
@@ -698,6 +715,13 @@ export const ko: Messages = {
698
715
  goodUntil: "만료",
699
716
  notActive: "아직 활동 없음",
700
717
  lastActive: "마지막 활동",
718
+ command: (version) => `console ${version}`,
719
+ commandVerdict: {
720
+ behind: "업데이트할 수 있음",
721
+ ahead: "이 콘솔보다 새 버전",
722
+ outdated: "업데이트 필요",
723
+ too_new: "이 콘솔에는 너무 새 버전",
724
+ },
701
725
  disconnect: "연결 끊기",
702
726
  form: {
703
727
  name: "이름",
package/src/index.ts CHANGED
@@ -100,6 +100,13 @@ export {
100
100
  ProjectRoleBadge,
101
101
  VISIBILITY_LABELS,
102
102
  } from "./components/projects.js";
103
+ export {
104
+ DEFAULT_TIMELINE_MONTHS,
105
+ GoalTimeline,
106
+ type GoalTimelineProps,
107
+ type TimelineBar,
108
+ timelineBars,
109
+ } from "./components/roadmap.js";
103
110
  export {
104
111
  RulesForm,
105
112
  type RulesFormProps,
@@ -230,6 +237,7 @@ export {
230
237
  projectHref,
231
238
  projectSettingsHref,
232
239
  type Route,
240
+ roadmapHref,
233
241
  settingsHref,
234
242
  signInFailureOf,
235
243
  type TaskFormKind,
package/src/router.ts CHANGED
@@ -59,7 +59,8 @@ export type Route =
59
59
  readonly month: string | undefined;
60
60
  readonly filter: BoardFilter;
61
61
  }
62
- | { readonly name: "roadmap"; readonly project: string }
62
+ /** The roadmap of a project: the months shown from the one named, or from this one. */
63
+ | { readonly name: "roadmap"; readonly project: string; readonly from: string | undefined }
63
64
  | { readonly name: "new-goal"; readonly project: string }
64
65
  | {
65
66
  readonly name: "goal";
@@ -345,7 +346,11 @@ export function matchRoute(pathname: string, search = ""): Route {
345
346
  return { name: "calendar", project, month: month ?? undefined, filter: boardFilterOf(params) };
346
347
  }
347
348
  if (page === "roadmap") {
348
- return { name: "roadmap", project };
349
+ const from = params.get("from");
350
+ if (from !== null && !isMonth(from)) {
351
+ return NOT_FOUND;
352
+ }
353
+ return { name: "roadmap", project, from: from ?? undefined };
349
354
  }
350
355
  if (page === "settings") {
351
356
  return {
@@ -455,6 +460,12 @@ export function customPageHref(page: string, project?: string): string {
455
460
  }
456
461
 
457
462
  /** Where a goal is set: the form. */
463
+ /** Where a project's roadmap is, from a month on, or from this one when none is named. */
464
+ export function roadmapHref(key: string, from?: string): string {
465
+ const base = projectHref(key, "roadmap");
466
+ return from === undefined ? base : `${base}?from=${from}`;
467
+ }
468
+
458
469
  export function newGoalHref(key: string): string {
459
470
  return `${PATHS.project}/${key}/roadmap/new`;
460
471
  }
package/src/schemas.ts CHANGED
@@ -196,6 +196,13 @@ export const restMeSchema = z.object({
196
196
  /** The language the person chose for the pages (ADR-0012), or `null` for the browser's. */
197
197
  language: z.nullable(languageTag),
198
198
  signIn: z.array(restProviderSchema),
199
+ /**
200
+ * The console's version and the range of command versions it takes, as npm spells one
201
+ * (ADR-0025): what a console of a person's own compares the package it was built on with.
202
+ * Every console answers it from 0.1.18; read as optional, so that a command or a console
203
+ * built on this package still reads an older console's answer, and says it is older.
204
+ */
205
+ console: z.optional(z.object({ version: z.string(), clientRange: z.string() })),
199
206
  });
200
207
  export type RestMe = z.infer<typeof restMeSchema>;
201
208
 
@@ -485,6 +492,12 @@ export const restGoalSchema = z.object({
485
492
  /** Markdown: what reaching the goal means. Shown as text or rendered to elements, never as HTML. */
486
493
  body: z.string(),
487
494
  state: z.enum(GOAL_STATES),
495
+ /** The category the goal is sorted into, as the workspace keeps it, or `null` (ADR-0026). */
496
+ category: z.nullable(
497
+ z.object({ key: z.string(), name: z.string(), color: z.enum(CATEGORY_COLORS) }),
498
+ ),
499
+ /** The plan (ADR-0026): the day the work towards the goal is meant to begin, or `null`. */
500
+ startsOn: z.nullable(day),
488
501
  /** The day the goal is meant to be reached by, or `null`. */
489
502
  dueOn: z.nullable(day),
490
503
  createdBy: restPersonSchema,
@@ -505,6 +518,9 @@ export const restGoalInputSchema = z.object({
505
518
  body: z.optional(body),
506
519
  /** Left out: `planned`. */
507
520
  state: z.optional(z.enum(GOAL_STATES)),
521
+ /** The key of a category of the workspace, or `null` for none. */
522
+ category: z.optional(z.nullable(categoryKey)),
523
+ startsOn: z.optional(z.nullable(day)),
508
524
  dueOn: z.optional(z.nullable(day)),
509
525
  });
510
526
  export type RestGoalInput = z.infer<typeof restGoalInputSchema>;
@@ -514,6 +530,8 @@ export const restGoalPatchSchema = z.object({
514
530
  title: z.optional(line(MAX_TITLE_LENGTH)),
515
531
  body: z.optional(body),
516
532
  state: z.optional(z.enum(GOAL_STATES)),
533
+ category: z.optional(z.nullable(categoryKey)),
534
+ startsOn: z.optional(z.nullable(day)),
517
535
  dueOn: z.optional(z.nullable(day)),
518
536
  });
519
537
  export type RestGoalPatch = z.infer<typeof restGoalPatchSchema>;
@@ -791,6 +809,8 @@ export const restTokenSchema = z.object({
791
809
  expiresAt: z.nullable(instant),
792
810
  /** When it was last presented, or `null` if never. */
793
811
  lastUsedAt: z.nullable(instant),
812
+ /** The version of the command that last presented it (`0.1.18`), or `null` when the last to present it named none. */
813
+ client: z.nullable(z.string()),
794
814
  });
795
815
  export type RestToken = z.infer<typeof restTokenSchema>;
796
816