@skillcdn/console 0.1.2 → 0.1.4

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 (157) hide show
  1. package/README.md +9 -5
  2. package/dist/api.d.ts +7 -5
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +6 -4
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli/cli.d.ts +3 -3
  7. package/dist/cli/cli.d.ts.map +1 -1
  8. package/dist/cli/cli.js +202 -18
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/credentials.d.ts +1 -1
  11. package/dist/cli/format.d.ts +10 -1
  12. package/dist/cli/format.d.ts.map +1 -1
  13. package/dist/cli/format.js +67 -0
  14. package/dist/cli/format.js.map +1 -1
  15. package/dist/cli/help.d.ts.map +1 -1
  16. package/dist/cli/help.js +66 -7
  17. package/dist/cli/help.js.map +1 -1
  18. package/dist/cli/main.js +3 -2
  19. package/dist/cli/main.js.map +1 -1
  20. package/dist/cli/terminal.d.ts +0 -5
  21. package/dist/cli/terminal.d.ts.map +1 -1
  22. package/dist/cli/terminal.js +0 -43
  23. package/dist/cli/terminal.js.map +1 -1
  24. package/dist/client.d.ts +51 -2
  25. package/dist/client.d.ts.map +1 -1
  26. package/dist/client.js +36 -11
  27. package/dist/client.js.map +1 -1
  28. package/dist/components/board.d.ts.map +1 -1
  29. package/dist/components/board.js +6 -3
  30. package/dist/components/board.js.map +1 -1
  31. package/dist/components/connect.d.ts +28 -0
  32. package/dist/components/connect.d.ts.map +1 -0
  33. package/dist/components/connect.js +30 -0
  34. package/dist/components/connect.js.map +1 -0
  35. package/dist/components/decision-form.d.ts.map +1 -1
  36. package/dist/components/decision-form.js +4 -1
  37. package/dist/components/decision-form.js.map +1 -1
  38. package/dist/components/decision-list.d.ts +11 -1
  39. package/dist/components/decision-list.d.ts.map +1 -1
  40. package/dist/components/decision-list.js +30 -5
  41. package/dist/components/decision-list.js.map +1 -1
  42. package/dist/components/documents.d.ts +87 -0
  43. package/dist/components/documents.d.ts.map +1 -0
  44. package/dist/components/documents.js +115 -0
  45. package/dist/components/documents.js.map +1 -0
  46. package/dist/components/event-feed.d.ts +3 -2
  47. package/dist/components/event-feed.d.ts.map +1 -1
  48. package/dist/components/event-feed.js +55 -45
  49. package/dist/components/event-feed.js.map +1 -1
  50. package/dist/components/markdown.d.ts +5 -2
  51. package/dist/components/markdown.d.ts.map +1 -1
  52. package/dist/components/markdown.js +8 -1
  53. package/dist/components/markdown.js.map +1 -1
  54. package/dist/components/people.d.ts +2 -0
  55. package/dist/components/people.d.ts.map +1 -1
  56. package/dist/components/people.js +4 -2
  57. package/dist/components/people.js.map +1 -1
  58. package/dist/components/projects.d.ts +1 -0
  59. package/dist/components/projects.d.ts.map +1 -1
  60. package/dist/components/projects.js +16 -10
  61. package/dist/components/projects.js.map +1 -1
  62. package/dist/components/runs.d.ts +4 -0
  63. package/dist/components/runs.d.ts.map +1 -1
  64. package/dist/components/runs.js +7 -3
  65. package/dist/components/runs.js.map +1 -1
  66. package/dist/components/shell.d.ts +30 -1
  67. package/dist/components/shell.d.ts.map +1 -1
  68. package/dist/components/shell.js +56 -8
  69. package/dist/components/shell.js.map +1 -1
  70. package/dist/components/sign-in.d.ts +3 -0
  71. package/dist/components/sign-in.d.ts.map +1 -1
  72. package/dist/components/sign-in.js +3 -9
  73. package/dist/components/sign-in.js.map +1 -1
  74. package/dist/components/skills.d.ts.map +1 -1
  75. package/dist/components/skills.js +8 -12
  76. package/dist/components/skills.js.map +1 -1
  77. package/dist/components/task-form.d.ts.map +1 -1
  78. package/dist/components/task-form.js +5 -2
  79. package/dist/components/task-form.js.map +1 -1
  80. package/dist/components/task-view.d.ts +12 -1
  81. package/dist/components/task-view.d.ts.map +1 -1
  82. package/dist/components/task-view.js +7 -14
  83. package/dist/components/task-view.js.map +1 -1
  84. package/dist/components/tokens.d.ts +12 -0
  85. package/dist/components/tokens.d.ts.map +1 -1
  86. package/dist/components/tokens.js +25 -10
  87. package/dist/components/tokens.js.map +1 -1
  88. package/dist/components/ui.d.ts +3 -3
  89. package/dist/components/ui.d.ts.map +1 -1
  90. package/dist/components/ui.js +15 -9
  91. package/dist/components/ui.js.map +1 -1
  92. package/dist/connect-page.d.ts +10 -0
  93. package/dist/connect-page.d.ts.map +1 -0
  94. package/dist/connect-page.js +67 -0
  95. package/dist/connect-page.js.map +1 -0
  96. package/dist/connect.d.ts +11 -0
  97. package/dist/connect.d.ts.map +1 -0
  98. package/dist/connect.js +24 -0
  99. package/dist/connect.js.map +1 -0
  100. package/dist/console.css +310 -10
  101. package/dist/console.d.ts +6 -1
  102. package/dist/console.d.ts.map +1 -1
  103. package/dist/console.js +339 -151
  104. package/dist/console.js.map +1 -1
  105. package/dist/data.d.ts +21 -1
  106. package/dist/data.d.ts.map +1 -1
  107. package/dist/data.js +23 -0
  108. package/dist/data.js.map +1 -1
  109. package/dist/docs-page.d.ts +14 -0
  110. package/dist/docs-page.d.ts.map +1 -0
  111. package/dist/docs-page.js +166 -0
  112. package/dist/docs-page.js.map +1 -0
  113. package/dist/documents.d.ts +19 -0
  114. package/dist/documents.d.ts.map +1 -0
  115. package/dist/documents.js +64 -0
  116. package/dist/documents.js.map +1 -0
  117. package/dist/i18n/en.d.ts +471 -0
  118. package/dist/i18n/en.d.ts.map +1 -0
  119. package/dist/i18n/en.js +481 -0
  120. package/dist/i18n/en.js.map +1 -0
  121. package/dist/i18n/index.d.ts +26 -0
  122. package/dist/i18n/index.d.ts.map +1 -0
  123. package/dist/i18n/index.js +46 -0
  124. package/dist/i18n/index.js.map +1 -0
  125. package/dist/i18n/ko.d.ts +3 -0
  126. package/dist/i18n/ko.d.ts.map +1 -0
  127. package/dist/i18n/ko.js +470 -0
  128. package/dist/i18n/ko.js.map +1 -0
  129. package/dist/index.d.ts +9 -3
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +8 -2
  132. package/dist/index.js.map +1 -1
  133. package/dist/limits.d.ts +14 -0
  134. package/dist/limits.d.ts.map +1 -1
  135. package/dist/limits.js +14 -0
  136. package/dist/limits.js.map +1 -1
  137. package/dist/router.d.ts +93 -10
  138. package/dist/router.d.ts.map +1 -1
  139. package/dist/router.js +168 -24
  140. package/dist/router.js.map +1 -1
  141. package/dist/routes.d.ts +11 -1
  142. package/dist/routes.d.ts.map +1 -1
  143. package/dist/routes.js +17 -0
  144. package/dist/routes.js.map +1 -1
  145. package/dist/schemas.d.ts +310 -2
  146. package/dist/schemas.d.ts.map +1 -1
  147. package/dist/schemas.js +162 -11
  148. package/dist/schemas.js.map +1 -1
  149. package/dist/use-action.d.ts +9 -0
  150. package/dist/use-action.d.ts.map +1 -0
  151. package/dist/use-action.js +34 -0
  152. package/dist/use-action.js.map +1 -0
  153. package/dist/vocabulary.d.ts +4 -1
  154. package/dist/vocabulary.d.ts.map +1 -1
  155. package/dist/vocabulary.js +7 -0
  156. package/dist/vocabulary.js.map +1 -1
  157. package/package.json +1 -1
package/dist/console.js CHANGED
@@ -1,23 +1,28 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { StrictMode, useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
- import { ApiError, createClient, } from "./api.js";
4
+ import { createClient, } from "./api.js";
5
5
  import { Board } from "./components/board.js";
6
+ import { ConnectWords } from "./components/connect.js";
6
7
  import { DecisionForm } from "./components/decision-form.js";
7
- import { DecisionList } from "./components/decision-list.js";
8
+ import { DecisionCard, DecisionList } from "./components/decision-list.js";
8
9
  import { EventFeed } from "./components/event-feed.js";
9
10
  import { PeopleList } from "./components/people.js";
10
11
  import { MemberList, ProjectForm, ProjectList, } from "./components/projects.js";
11
- import { Shell } from "./components/shell.js";
12
+ import { isPlainClick, Shell, } from "./components/shell.js";
12
13
  import { SignIn } from "./components/sign-in.js";
13
14
  import { SkillList } from "./components/skills.js";
14
15
  import { TaskForm } from "./components/task-form.js";
15
16
  import { TaskView } from "./components/task-view.js";
16
17
  import { NewToken, TokenForm, TokenList } from "./components/tokens.js";
17
18
  import { Button, Callout, EmptyState, Spinner } from "./components/ui.js";
19
+ import { ConnectPage } from "./connect-page.js";
18
20
  import { useConsoleData } from "./data.js";
19
- import { matchRoute, PATHS, projectHref, signInFailureOf, taskHref, withoutSignInParam, } from "./router.js";
21
+ import { DocsPage } from "./docs-page.js";
22
+ import { chooseLanguage, DEFAULT_LANGUAGES, LanguageContext, useWords, } from "./i18n/index.js";
23
+ import { decisionHref, docHref, matchRoute, newDecisionHref, newTaskHref, PATHS, personAgentsHref, projectHref, signInFailureOf, taskHref, withoutSignInParam, } from "./router.js";
20
24
  import { projectPath } from "./routes.js";
25
+ import { useAction } from "./use-action.js";
21
26
  export const DEFAULT_COMPONENTS = {
22
27
  Shell,
23
28
  Board,
@@ -31,8 +36,21 @@ export const DEFAULT_COMPONENTS = {
31
36
  ProjectList,
32
37
  MemberList,
33
38
  };
34
- /** The browser's location, as the app reads it; a given path while rendering elsewhere. */
35
- function useLocation(initialPath) {
39
+ /** How many steps back from a history entry stay within the app: kept on the entry itself. */
40
+ function depthOf(state) {
41
+ return typeof state === "object" &&
42
+ state !== null &&
43
+ "scDepth" in state &&
44
+ typeof state.scDepth === "number"
45
+ ? state.scDepth
46
+ : 0;
47
+ }
48
+ /**
49
+ * The browser's location as the app reads it, and the ways to move: one history entry per step
50
+ * the person takes, none for the same address again, a replacement for a redirect or for what a
51
+ * form made, and back to where a form was opened from when that is within the app.
52
+ */
53
+ function useNavigation(initialPath) {
36
54
  const read = useCallback(() => typeof window === "undefined"
37
55
  ? { pathname: initialPath ?? "/", search: "" }
38
56
  : { pathname: window.location.pathname, search: window.location.search }, [initialPath]);
@@ -42,69 +60,121 @@ function useLocation(initialPath) {
42
60
  window.addEventListener("popstate", update);
43
61
  return () => window.removeEventListener("popstate", update);
44
62
  }, [read]);
45
- const navigate = useCallback((href, replace = false) => {
46
- if (typeof window === "undefined") {
47
- return;
48
- }
49
- if (replace) {
50
- window.history.replaceState(null, "", href);
51
- }
52
- else {
53
- window.history.pushState(null, "", href);
54
- window.scrollTo({ top: 0, left: 0, behavior: "instant" });
55
- }
56
- setLocation(read());
63
+ const navigation = useMemo(() => {
64
+ const show = (href, replace) => {
65
+ if (typeof window === "undefined") {
66
+ return;
67
+ }
68
+ const { pathname, search, hash } = window.location;
69
+ if (href === `${pathname}${search}${hash}`) {
70
+ return;
71
+ }
72
+ const depth = depthOf(window.history.state);
73
+ if (replace) {
74
+ window.history.replaceState({ scDepth: depth }, "", href);
75
+ }
76
+ else {
77
+ window.history.pushState({ scDepth: depth + 1 }, "", href);
78
+ window.scrollTo({ top: 0, left: 0, behavior: "instant" });
79
+ }
80
+ setLocation(read());
81
+ };
82
+ return {
83
+ go: (href) => show(href, false),
84
+ replace: (href) => show(href, true),
85
+ back: (fallback) => {
86
+ if (typeof window !== "undefined" && depthOf(window.history.state) > 0) {
87
+ window.history.back();
88
+ }
89
+ else {
90
+ show(fallback, true);
91
+ }
92
+ },
93
+ };
57
94
  }, [read]);
58
- return { location, navigate };
59
- }
60
- function errorWords(error) {
61
- if (error instanceof ApiError) {
62
- return error.message;
63
- }
64
- return "Something went wrong. Try again.";
95
+ return { location, navigation };
65
96
  }
66
- /** Runs a change, keeps what went wrong for the page, and says when it is busy. */
67
- function useAction() {
68
- const [busy, setBusy] = useState(false);
69
- const [error, setError] = useState(undefined);
70
- const act = async (work) => {
71
- setBusy(true);
72
- setError(undefined);
73
- try {
74
- await work();
75
- }
76
- catch (failure) {
77
- setError(errorWords(failure));
78
- }
79
- finally {
80
- setBusy(false);
97
+ /**
98
+ * Every plain click on a link to one of the console's own addresses is followed in place, from
99
+ * here, wherever the link is: in a document, in a feed line, in a task. A modified click, a link
100
+ * elsewhere, a file of the API, a sign-in, and a jump within the page stay the browser's.
101
+ */
102
+ function useFollowLinks(navigation) {
103
+ useEffect(() => {
104
+ if (typeof document === "undefined") {
105
+ return;
81
106
  }
82
- };
83
- return { busy, error, act };
107
+ const onClick = (event) => {
108
+ if (event.defaultPrevented || !isPlainClick(event)) {
109
+ return;
110
+ }
111
+ const anchor = event.target instanceof Element ? event.target.closest("a[href]") : null;
112
+ if (!(anchor instanceof HTMLAnchorElement)) {
113
+ return;
114
+ }
115
+ if ((anchor.target !== "" && anchor.target !== "_self") || anchor.hasAttribute("download")) {
116
+ return;
117
+ }
118
+ const url = new URL(anchor.href, window.location.href);
119
+ if (url.origin !== window.location.origin) {
120
+ return;
121
+ }
122
+ const { pathname, search } = window.location;
123
+ if (url.pathname === pathname && url.search === search && url.hash !== "") {
124
+ return;
125
+ }
126
+ if (matchRoute(url.pathname, url.search).name === "not-found") {
127
+ return;
128
+ }
129
+ event.preventDefault();
130
+ navigation.go(`${url.pathname}${url.search}${url.hash}`);
131
+ };
132
+ document.addEventListener("click", onClick);
133
+ return () => document.removeEventListener("click", onClick);
134
+ }, [navigation]);
84
135
  }
85
136
  /** The key of the project a route is on, or nothing. */
86
137
  const projectOf = (route) => "project" in route ? route.project : undefined;
138
+ /** A link that reads as a button: where a form is, for instance. */
139
+ function LinkButton(props) {
140
+ return (_jsx("a", { className: `sc-button sc-button-${props.variant ?? "secondary"}`, href: props.href, children: props.children }));
141
+ }
87
142
  function ProjectsPage(props) {
88
- const { data, components, navigate } = props;
89
- const [making, setMaking] = useState(false);
143
+ const words = useWords().projects;
144
+ const { data, components, navigation, making } = props;
90
145
  const { busy, error, act } = useAction();
91
146
  const href = (project) => projectHref(project.key);
92
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: "Projects" }), _jsx(Button, { variant: "primary", onClick: () => setMaking(true), disabled: making, children: "New project" })] }), _jsx("p", { className: "sc-lead", children: "A project holds its board, its skills and its people. What you may see and change is decided per project: an owner configures it, a member works on it." }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel", "aria-label": "New project", children: _jsx(ProjectForm, { busy: busy, onSubmit: (input) => void act(async () => {
147
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.title }), !making && (_jsx(LinkButton, { href: PATHS.newProject, variant: "primary", children: words.newProject }))] }), _jsx("p", { className: "sc-lead", children: words.lead }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), making && (_jsx("section", { className: "sc-panel", "aria-label": words.form.aria, children: _jsx(ProjectForm, { busy: busy, onSubmit: (input) => void act(async () => {
93
148
  const made = await data.actions.createProject(input);
94
- setMaking(false);
95
- navigate(projectHref(made.key));
96
- }), onCancel: () => setMaking(false) }) })), _jsx(components.ProjectList, { projects: data.projects, projectHref: href, onOpen: (project) => navigate(href(project)), empty: _jsx(EmptyState, { title: "No projects yet", body: "Make the first one: what it is for, and who is in it. Or ask an owner to add you to theirs.", action: _jsx(Button, { onClick: () => setMaking(true), children: "New project" }) }) })] }));
149
+ navigation.replace(projectHref(made.key));
150
+ }), onCancel: () => navigation.back(PATHS.projects) }) })), _jsx(components.ProjectList, { projects: data.projects, projectHref: href, onOpen: (project) => navigation.go(href(project)), empty: _jsx(EmptyState, { title: words.empty.title, body: words.empty.body, action: _jsx(LinkButton, { href: PATHS.newProject, children: words.newProject }) }) })] }));
97
151
  }
98
152
  function TaskPage(props) {
99
- const { project, task, data, components, busy, error, act } = props;
153
+ const words = useWords();
154
+ const { route, project, data, components, navigation, busy, error, act } = props;
155
+ const key = project.key;
156
+ const task = route.number === undefined
157
+ ? data.tasks.find((candidate) => candidate.id === route.id)
158
+ : data.tasks.find((candidate) => candidate.number === route.number);
100
159
  const [history, setHistory] = useState(undefined);
101
160
  const { taskHistory } = data.actions;
161
+ const taskId = task?.id;
162
+ const number = task?.number;
163
+ // A task named by its id is shown at its number's address.
164
+ useEffect(() => {
165
+ if (route.id !== undefined && number !== undefined) {
166
+ navigation.replace(taskHref(key, number, route.form));
167
+ }
168
+ }, [route.id, route.form, number, key, navigation]);
102
169
  // Everything that happened to the task, asked for again whenever the project's feed grows.
103
170
  const feedLength = data.events.length;
104
171
  // biome-ignore lint/correctness/useExhaustiveDependencies: the feed's length is the trigger
105
172
  useEffect(() => {
173
+ if (taskId === undefined) {
174
+ return;
175
+ }
106
176
  const controller = new AbortController();
107
- taskHistory(task.id, controller.signal)
177
+ taskHistory(taskId, controller.signal)
108
178
  .then((events) => {
109
179
  if (!controller.signal.aborted) {
110
180
  setHistory(events);
@@ -112,171 +182,289 @@ function TaskPage(props) {
112
182
  })
113
183
  .catch(() => undefined);
114
184
  return () => controller.abort();
115
- }, [task.id, taskHistory, feedLength]);
116
- return (_jsx(components.TaskView, { task: task, people: data.people, tasks: data.tasks, decisions: data.decisions, runs: data.runs, history: history, taskHref: (candidate) => taskHref(project.key, candidate.id), decisionHref: () => projectHref(project.key, "decisions"), fileHref: (artifact) => projectPath(project.key, "files", artifact.id), busy: busy, error: error, onChange: (target, patch) => void act(() => data.actions.updateTask(target.id, patch)), onMove: (target, state) => void act(() => data.actions.updateTask(target.id, { state })), onRaiseDecision: (input) => void act(() => data.actions.raiseDecision(input)), onAnswer: (decision, input) => void act(() => data.actions.answerDecision(decision.id, input)), onAbandonRun: (run) => void act(() => data.actions.abandonRun(run.id)) }));
185
+ }, [taskId, taskHistory, feedLength]);
186
+ if (task === undefined) {
187
+ return (_jsx(EmptyState, { title: words.task.noSuch.title, body: words.task.noSuch.body, action: _jsx(LinkButton, { href: projectHref(key), children: words.nav.board }) }));
188
+ }
189
+ const here = taskHref(key, task.number);
190
+ return (_jsx(components.TaskView, { task: task, people: data.people, tasks: data.tasks, decisions: data.decisions, runs: data.runs, history: history, taskHref: (candidate) => taskHref(key, candidate.number), decisionHref: (decisionId) => decisionHref(key, decisionId), fileHref: (artifact) => projectPath(key, "files", artifact.id), docHref: (path) => docHref(key, path), form: route.form, formHref: (form) => taskHref(key, task.number, form), onCancel: () => navigation.back(here), busy: busy, error: error, onChange: (target, patch) => void act(async () => {
191
+ await data.actions.updateTask(target.id, patch);
192
+ navigation.replace(here);
193
+ }), onMove: (target, state) => void act(() => data.actions.updateTask(target.id, { state })), onRaiseDecision: (input) => void act(async () => {
194
+ await data.actions.raiseDecision(input);
195
+ navigation.replace(here);
196
+ }), onAnswer: (decision, input) => void act(() => data.actions.answerDecision(decision.id, input)), onUpdateDecision: (decision, patch) => void act(() => data.actions.updateDecision(decision.id, patch)), onAbandonRun: (run) => void act(() => data.actions.abandonRun(run.id)) }));
117
197
  }
118
198
  function ProjectPage(props) {
119
- const { route, project, data, components, navigate } = props;
120
- const [writing, setWriting] = useState(false);
121
- const [asking, setAsking] = useState(false);
199
+ const words = useWords();
200
+ const { route, project, data, components, navigation } = props;
122
201
  const { busy, error, act } = useAction();
123
202
  const tasksById = useMemo(() => new Map(data.tasks.map((task) => [task.id, task])), [data.tasks]);
124
- const href = (task) => taskHref(project.key, task.id);
203
+ const key = project.key;
204
+ const href = (task) => taskHref(key, task.number);
205
+ const toDoc = (path) => docHref(key, path);
125
206
  const owner = project.role === "owner";
126
207
  const onMove = (task, state) => void act(() => data.actions.updateTask(task.id, { state }));
127
208
  const onAnswer = (decision, input) => void act(() => data.actions.answerDecision(decision.id, input));
209
+ const onUpdate = (decision, patch) => void act(() => data.actions.updateDecision(decision.id, patch));
128
210
  if (route.name === "board") {
129
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: project.name }), _jsx(Button, { variant: "primary", onClick: () => setWriting(true), disabled: writing, children: "Write a task" })] }), project.description.length > 0 && _jsx("p", { className: "sc-lead", children: project.description }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), writing && (_jsx("section", { className: "sc-panel", "aria-label": "Write a task", children: _jsx(TaskForm, { people: data.people, parents: data.tasks, busy: busy, onSubmit: (input) => void act(async () => {
130
- await data.actions.createTask(input);
131
- setWriting(false);
132
- }), onCancel: () => setWriting(false) }) })), _jsx(components.Board, { tasks: data.tasks, taskHref: href, onOpen: (task) => navigate(href(task)), onMove: onMove, empty: data.tasks.length === 0 ? (_jsx(EmptyState, { title: "Nothing on the board yet", body: "Write the first task: what is to be done, and for whom.", action: _jsx(Button, { onClick: () => setWriting(true), children: "Write a task" }) })) : undefined })] }));
211
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: project.name }), _jsx(LinkButton, { href: newTaskHref(key), variant: "primary", children: words.board.writeTask })] }), project.description.length > 0 && _jsx("p", { className: "sc-lead", children: project.description }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.Board, { tasks: data.tasks, taskHref: href, onOpen: (task) => navigation.go(href(task)), onMove: onMove, empty: data.tasks.length === 0 ? (_jsx(EmptyState, { title: words.board.empty.title, body: words.board.empty.body, action: _jsx(LinkButton, { href: newTaskHref(key), children: words.board.writeTask }) })) : undefined })] }));
212
+ }
213
+ if (route.name === "new-task") {
214
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.task.newTitle }) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel", "aria-label": words.task.newAria, children: _jsx(TaskForm, { people: data.people, parents: data.tasks, busy: busy, onSubmit: (input) => void act(async () => {
215
+ const made = await data.actions.createTask(input);
216
+ navigation.replace(taskHref(key, made.number));
217
+ }), onCancel: () => navigation.back(projectHref(key)) }) })] }));
133
218
  }
134
219
  if (route.name === "task") {
135
- const task = tasksById.get(route.id);
136
- if (task === undefined) {
137
- return (_jsx(EmptyState, { title: "No such task", body: "It may belong to another project, or the link is wrong." }));
138
- }
139
- return (_jsx(TaskPage, { project: project, task: task, data: data, components: components, busy: busy, error: error, act: act }));
220
+ return (_jsx(TaskPage, { route: route, project: project, data: data, components: components, navigation: navigation, busy: busy, error: error, act: act }));
221
+ }
222
+ if (route.name === "docs") {
223
+ return (_jsx(DocsPage, { project: project, path: route.path, view: route.view, data: data, navigation: navigation }));
140
224
  }
141
225
  if (route.name === "decisions") {
142
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: "Decisions" }), _jsx(Button, { variant: "primary", onClick: () => setAsking(true), disabled: asking, children: "Raise a decision" })] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), asking && (_jsx("section", { className: "sc-panel", "aria-label": "Raise a decision", children: _jsx(DecisionForm, { tasks: data.tasks, busy: busy, onSubmit: (input) => void act(async () => {
143
- await data.actions.raiseDecision(input);
144
- setAsking(false);
145
- }), onCancel: () => setAsking(false) }) })), _jsx(components.DecisionList, { decisions: data.decisions, tasks: tasksById, taskHref: href, onAnswer: onAnswer, busy: busy, empty: _jsx(EmptyState, { title: "Nothing waits for a person", body: "A decision raised from a task, or from here, shows up on this page." }) })] }));
226
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.decisions.title }), _jsx(LinkButton, { href: newDecisionHref(key), variant: "primary", children: words.decisions.raise })] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.DecisionList, { decisions: data.decisions, tasks: tasksById, taskHref: href, docHref: toDoc, decisionHref: (decision) => decisionHref(key, decision.id), onAnswer: onAnswer, onUpdate: onUpdate, busy: busy, empty: _jsx(EmptyState, { title: words.decisions.empty.title, body: words.decisions.empty.body }) })] }));
227
+ }
228
+ if (route.name === "new-decision") {
229
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.decisions.raise }) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel", "aria-label": words.decisions.raiseAria, children: _jsx(DecisionForm, { tasks: data.tasks, busy: busy, onSubmit: (input) => void act(async () => {
230
+ const made = await data.actions.raiseDecision(input);
231
+ navigation.replace(decisionHref(key, made.id));
232
+ }), onCancel: () => navigation.back(projectHref(key, "decisions")) }) })] }));
233
+ }
234
+ if (route.name === "decision") {
235
+ const decision = data.decisions.find((candidate) => candidate.id === route.id);
236
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.decisions.one }), _jsx(LinkButton, { href: projectHref(key, "decisions"), children: words.decisions.all })] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), decision === undefined ? (_jsx(EmptyState, { title: words.decisions.noSuch.title, body: words.decisions.noSuch.body })) : (_jsx(DecisionCard, { decision: decision, task: decision.taskId === null ? undefined : tasksById.get(decision.taskId), taskHref: href, docHref: toDoc, onAnswer: onAnswer, onUpdate: onUpdate, busy: busy }))] }));
146
237
  }
147
238
  if (route.name === "feed") {
148
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: "What happened" }) }), _jsx(components.EventFeed, { events: data.events, href: (event) => event.taskId === null ? undefined : taskHref(project.key, event.taskId), empty: _jsx(EmptyState, { title: "Nothing happened yet" }) })] }));
239
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.feed.title }) }), _jsx(components.EventFeed, { events: data.events, href: (event) => {
240
+ const task = event.taskId === null ? undefined : tasksById.get(event.taskId);
241
+ if (task !== undefined) {
242
+ return href(task);
243
+ }
244
+ if (event.decisionId !== null) {
245
+ return decisionHref(key, event.decisionId);
246
+ }
247
+ return event.data.path === undefined ? undefined : toDoc(event.data.path);
248
+ }, empty: _jsx(EmptyState, { title: words.feed.empty }) })] }));
149
249
  }
150
250
  if (route.name === "skills") {
151
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: "Skills" }) }), _jsx(components.SkillList, { skills: data.skills })] }));
251
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.nav.skills }) }), _jsx(components.SkillList, { skills: data.skills })] }));
152
252
  }
153
253
  if (route.name === "members") {
154
254
  const me = data.me?.person ?? undefined;
155
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: "Members" }) }), _jsxs("p", { className: "sc-lead", children: [project.visibility === "workspace"
156
- ? "Everyone of the workspace is a member of this project; those listed here are its owners and the people named besides. "
157
- : "Only those listed here are in this project. ", "An owner configures the project; a member works on it. A workspace administrator is an owner of every project."] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.MemberList, { members: data.members, people: data.people, me: me, busy: busy, onAdd: owner ? (input) => void act(() => data.actions.addMember(input)) : undefined, onChangeRole: owner
255
+ const members = words.projects.members;
256
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: members.title }) }), _jsxs("p", { className: "sc-lead", children: [project.visibility === "workspace" ? members.leadWorkspace : members.leadPrivate, " ", members.leadRoles] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.MemberList, { members: data.members, people: data.people, me: me, busy: busy, onAdd: owner ? (input) => void act(() => data.actions.addMember(input)) : undefined, onChangeRole: owner
158
257
  ? (member, role) => void act(() => data.actions.updateMember(member.person.id, { role }))
159
258
  : undefined, onRemove: owner
160
259
  ? (member) => void act(() => data.actions.removeMember(member.person.id))
161
- : undefined, empty: _jsx(EmptyState, { title: "Nobody is listed", body: project.visibility === "workspace"
162
- ? "Everyone of the workspace is in; the administrators own it."
163
- : "The administrators own it until an owner is listed." }) })] }));
260
+ : undefined, empty: _jsx(EmptyState, { title: members.empty.title, body: project.visibility === "workspace" ? members.empty.workspace : members.empty.private }) })] }));
164
261
  }
165
262
  if (route.name === "settings") {
263
+ const settings = words.projects.settings;
166
264
  if (!owner) {
167
- return (_jsx(EmptyState, { title: "Only an owner may change the project", body: "Ask one of its owners, or a workspace administrator." }));
265
+ return _jsx(EmptyState, { title: settings.onlyOwner.title, body: settings.onlyOwner.body });
168
266
  }
169
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: "Settings" }) }), _jsxs("p", { className: "sc-lead", children: ["The key, ", _jsx("code", { children: project.key }), ", is what paths and the command say, and does not change. The skills address names a repository served by SkillCDN; empty, the organization's skills are shown."] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel", "aria-label": "Project settings", children: _jsx(ProjectForm, { project: project, busy: busy, onSubmit: (input) => void act(() => data.actions.updateProject(input)) }) })] }));
267
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: settings.title }) }), _jsxs("p", { className: "sc-lead", children: [settings.leadBefore, _jsx("code", { children: project.key }), settings.leadAfter] }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx("section", { className: "sc-panel", "aria-label": settings.aria, children: _jsx(ProjectForm, { project: project, busy: busy, onSubmit: (input) => void act(() => data.actions.updateProject(input)) }) })] }));
170
268
  }
171
- return _jsx(EmptyState, { title: "There is nothing at this address" });
269
+ return _jsx(NothingHere, {});
270
+ }
271
+ function NothingHere() {
272
+ const words = useWords();
273
+ return (_jsx(EmptyState, { title: words.common.nothingHere.title, body: words.common.nothingHere.body, action: _jsx(LinkButton, { href: PATHS.projects, children: words.nav.projects }) }));
274
+ }
275
+ /** One person's agents, for an administrator: read on their own, and taken away from here. */
276
+ function PersonAgentsPage(props) {
277
+ const words = useWords();
278
+ const { id, data, components } = props;
279
+ const me = data.me?.person ?? undefined;
280
+ const administrator = me?.role === "admin";
281
+ const person = data.people.find((candidate) => candidate.id === id);
282
+ const [tokens, setTokens] = useState(undefined);
283
+ const [generation, setGeneration] = useState(0);
284
+ const { busy, error, act } = useAction();
285
+ const { personTokens } = data.actions;
286
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `generation` is the trigger
287
+ useEffect(() => {
288
+ if (!administrator) {
289
+ return;
290
+ }
291
+ const controller = new AbortController();
292
+ personTokens(id, controller.signal)
293
+ .then((answer) => {
294
+ if (!controller.signal.aborted) {
295
+ setTokens(answer.items);
296
+ }
297
+ })
298
+ .catch(() => undefined);
299
+ return () => controller.abort();
300
+ }, [id, administrator, personTokens, generation]);
301
+ if (!administrator) {
302
+ return (_jsx(EmptyState, { title: words.people.onlyAdmin.title, body: words.people.onlyAdmin.body, action: _jsx(LinkButton, { href: PATHS.agents, children: words.nav.agents }) }));
303
+ }
304
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: words.people.agentsOf(person?.login ?? "?") }), _jsx(LinkButton, { href: PATHS.people, children: words.nav.people })] }), _jsx("p", { className: "sc-lead", children: words.people.agentsLead(person?.name ?? person?.login ?? words.feed.someone) }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), tokens === undefined ? (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: words.people.loadingAgents }) })) : (_jsx(components.TokenList, { tokens: tokens, busy: busy, onRevoke: (token) => void act(async () => {
305
+ await data.actions.revokePersonToken(id, token.id);
306
+ setGeneration((current) => current + 1);
307
+ }), empty: _jsx(EmptyState, { title: words.people.noAgent }) }))] }));
172
308
  }
173
309
  function WorkspacePage(props) {
174
- const { route, data, components } = props;
175
- const [making, setMaking] = useState(false);
310
+ const words = useWords();
311
+ const { route, data, components, navigation } = props;
176
312
  const [fresh, setFresh] = useState(undefined);
177
313
  const { busy, error, act } = useAction();
178
314
  if (route.name === "people") {
179
315
  const me = data.me?.person ?? undefined;
180
316
  const administrator = me?.role === "admin";
181
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: "People" }) }), _jsx("p", { className: "sc-lead", children: "Everyone who has signed in. An administrator configures the workspace and says what each person is; a member works in the projects they are in. What a person may do, their agents may do." }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.PeopleList, { people: data.people, me: me, busy: busy, onChangeRole: administrator
317
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "sc-page-head", children: _jsx("h1", { className: "sc-page-title", children: words.people.title }) }), _jsx("p", { className: "sc-lead", children: words.people.lead }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), _jsx(components.PeopleList, { people: data.people, me: me, busy: busy, onChangeRole: administrator
182
318
  ? (person, role) => void act(() => data.actions.updatePerson(person.id, { role }))
183
- : undefined })] }));
319
+ : undefined, agentsHref: administrator ? (person) => personAgentsHref(person.id) : undefined })] }));
184
320
  }
185
- if (route.name === "tokens") {
321
+ if (route.name === "person-agents") {
322
+ return _jsx(PersonAgentsPage, { id: route.id, data: data, components: components });
323
+ }
324
+ if (route.name === "connect") {
325
+ return _jsx(ConnectPage, { code: route.code, data: data, navigation: navigation });
326
+ }
327
+ if (route.name === "agents" || route.name === "new-token") {
328
+ const agents = words.agents;
329
+ const making = route.name === "new-token";
186
330
  const origin = typeof window === "undefined" ? "" : window.location.origin;
187
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: "Tokens" }), _jsx(Button, { variant: "primary", onClick: () => setMaking(true), disabled: making, children: "Make a token" })] }), _jsx("p", { className: "sc-lead", children: "A token lets an agent, a script or a console of your own act as you on this board: what you may do, it may do. Make one per agent, name it after where it runs, and remove it when that is over." }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), fresh !== undefined && (_jsx(NewToken, { token: fresh.token, secret: fresh.secret, onDone: () => setFresh(undefined) })), making && (_jsx("section", { className: "sc-panel", "aria-label": "Make a token", children: _jsx(TokenForm, { busy: busy, onSubmit: (input) => void act(async () => {
331
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sc-page-head", children: [_jsx("h1", { className: "sc-page-title", children: agents.title }), !making && (_jsx(LinkButton, { href: PATHS.newToken, variant: "primary", children: agents.makeByHand }))] }), _jsx("p", { className: "sc-lead", children: agents.lead }), error !== undefined && _jsx(Callout, { tone: "danger", children: error }), fresh !== undefined && (_jsx(NewToken, { token: fresh.token, secret: fresh.secret, onDone: () => setFresh(undefined) })), making && (_jsx("section", { className: "sc-panel", "aria-label": agents.makeAria, children: _jsx(TokenForm, { daysAtMost: data.me?.workspace.tokenDaysAtMost, busy: busy, onSubmit: (input) => void act(async () => {
188
332
  setFresh(await data.actions.createToken(input));
189
- setMaking(false);
190
- }), onCancel: () => setMaking(false) }) })), _jsx(components.TokenList, { tokens: data.tokens, busy: busy, onRevoke: (token) => void act(() => data.actions.revokeToken(token.id)), empty: _jsx(EmptyState, { title: "No tokens yet", body: "Make one for your agent, and it can work on this board as you." }) }), _jsxs("section", { className: "sc-panel sc-connect", "aria-label": "Connecting an agent", children: [_jsx("h2", { className: "sc-section-title", children: "Connecting an agent" }), _jsxs("p", { children: ["An agent works this board with the ", _jsx("code", { children: "console" }), " command, which comes with the", " ", _jsx("code", { children: "@skillcdn/console" }), " package. Install the package where the agent runs, sign the command in with a token made here, and say which project the directory works in:"] }), _jsxs("pre", { className: "sc-code", children: ["npm install -g @skillcdn/console", "\n", "console login --url ", origin, "\n", "console use ", "<project key>"] }), _jsxs("p", { children: ["It asks for the token and keeps it in your home directory, and your agent is you from then on: ", _jsx("code", { children: "console take" }), " starts work on a task, ", _jsx("code", { children: "console report" }), ",", " ", _jsx("code", { children: "console hand-in" }), " and ", _jsx("code", { children: "console ask" }), " say how it goes, and", " ", _jsx("code", { children: "console finish" }), " ends the run. ", _jsx("code", { children: "console help" }), " says the rest."] })] })] }));
333
+ navigation.replace(PATHS.agents);
334
+ }), onCancel: () => navigation.back(PATHS.agents) }) })), _jsx(components.TokenList, { tokens: data.tokens, busy: busy, onRevoke: (token) => void act(() => data.actions.revokeToken(token.id)), empty: _jsx(EmptyState, { title: agents.empty.title, body: agents.empty.body }) }), _jsx(ConnectWords, { origin: origin })] }));
191
335
  }
192
- return _jsx(EmptyState, { title: "There is nothing at this address" });
336
+ return _jsx(NothingHere, {});
193
337
  }
194
338
  export function createConsole(config = {}) {
195
339
  const components = { ...DEFAULT_COMPONENTS, ...config.components };
196
340
  const client = config.client ?? createClient({ baseUrl: config.baseUrl ?? "" });
197
341
  const fallbackTitle = config.title ?? "Console";
342
+ const languages = config.languages ?? DEFAULT_LANGUAGES;
198
343
  function App(props) {
199
- const { location, navigate } = useLocation(props.initialPath);
200
- const route = useMemo(() => matchRoute(location.pathname), [location.pathname]);
344
+ const { location, navigation } = useNavigation(props.initialPath);
345
+ useFollowLinks(navigation);
346
+ const route = useMemo(() => matchRoute(location.pathname, location.search), [location.pathname, location.search]);
201
347
  const projectKey = projectOf(route);
202
348
  const data = useConsoleData(client, projectKey);
203
349
  const failure = signInFailureOf(location.search);
204
350
  const title = data.me?.workspace.name ?? fallbackTitle;
351
+ // The language: the person's choice, else the browser's, else the first the console speaks.
352
+ const chosen = data.me?.language;
353
+ const pack = useMemo(() => chooseLanguage(chosen, typeof navigator === "undefined" ? [] : navigator.languages, languages), [chosen]);
354
+ const words = pack.messages;
355
+ // An address that moved is shown at its new one, in its place.
356
+ useEffect(() => {
357
+ if (route.name === "moved") {
358
+ navigation.replace(route.to);
359
+ }
360
+ }, [route, navigation]);
205
361
  useEffect(() => {
206
362
  if (typeof document !== "undefined") {
207
363
  document.title = data.project === undefined ? title : `${data.project.name} · ${title}`;
364
+ document.documentElement.lang = pack.tag;
208
365
  }
209
- }, [title, data.project]);
366
+ }, [title, data.project, pack.tag]);
367
+ let content;
210
368
  if (data.me === undefined) {
211
- return (_jsx("div", { className: "sc-loading", children: data.error === undefined ? (_jsx(Spinner, { label: "Loading" })) : (_jsx(Callout, { tone: "danger", title: "The console could not be reached", action: _jsx(Button, { onClick: data.reload, children: "Try again" }), children: data.error })) }));
369
+ content = (_jsx("div", { className: "sc-loading", children: data.error === undefined ? (_jsx(Spinner, { label: words.common.loading })) : (_jsx(Callout, { tone: "danger", title: words.common.couldNotReach, action: _jsx(Button, { onClick: data.reload, children: words.common.tryAgain }), children: data.error })) }));
212
370
  }
213
- if (data.me.person === null) {
214
- return (_jsx(components.SignIn, { title: title, providers: data.me.signIn, returnTo: withoutSignInParam(location.pathname, location.search), failure: failure }));
371
+ else if (data.me.person === null) {
372
+ content = (_jsx(components.SignIn, { title: title, brand: config.brand, providers: data.me.signIn, returnTo: withoutSignInParam(location.pathname, location.search), failure: failure }));
215
373
  }
216
- const project = data.project;
217
- const waiting = data.decisions.filter((decision) => decision.answer === null).length;
218
- const nav = projectKey === undefined
219
- ? [{ href: PATHS.projects, label: "Projects", current: route.name === "projects" }]
220
- : [
221
- {
222
- href: projectHref(projectKey),
223
- label: "Board",
224
- current: route.name === "board" || route.name === "task",
225
- },
226
- {
227
- href: projectHref(projectKey, "decisions"),
228
- label: "Decisions",
229
- current: route.name === "decisions",
230
- count: waiting,
231
- },
374
+ else {
375
+ const project = data.project;
376
+ const waiting = data.decisions.filter((decision) => decision.answer === null).length;
377
+ const workspaceNav = [
232
378
  {
233
- href: projectHref(projectKey, "feed"),
234
- label: "Feed",
235
- current: route.name === "feed",
379
+ href: PATHS.projects,
380
+ label: words.nav.projects,
381
+ current: route.name === "projects" || route.name === "new-project",
236
382
  },
237
383
  {
238
- href: projectHref(projectKey, "skills"),
239
- label: "Skills",
240
- current: route.name === "skills",
384
+ href: PATHS.people,
385
+ label: words.nav.people,
386
+ current: route.name === "people" || route.name === "person-agents",
241
387
  },
242
388
  {
243
- href: projectHref(projectKey, "members"),
244
- label: "Members",
245
- current: route.name === "members",
389
+ href: PATHS.agents,
390
+ label: words.nav.agents,
391
+ current: route.name === "agents" || route.name === "new-token",
246
392
  },
247
- ...(project?.role === "owner"
248
- ? [
249
- {
250
- href: projectHref(projectKey, "settings"),
251
- label: "Settings",
252
- current: route.name === "settings",
253
- },
254
- ]
255
- : []),
256
393
  ];
257
- nav.push({ href: PATHS.people, label: "People", current: route.name === "people" }, { href: PATHS.tokens, label: "Tokens", current: route.name === "tokens" });
258
- let page;
259
- if (!data.loaded) {
260
- page = (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: "Loading the workspace" }) }));
261
- }
262
- else if (route.name === "projects") {
263
- page = _jsx(ProjectsPage, { data: data, components: components, navigate: navigate });
264
- }
265
- else if (projectKey === undefined) {
266
- page = _jsx(WorkspacePage, { route: route, data: data, components: components });
267
- }
268
- else if (data.projectError !== undefined) {
269
- page = (_jsx(EmptyState, { title: "No such project", body: "It may not exist, or it is not yours to see. The projects you may work in are on the front page.", action: _jsx(Button, { onClick: () => navigate(PATHS.projects), children: "Projects" }) }));
270
- }
271
- else if (!data.projectLoaded || project === undefined) {
272
- page = (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: "Loading the project" }) }));
273
- }
274
- else {
275
- page = (_jsx(ProjectPage, { route: route, project: project, data: data, components: components, navigate: navigate }));
394
+ const nav = projectKey === undefined
395
+ ? workspaceNav
396
+ : [
397
+ {
398
+ href: projectHref(projectKey),
399
+ label: words.nav.board,
400
+ current: route.name === "board" || route.name === "task" || route.name === "new-task",
401
+ },
402
+ {
403
+ href: projectHref(projectKey, "docs"),
404
+ label: words.nav.docs,
405
+ current: route.name === "docs",
406
+ },
407
+ {
408
+ href: projectHref(projectKey, "decisions"),
409
+ label: words.nav.decisions,
410
+ current: route.name === "decisions" ||
411
+ route.name === "decision" ||
412
+ route.name === "new-decision",
413
+ count: waiting,
414
+ },
415
+ {
416
+ href: projectHref(projectKey, "feed"),
417
+ label: words.nav.feed,
418
+ current: route.name === "feed",
419
+ },
420
+ {
421
+ href: projectHref(projectKey, "skills"),
422
+ label: words.nav.skills,
423
+ current: route.name === "skills",
424
+ },
425
+ {
426
+ href: projectHref(projectKey, "members"),
427
+ label: words.nav.members,
428
+ current: route.name === "members",
429
+ },
430
+ ...(project?.role === "owner"
431
+ ? [
432
+ {
433
+ href: projectHref(projectKey, "settings"),
434
+ label: words.nav.settings,
435
+ current: route.name === "settings",
436
+ },
437
+ ]
438
+ : []),
439
+ ];
440
+ let page;
441
+ if (!data.loaded || route.name === "moved") {
442
+ page = (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: words.common.loadingWorkspace }) }));
443
+ }
444
+ else if (route.name === "projects" || route.name === "new-project") {
445
+ page = (_jsx(ProjectsPage, { data: data, components: components, navigation: navigation, making: route.name === "new-project" }));
446
+ }
447
+ else if (projectKey === undefined) {
448
+ page = (_jsx(WorkspacePage, { route: route, data: data, components: components, navigation: navigation }));
449
+ }
450
+ else if (data.projectError !== undefined) {
451
+ page = (_jsx(EmptyState, { title: words.common.noSuchProject.title, body: words.common.noSuchProject.body, action: _jsx(LinkButton, { href: PATHS.projects, children: words.nav.projects }) }));
452
+ }
453
+ else if (!data.projectLoaded || project === undefined) {
454
+ page = (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: words.common.loadingProject }) }));
455
+ }
456
+ else {
457
+ page = (_jsx(ProjectPage, { route: route, project: project, data: data, components: components, navigation: navigation }));
458
+ }
459
+ content = (_jsx(components.Shell, { title: title, brand: config.brand, project: projectKey === undefined
460
+ ? undefined
461
+ : { name: project?.name ?? projectKey, href: projectHref(projectKey) }, nav: nav, menu: workspaceNav, languages: languages.map((candidate) => ({
462
+ tag: candidate.tag,
463
+ label: candidate.label,
464
+ current: candidate.tag === pack.tag,
465
+ })), onLanguage: (tag) => void data.actions.updateMe({ language: tag }).catch(() => undefined), person: data.me.person, live: projectKey === undefined ? undefined : data.live, onNavigate: (href) => navigation.go(href), onSignOut: () => void data.actions.signOut(), children: page }));
276
466
  }
277
- return (_jsx(components.Shell, { title: title, project: projectKey === undefined
278
- ? undefined
279
- : { name: project?.name ?? projectKey, href: projectHref(projectKey) }, nav: nav, person: data.me.person, live: projectKey === undefined ? undefined : data.live, onNavigate: (href) => navigate(href), onSignOut: () => void data.actions.signOut(), children: page }));
467
+ return _jsx(LanguageContext.Provider, { value: pack, children: content });
280
468
  }
281
469
  return {
282
470
  App,