@skillcdn/console 0.1.3 → 0.1.5

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 (141) hide show
  1. package/README.md +7 -6
  2. package/dist/api.d.ts +4 -3
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +4 -3
  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 +45 -15
  9. package/dist/cli/cli.js.map +1 -1
  10. package/dist/cli/credentials.d.ts +1 -1
  11. package/dist/cli/help.d.ts.map +1 -1
  12. package/dist/cli/help.js +7 -4
  13. package/dist/cli/help.js.map +1 -1
  14. package/dist/cli/main.js +3 -2
  15. package/dist/cli/main.js.map +1 -1
  16. package/dist/cli/terminal.d.ts +0 -5
  17. package/dist/cli/terminal.d.ts.map +1 -1
  18. package/dist/cli/terminal.js +0 -43
  19. package/dist/cli/terminal.js.map +1 -1
  20. package/dist/client.d.ts +20 -1
  21. package/dist/client.d.ts.map +1 -1
  22. package/dist/client.js +10 -2
  23. package/dist/client.js.map +1 -1
  24. package/dist/components/board.d.ts.map +1 -1
  25. package/dist/components/board.js +6 -3
  26. package/dist/components/board.js.map +1 -1
  27. package/dist/components/connect.d.ts +28 -0
  28. package/dist/components/connect.d.ts.map +1 -0
  29. package/dist/components/connect.js +30 -0
  30. package/dist/components/connect.js.map +1 -0
  31. package/dist/components/decision-form.d.ts.map +1 -1
  32. package/dist/components/decision-form.js +4 -1
  33. package/dist/components/decision-form.js.map +1 -1
  34. package/dist/components/decision-list.d.ts +4 -0
  35. package/dist/components/decision-list.d.ts.map +1 -1
  36. package/dist/components/decision-list.js +15 -7
  37. package/dist/components/decision-list.js.map +1 -1
  38. package/dist/components/documents.d.ts +7 -5
  39. package/dist/components/documents.d.ts.map +1 -1
  40. package/dist/components/documents.js +28 -16
  41. package/dist/components/documents.js.map +1 -1
  42. package/dist/components/event-feed.d.ts +3 -2
  43. package/dist/components/event-feed.d.ts.map +1 -1
  44. package/dist/components/event-feed.js +50 -52
  45. package/dist/components/event-feed.js.map +1 -1
  46. package/dist/components/people.d.ts +2 -0
  47. package/dist/components/people.d.ts.map +1 -1
  48. package/dist/components/people.js +4 -2
  49. package/dist/components/people.js.map +1 -1
  50. package/dist/components/projects.d.ts +1 -0
  51. package/dist/components/projects.d.ts.map +1 -1
  52. package/dist/components/projects.js +16 -10
  53. package/dist/components/projects.js.map +1 -1
  54. package/dist/components/runs.d.ts +1 -0
  55. package/dist/components/runs.d.ts.map +1 -1
  56. package/dist/components/runs.js +6 -2
  57. package/dist/components/runs.js.map +1 -1
  58. package/dist/components/shell.d.ts +32 -1
  59. package/dist/components/shell.d.ts.map +1 -1
  60. package/dist/components/shell.js +56 -8
  61. package/dist/components/shell.js.map +1 -1
  62. package/dist/components/sign-in.d.ts +3 -0
  63. package/dist/components/sign-in.d.ts.map +1 -1
  64. package/dist/components/sign-in.js +3 -9
  65. package/dist/components/sign-in.js.map +1 -1
  66. package/dist/components/skills.d.ts.map +1 -1
  67. package/dist/components/skills.js +8 -12
  68. package/dist/components/skills.js.map +1 -1
  69. package/dist/components/task-form.d.ts.map +1 -1
  70. package/dist/components/task-form.js +5 -2
  71. package/dist/components/task-form.js.map +1 -1
  72. package/dist/components/task-view.d.ts +7 -0
  73. package/dist/components/task-view.d.ts.map +1 -1
  74. package/dist/components/task-view.js +6 -13
  75. package/dist/components/task-view.js.map +1 -1
  76. package/dist/components/tokens.d.ts +12 -0
  77. package/dist/components/tokens.d.ts.map +1 -1
  78. package/dist/components/tokens.js +25 -10
  79. package/dist/components/tokens.js.map +1 -1
  80. package/dist/components/ui.d.ts +3 -3
  81. package/dist/components/ui.d.ts.map +1 -1
  82. package/dist/components/ui.js +15 -9
  83. package/dist/components/ui.js.map +1 -1
  84. package/dist/connect-page.d.ts +10 -0
  85. package/dist/connect-page.d.ts.map +1 -0
  86. package/dist/connect-page.js +67 -0
  87. package/dist/connect-page.js.map +1 -0
  88. package/dist/connect.d.ts +11 -0
  89. package/dist/connect.d.ts.map +1 -0
  90. package/dist/connect.js +24 -0
  91. package/dist/connect.js.map +1 -0
  92. package/dist/console.css +976 -201
  93. package/dist/console.d.ts +6 -1
  94. package/dist/console.d.ts.map +1 -1
  95. package/dist/console.js +339 -136
  96. package/dist/console.js.map +1 -1
  97. package/dist/data.d.ts +10 -1
  98. package/dist/data.d.ts.map +1 -1
  99. package/dist/data.js +10 -0
  100. package/dist/data.js.map +1 -1
  101. package/dist/docs-page.d.ts +4 -1
  102. package/dist/docs-page.d.ts.map +1 -1
  103. package/dist/docs-page.js +74 -54
  104. package/dist/docs-page.js.map +1 -1
  105. package/dist/i18n/en.d.ts +473 -0
  106. package/dist/i18n/en.d.ts.map +1 -0
  107. package/dist/i18n/en.js +483 -0
  108. package/dist/i18n/en.js.map +1 -0
  109. package/dist/i18n/index.d.ts +26 -0
  110. package/dist/i18n/index.d.ts.map +1 -0
  111. package/dist/i18n/index.js +46 -0
  112. package/dist/i18n/index.js.map +1 -0
  113. package/dist/i18n/ko.d.ts +3 -0
  114. package/dist/i18n/ko.d.ts.map +1 -0
  115. package/dist/i18n/ko.js +472 -0
  116. package/dist/i18n/ko.js.map +1 -0
  117. package/dist/index.d.ts +5 -2
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +5 -2
  120. package/dist/index.js.map +1 -1
  121. package/dist/limits.d.ts +4 -0
  122. package/dist/limits.d.ts.map +1 -1
  123. package/dist/limits.js +4 -0
  124. package/dist/limits.js.map +1 -1
  125. package/dist/router.d.ts +88 -11
  126. package/dist/router.d.ts.map +1 -1
  127. package/dist/router.js +163 -34
  128. package/dist/router.js.map +1 -1
  129. package/dist/routes.d.ts +8 -0
  130. package/dist/routes.d.ts.map +1 -1
  131. package/dist/routes.js +14 -0
  132. package/dist/routes.js.map +1 -1
  133. package/dist/schemas.d.ts +59 -2
  134. package/dist/schemas.d.ts.map +1 -1
  135. package/dist/schemas.js +55 -9
  136. package/dist/schemas.js.map +1 -1
  137. package/dist/use-action.d.ts +2 -1
  138. package/dist/use-action.d.ts.map +1 -1
  139. package/dist/use-action.js +7 -3
  140. package/dist/use-action.js.map +1 -1
  141. package/package.json +1 -1
package/dist/console.js CHANGED
@@ -3,21 +3,24 @@ import { StrictMode, useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
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
21
  import { DocsPage } from "./docs-page.js";
20
- import { docHref, matchRoute, PATHS, projectHref, signInFailureOf, taskHref, withoutSignInParam, } from "./router.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";
21
24
  import { projectPath } from "./routes.js";
22
25
  import { useAction } from "./use-action.js";
23
26
  export const DEFAULT_COMPONENTS = {
@@ -33,8 +36,21 @@ export const DEFAULT_COMPONENTS = {
33
36
  ProjectList,
34
37
  MemberList,
35
38
  };
36
- /** The browser's location, as the app reads it; a given path while rendering elsewhere. */
37
- 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) {
38
54
  const read = useCallback(() => typeof window === "undefined"
39
55
  ? { pathname: initialPath ?? "/", search: "" }
40
56
  : { pathname: window.location.pathname, search: window.location.search }, [initialPath]);
@@ -44,44 +60,125 @@ function useLocation(initialPath) {
44
60
  window.addEventListener("popstate", update);
45
61
  return () => window.removeEventListener("popstate", update);
46
62
  }, [read]);
47
- const navigate = useCallback((href, replace = false) => {
48
- if (typeof window === "undefined") {
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
+ };
94
+ }, [read]);
95
+ return { location, navigation };
96
+ }
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") {
49
105
  return;
50
106
  }
51
- if (replace) {
52
- window.history.replaceState(null, "", href);
53
- }
54
- else {
55
- window.history.pushState(null, "", href);
56
- window.scrollTo({ top: 0, left: 0, behavior: "instant" });
57
- }
58
- setLocation(read());
59
- }, [read]);
60
- return { location, navigate };
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]);
61
135
  }
62
136
  /** The key of the project a route is on, or nothing. */
63
137
  const projectOf = (route) => "project" in route ? route.project : undefined;
138
+ /** The field the pages lie on, for a page without the shell: signing in, and the wait before it. */
139
+ function Ground(props) {
140
+ return (_jsx("div", { className: "sc-shell", children: _jsx("main", { className: "sc-main", children: props.children }) }));
141
+ }
142
+ /** A link that reads as a button: where a form is, for instance. */
143
+ function LinkButton(props) {
144
+ return (_jsx("a", { className: `sc-button sc-button-${props.variant ?? "secondary"}`, href: props.href, children: props.children }));
145
+ }
64
146
  function ProjectsPage(props) {
65
- const { data, components, navigate } = props;
66
- const [making, setMaking] = useState(false);
147
+ const words = useWords().projects;
148
+ const { data, components, navigation, making } = props;
67
149
  const { busy, error, act } = useAction();
68
150
  const href = (project) => projectHref(project.key);
69
- 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 () => {
151
+ 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 sc-panel-form", "aria-label": words.form.aria, children: _jsx(ProjectForm, { busy: busy, onSubmit: (input) => void act(async () => {
70
152
  const made = await data.actions.createProject(input);
71
- setMaking(false);
72
- navigate(projectHref(made.key));
73
- }), 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" }) }) })] }));
153
+ navigation.replace(projectHref(made.key));
154
+ }), 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 }) }) })] }));
74
155
  }
75
156
  function TaskPage(props) {
76
- const { project, task, data, components, busy, error, act } = props;
157
+ const words = useWords();
158
+ const { route, project, data, components, navigation, busy, error, act } = props;
159
+ const key = project.key;
160
+ const task = route.number === undefined
161
+ ? data.tasks.find((candidate) => candidate.id === route.id)
162
+ : data.tasks.find((candidate) => candidate.number === route.number);
77
163
  const [history, setHistory] = useState(undefined);
78
164
  const { taskHistory } = data.actions;
165
+ const taskId = task?.id;
166
+ const number = task?.number;
167
+ // A task named by its id is shown at its number's address.
168
+ useEffect(() => {
169
+ if (route.id !== undefined && number !== undefined) {
170
+ navigation.replace(taskHref(key, number, route.form));
171
+ }
172
+ }, [route.id, route.form, number, key, navigation]);
79
173
  // Everything that happened to the task, asked for again whenever the project's feed grows.
80
174
  const feedLength = data.events.length;
81
175
  // biome-ignore lint/correctness/useExhaustiveDependencies: the feed's length is the trigger
82
176
  useEffect(() => {
177
+ if (taskId === undefined) {
178
+ return;
179
+ }
83
180
  const controller = new AbortController();
84
- taskHistory(task.id, controller.signal)
181
+ taskHistory(taskId, controller.signal)
85
182
  .then((events) => {
86
183
  if (!controller.signal.aborted) {
87
184
  setHistory(events);
@@ -89,183 +186,289 @@ function TaskPage(props) {
89
186
  })
90
187
  .catch(() => undefined);
91
188
  return () => controller.abort();
92
- }, [task.id, taskHistory, feedLength]);
93
- 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), docHref: (path) => docHref(project.key, path), 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)), onUpdateDecision: (decision, patch) => void act(() => data.actions.updateDecision(decision.id, patch)), onAbandonRun: (run) => void act(() => data.actions.abandonRun(run.id)) }));
189
+ }, [taskId, taskHistory, feedLength]);
190
+ if (task === undefined) {
191
+ return (_jsx(EmptyState, { title: words.task.noSuch.title, body: words.task.noSuch.body, action: _jsx(LinkButton, { href: projectHref(key), children: words.nav.board }) }));
192
+ }
193
+ const here = taskHref(key, task.number);
194
+ 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 () => {
195
+ await data.actions.updateTask(target.id, patch);
196
+ navigation.replace(here);
197
+ }), onMove: (target, state) => void act(() => data.actions.updateTask(target.id, { state })), onRaiseDecision: (input) => void act(async () => {
198
+ await data.actions.raiseDecision(input);
199
+ navigation.replace(here);
200
+ }), 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)) }));
94
201
  }
95
202
  function ProjectPage(props) {
96
- const { route, project, data, components, navigate } = props;
97
- const [writing, setWriting] = useState(false);
98
- const [asking, setAsking] = useState(false);
203
+ const words = useWords();
204
+ const { route, project, data, components, navigation } = props;
99
205
  const { busy, error, act } = useAction();
100
206
  const tasksById = useMemo(() => new Map(data.tasks.map((task) => [task.id, task])), [data.tasks]);
101
- const href = (task) => taskHref(project.key, task.id);
207
+ const key = project.key;
208
+ const href = (task) => taskHref(key, task.number);
209
+ const toDoc = (path) => docHref(key, path);
102
210
  const owner = project.role === "owner";
103
211
  const onMove = (task, state) => void act(() => data.actions.updateTask(task.id, { state }));
104
212
  const onAnswer = (decision, input) => void act(() => data.actions.answerDecision(decision.id, input));
213
+ const onUpdate = (decision, patch) => void act(() => data.actions.updateDecision(decision.id, patch));
105
214
  if (route.name === "board") {
106
- 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 () => {
107
- await data.actions.createTask(input);
108
- setWriting(false);
109
- }), 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 })] }));
215
+ 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 })] }));
216
+ }
217
+ if (route.name === "new-task") {
218
+ 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 sc-panel-form", "aria-label": words.task.newAria, children: _jsx(TaskForm, { people: data.people, parents: data.tasks, busy: busy, onSubmit: (input) => void act(async () => {
219
+ const made = await data.actions.createTask(input);
220
+ navigation.replace(taskHref(key, made.number));
221
+ }), onCancel: () => navigation.back(projectHref(key)) }) })] }));
110
222
  }
111
223
  if (route.name === "task") {
112
- const task = tasksById.get(route.id);
113
- if (task === undefined) {
114
- return (_jsx(EmptyState, { title: "No such task", body: "It may belong to another project, or the link is wrong." }));
115
- }
116
- return (_jsx(TaskPage, { project: project, task: task, data: data, components: components, busy: busy, error: error, act: act }));
224
+ return (_jsx(TaskPage, { route: route, project: project, data: data, components: components, navigation: navigation, busy: busy, error: error, act: act }));
117
225
  }
118
226
  if (route.name === "docs") {
119
- return _jsx(DocsPage, { project: project, path: route.path, data: data, navigate: navigate });
227
+ return (_jsx(DocsPage, { project: project, path: route.path, view: route.view, data: data, navigation: navigation }));
120
228
  }
121
229
  if (route.name === "decisions") {
122
- 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 () => {
123
- await data.actions.raiseDecision(input);
124
- setAsking(false);
125
- }), onCancel: () => setAsking(false) }) })), _jsx(components.DecisionList, { decisions: data.decisions, tasks: tasksById, taskHref: href, docHref: (path) => docHref(project.key, path), onAnswer: onAnswer, onUpdate: (decision, patch) => void act(() => data.actions.updateDecision(decision.id, patch)), 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." }) })] }));
230
+ 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 }) })] }));
231
+ }
232
+ if (route.name === "new-decision") {
233
+ 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 sc-panel-form", "aria-label": words.decisions.raiseAria, children: _jsx(DecisionForm, { tasks: data.tasks, busy: busy, onSubmit: (input) => void act(async () => {
234
+ const made = await data.actions.raiseDecision(input);
235
+ navigation.replace(decisionHref(key, made.id));
236
+ }), onCancel: () => navigation.back(projectHref(key, "decisions")) }) })] }));
237
+ }
238
+ if (route.name === "decision") {
239
+ const decision = data.decisions.find((candidate) => candidate.id === route.id);
240
+ 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 }))] }));
126
241
  }
127
242
  if (route.name === "feed") {
128
- 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
129
- ? taskHref(project.key, event.taskId)
130
- : event.data.path !== undefined
131
- ? docHref(project.key, event.data.path)
132
- : undefined, empty: _jsx(EmptyState, { title: "Nothing happened yet" }) })] }));
243
+ 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) => {
244
+ const task = event.taskId === null ? undefined : tasksById.get(event.taskId);
245
+ if (task !== undefined) {
246
+ return href(task);
247
+ }
248
+ if (event.decisionId !== null) {
249
+ return decisionHref(key, event.decisionId);
250
+ }
251
+ return event.data.path === undefined ? undefined : toDoc(event.data.path);
252
+ }, empty: _jsx(EmptyState, { title: words.feed.empty }) })] }));
133
253
  }
134
254
  if (route.name === "skills") {
135
- 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 })] }));
255
+ 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 })] }));
136
256
  }
137
257
  if (route.name === "members") {
138
258
  const me = data.me?.person ?? undefined;
139
- 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"
140
- ? "Everyone of the workspace is a member of this project; those listed here are its owners and the people named besides. "
141
- : "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
259
+ const members = words.projects.members;
260
+ 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
142
261
  ? (member, role) => void act(() => data.actions.updateMember(member.person.id, { role }))
143
262
  : undefined, onRemove: owner
144
263
  ? (member) => void act(() => data.actions.removeMember(member.person.id))
145
- : undefined, empty: _jsx(EmptyState, { title: "Nobody is listed", body: project.visibility === "workspace"
146
- ? "Everyone of the workspace is in; the administrators own it."
147
- : "The administrators own it until an owner is listed." }) })] }));
264
+ : undefined, empty: _jsx(EmptyState, { title: members.empty.title, body: project.visibility === "workspace" ? members.empty.workspace : members.empty.private }) })] }));
148
265
  }
149
266
  if (route.name === "settings") {
267
+ const settings = words.projects.settings;
150
268
  if (!owner) {
151
- return (_jsx(EmptyState, { title: "Only an owner may change the project", body: "Ask one of its owners, or a workspace administrator." }));
269
+ return _jsx(EmptyState, { title: settings.onlyOwner.title, body: settings.onlyOwner.body });
152
270
  }
153
- 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)) }) })] }));
271
+ 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 sc-panel-form", "aria-label": settings.aria, children: _jsx(ProjectForm, { project: project, busy: busy, onSubmit: (input) => void act(() => data.actions.updateProject(input)) }) })] }));
154
272
  }
155
- return _jsx(EmptyState, { title: "There is nothing at this address" });
273
+ return _jsx(NothingHere, {});
274
+ }
275
+ function NothingHere() {
276
+ const words = useWords();
277
+ return (_jsx(EmptyState, { title: words.common.nothingHere.title, body: words.common.nothingHere.body, action: _jsx(LinkButton, { href: PATHS.projects, children: words.nav.projects }) }));
278
+ }
279
+ /** One person's agents, for an administrator: read on their own, and taken away from here. */
280
+ function PersonAgentsPage(props) {
281
+ const words = useWords();
282
+ const { id, data, components } = props;
283
+ const me = data.me?.person ?? undefined;
284
+ const administrator = me?.role === "admin";
285
+ const person = data.people.find((candidate) => candidate.id === id);
286
+ const [tokens, setTokens] = useState(undefined);
287
+ const [generation, setGeneration] = useState(0);
288
+ const { busy, error, act } = useAction();
289
+ const { personTokens } = data.actions;
290
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `generation` is the trigger
291
+ useEffect(() => {
292
+ if (!administrator) {
293
+ return;
294
+ }
295
+ const controller = new AbortController();
296
+ personTokens(id, controller.signal)
297
+ .then((answer) => {
298
+ if (!controller.signal.aborted) {
299
+ setTokens(answer.items);
300
+ }
301
+ })
302
+ .catch(() => undefined);
303
+ return () => controller.abort();
304
+ }, [id, administrator, personTokens, generation]);
305
+ if (!administrator) {
306
+ return (_jsx(EmptyState, { title: words.people.onlyAdmin.title, body: words.people.onlyAdmin.body, action: _jsx(LinkButton, { href: PATHS.agents, children: words.nav.agents }) }));
307
+ }
308
+ 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 () => {
309
+ await data.actions.revokePersonToken(id, token.id);
310
+ setGeneration((current) => current + 1);
311
+ }), empty: _jsx(EmptyState, { title: words.people.noAgent }) }))] }));
156
312
  }
157
313
  function WorkspacePage(props) {
158
- const { route, data, components } = props;
159
- const [making, setMaking] = useState(false);
314
+ const words = useWords();
315
+ const { route, data, components, navigation } = props;
160
316
  const [fresh, setFresh] = useState(undefined);
161
317
  const { busy, error, act } = useAction();
162
318
  if (route.name === "people") {
163
319
  const me = data.me?.person ?? undefined;
164
320
  const administrator = me?.role === "admin";
165
- 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
321
+ 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
166
322
  ? (person, role) => void act(() => data.actions.updatePerson(person.id, { role }))
167
- : undefined })] }));
323
+ : undefined, agentsHref: administrator ? (person) => personAgentsHref(person.id) : undefined })] }));
324
+ }
325
+ if (route.name === "person-agents") {
326
+ return _jsx(PersonAgentsPage, { id: route.id, data: data, components: components });
168
327
  }
169
- if (route.name === "tokens") {
328
+ if (route.name === "connect") {
329
+ return _jsx(ConnectPage, { code: route.code, data: data, navigation: navigation });
330
+ }
331
+ if (route.name === "agents" || route.name === "new-token") {
332
+ const agents = words.agents;
333
+ const making = route.name === "new-token";
170
334
  const origin = typeof window === "undefined" ? "" : window.location.origin;
171
- 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 () => {
335
+ 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 sc-panel-form", "aria-label": agents.makeAria, children: _jsx(TokenForm, { daysAtMost: data.me?.workspace.tokenDaysAtMost, busy: busy, onSubmit: (input) => void act(async () => {
172
336
  setFresh(await data.actions.createToken(input));
173
- setMaking(false);
174
- }), 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."] })] })] }));
337
+ navigation.replace(PATHS.agents);
338
+ }), 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 })] }));
175
339
  }
176
- return _jsx(EmptyState, { title: "There is nothing at this address" });
340
+ return _jsx(NothingHere, {});
177
341
  }
178
342
  export function createConsole(config = {}) {
179
343
  const components = { ...DEFAULT_COMPONENTS, ...config.components };
180
344
  const client = config.client ?? createClient({ baseUrl: config.baseUrl ?? "" });
181
345
  const fallbackTitle = config.title ?? "Console";
346
+ const languages = config.languages ?? DEFAULT_LANGUAGES;
182
347
  function App(props) {
183
- const { location, navigate } = useLocation(props.initialPath);
184
- const route = useMemo(() => matchRoute(location.pathname), [location.pathname]);
348
+ const { location, navigation } = useNavigation(props.initialPath);
349
+ useFollowLinks(navigation);
350
+ const route = useMemo(() => matchRoute(location.pathname, location.search), [location.pathname, location.search]);
185
351
  const projectKey = projectOf(route);
186
352
  const data = useConsoleData(client, projectKey);
187
353
  const failure = signInFailureOf(location.search);
188
354
  const title = data.me?.workspace.name ?? fallbackTitle;
355
+ // The language: the person's choice, else the browser's, else the first the console speaks.
356
+ const chosen = data.me?.language;
357
+ const pack = useMemo(() => chooseLanguage(chosen, typeof navigator === "undefined" ? [] : navigator.languages, languages), [chosen]);
358
+ const words = pack.messages;
359
+ // An address that moved is shown at its new one, in its place.
360
+ useEffect(() => {
361
+ if (route.name === "moved") {
362
+ navigation.replace(route.to);
363
+ }
364
+ }, [route, navigation]);
189
365
  useEffect(() => {
190
366
  if (typeof document !== "undefined") {
191
367
  document.title = data.project === undefined ? title : `${data.project.name} · ${title}`;
368
+ document.documentElement.lang = pack.tag;
192
369
  }
193
- }, [title, data.project]);
370
+ }, [title, data.project, pack.tag]);
371
+ let content;
194
372
  if (data.me === undefined) {
195
- 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 })) }));
373
+ content = (_jsx(Ground, { children: _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 })) }) }));
196
374
  }
197
- if (data.me.person === null) {
198
- return (_jsx(components.SignIn, { title: title, providers: data.me.signIn, returnTo: withoutSignInParam(location.pathname, location.search), failure: failure }));
375
+ else if (data.me.person === null) {
376
+ content = (_jsx(Ground, { children: _jsx(components.SignIn, { title: title, brand: config.brand, providers: data.me.signIn, returnTo: withoutSignInParam(location.pathname, location.search), failure: failure }) }));
199
377
  }
200
- const project = data.project;
201
- const waiting = data.decisions.filter((decision) => decision.answer === null).length;
202
- const nav = projectKey === undefined
203
- ? [{ href: PATHS.projects, label: "Projects", current: route.name === "projects" }]
204
- : [
205
- {
206
- href: projectHref(projectKey),
207
- label: "Board",
208
- current: route.name === "board" || route.name === "task",
209
- },
210
- {
211
- href: projectHref(projectKey, "docs"),
212
- label: "Docs",
213
- current: route.name === "docs",
214
- },
215
- {
216
- href: projectHref(projectKey, "decisions"),
217
- label: "Decisions",
218
- current: route.name === "decisions",
219
- count: waiting,
220
- },
378
+ else {
379
+ const project = data.project;
380
+ const waiting = data.decisions.filter((decision) => decision.answer === null).length;
381
+ const workspaceNav = [
221
382
  {
222
- href: projectHref(projectKey, "feed"),
223
- label: "Feed",
224
- current: route.name === "feed",
383
+ href: PATHS.projects,
384
+ label: words.nav.projects,
385
+ current: route.name === "projects" || route.name === "new-project",
225
386
  },
226
387
  {
227
- href: projectHref(projectKey, "skills"),
228
- label: "Skills",
229
- current: route.name === "skills",
388
+ href: PATHS.people,
389
+ label: words.nav.people,
390
+ current: route.name === "people" || route.name === "person-agents",
230
391
  },
231
392
  {
232
- href: projectHref(projectKey, "members"),
233
- label: "Members",
234
- current: route.name === "members",
393
+ href: PATHS.agents,
394
+ label: words.nav.agents,
395
+ current: route.name === "agents" || route.name === "new-token",
235
396
  },
236
- ...(project?.role === "owner"
237
- ? [
238
- {
239
- href: projectHref(projectKey, "settings"),
240
- label: "Settings",
241
- current: route.name === "settings",
242
- },
243
- ]
244
- : []),
245
397
  ];
246
- nav.push({ href: PATHS.people, label: "People", current: route.name === "people" }, { href: PATHS.tokens, label: "Tokens", current: route.name === "tokens" });
247
- let page;
248
- if (!data.loaded) {
249
- page = (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: "Loading the workspace" }) }));
250
- }
251
- else if (route.name === "projects") {
252
- page = _jsx(ProjectsPage, { data: data, components: components, navigate: navigate });
253
- }
254
- else if (projectKey === undefined) {
255
- page = _jsx(WorkspacePage, { route: route, data: data, components: components });
256
- }
257
- else if (data.projectError !== undefined) {
258
- 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" }) }));
259
- }
260
- else if (!data.projectLoaded || project === undefined) {
261
- page = (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: "Loading the project" }) }));
262
- }
263
- else {
264
- page = (_jsx(ProjectPage, { route: route, project: project, data: data, components: components, navigate: navigate }));
398
+ const nav = projectKey === undefined
399
+ ? workspaceNav
400
+ : [
401
+ {
402
+ href: projectHref(projectKey),
403
+ label: words.nav.board,
404
+ current: route.name === "board" || route.name === "task" || route.name === "new-task",
405
+ },
406
+ {
407
+ href: projectHref(projectKey, "docs"),
408
+ label: words.nav.docs,
409
+ current: route.name === "docs",
410
+ },
411
+ {
412
+ href: projectHref(projectKey, "decisions"),
413
+ label: words.nav.decisions,
414
+ current: route.name === "decisions" ||
415
+ route.name === "decision" ||
416
+ route.name === "new-decision",
417
+ count: waiting,
418
+ },
419
+ {
420
+ href: projectHref(projectKey, "feed"),
421
+ label: words.nav.feed,
422
+ current: route.name === "feed",
423
+ },
424
+ {
425
+ href: projectHref(projectKey, "skills"),
426
+ label: words.nav.skills,
427
+ current: route.name === "skills",
428
+ },
429
+ {
430
+ href: projectHref(projectKey, "members"),
431
+ label: words.nav.members,
432
+ current: route.name === "members",
433
+ },
434
+ ...(project?.role === "owner"
435
+ ? [
436
+ {
437
+ href: projectHref(projectKey, "settings"),
438
+ label: words.nav.settings,
439
+ current: route.name === "settings",
440
+ },
441
+ ]
442
+ : []),
443
+ ];
444
+ let page;
445
+ if (!data.loaded || route.name === "moved") {
446
+ page = (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: words.common.loadingWorkspace }) }));
447
+ }
448
+ else if (route.name === "projects" || route.name === "new-project") {
449
+ page = (_jsx(ProjectsPage, { data: data, components: components, navigation: navigation, making: route.name === "new-project" }));
450
+ }
451
+ else if (projectKey === undefined) {
452
+ page = (_jsx(WorkspacePage, { route: route, data: data, components: components, navigation: navigation }));
453
+ }
454
+ else if (data.projectError !== undefined) {
455
+ page = (_jsx(EmptyState, { title: words.common.noSuchProject.title, body: words.common.noSuchProject.body, action: _jsx(LinkButton, { href: PATHS.projects, children: words.nav.projects }) }));
456
+ }
457
+ else if (!data.projectLoaded || project === undefined) {
458
+ page = (_jsx("div", { className: "sc-loading", children: _jsx(Spinner, { label: words.common.loadingProject }) }));
459
+ }
460
+ else {
461
+ page = (_jsx(ProjectPage, { route: route, project: project, data: data, components: components, navigation: navigation }));
462
+ }
463
+ content = (_jsx(components.Shell, { title: title, brand: config.brand, project: projectKey === undefined
464
+ ? undefined
465
+ : { name: project?.name ?? projectKey, href: projectHref(projectKey) }, nav: nav, menu: workspaceNav, languages: languages.map((candidate) => ({
466
+ tag: candidate.tag,
467
+ label: candidate.label,
468
+ current: candidate.tag === pack.tag,
469
+ })), onLanguage: (tag) => void data.actions.updateMe({ language: tag }).catch(() => undefined), person: data.me.person, live: projectKey === undefined ? undefined : data.live, wide: route.name === "board", onNavigate: (href) => navigation.go(href), onSignOut: () => void data.actions.signOut(), children: page }));
265
470
  }
266
- return (_jsx(components.Shell, { title: title, project: projectKey === undefined
267
- ? undefined
268
- : { 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 }));
471
+ return _jsx(LanguageContext.Provider, { value: pack, children: content });
269
472
  }
270
473
  return {
271
474
  App,