@skillcdn/console 0.1.3 → 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 (141) hide show
  1. package/README.md +5 -4
  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 +27 -15
  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 +30 -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 +7 -14
  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 +148 -10
  93. package/dist/console.d.ts +6 -1
  94. package/dist/console.d.ts.map +1 -1
  95. package/dist/console.js +335 -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 +471 -0
  106. package/dist/i18n/en.d.ts.map +1 -0
  107. package/dist/i18n/en.js +481 -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 +470 -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,121 @@ 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
+ /** 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
+ }
64
142
  function ProjectsPage(props) {
65
- const { data, components, navigate } = props;
66
- const [making, setMaking] = useState(false);
143
+ const words = useWords().projects;
144
+ const { data, components, navigation, making } = props;
67
145
  const { busy, error, act } = useAction();
68
146
  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 () => {
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 () => {
70
148
  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" }) }) })] }));
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 }) }) })] }));
74
151
  }
75
152
  function TaskPage(props) {
76
- 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);
77
159
  const [history, setHistory] = useState(undefined);
78
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]);
79
169
  // Everything that happened to the task, asked for again whenever the project's feed grows.
80
170
  const feedLength = data.events.length;
81
171
  // biome-ignore lint/correctness/useExhaustiveDependencies: the feed's length is the trigger
82
172
  useEffect(() => {
173
+ if (taskId === undefined) {
174
+ return;
175
+ }
83
176
  const controller = new AbortController();
84
- taskHistory(task.id, controller.signal)
177
+ taskHistory(taskId, controller.signal)
85
178
  .then((events) => {
86
179
  if (!controller.signal.aborted) {
87
180
  setHistory(events);
@@ -89,183 +182,289 @@ function TaskPage(props) {
89
182
  })
90
183
  .catch(() => undefined);
91
184
  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)) }));
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)) }));
94
197
  }
95
198
  function ProjectPage(props) {
96
- const { route, project, data, components, navigate } = props;
97
- const [writing, setWriting] = useState(false);
98
- const [asking, setAsking] = useState(false);
199
+ const words = useWords();
200
+ const { route, project, data, components, navigation } = props;
99
201
  const { busy, error, act } = useAction();
100
202
  const tasksById = useMemo(() => new Map(data.tasks.map((task) => [task.id, task])), [data.tasks]);
101
- 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);
102
206
  const owner = project.role === "owner";
103
207
  const onMove = (task, state) => void act(() => data.actions.updateTask(task.id, { state }));
104
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));
105
210
  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 })] }));
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)) }) })] }));
110
218
  }
111
219
  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 }));
220
+ return (_jsx(TaskPage, { route: route, project: project, data: data, components: components, navigation: navigation, busy: busy, error: error, act: act }));
117
221
  }
118
222
  if (route.name === "docs") {
119
- return _jsx(DocsPage, { project: project, path: route.path, data: data, navigate: navigate });
223
+ return (_jsx(DocsPage, { project: project, path: route.path, view: route.view, data: data, navigation: navigation }));
120
224
  }
121
225
  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." }) })] }));
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 }))] }));
126
237
  }
127
238
  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" }) })] }));
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 }) })] }));
133
249
  }
134
250
  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 })] }));
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 })] }));
136
252
  }
137
253
  if (route.name === "members") {
138
254
  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
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
142
257
  ? (member, role) => void act(() => data.actions.updateMember(member.person.id, { role }))
143
258
  : undefined, onRemove: owner
144
259
  ? (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." }) })] }));
260
+ : undefined, empty: _jsx(EmptyState, { title: members.empty.title, body: project.visibility === "workspace" ? members.empty.workspace : members.empty.private }) })] }));
148
261
  }
149
262
  if (route.name === "settings") {
263
+ const settings = words.projects.settings;
150
264
  if (!owner) {
151
- 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 });
266
+ }
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)) }) })] }));
268
+ }
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;
152
290
  }
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)) }) })] }));
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 }) }));
154
303
  }
155
- return _jsx(EmptyState, { title: "There is nothing at this address" });
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 }) }))] }));
156
308
  }
157
309
  function WorkspacePage(props) {
158
- const { route, data, components } = props;
159
- const [making, setMaking] = useState(false);
310
+ const words = useWords();
311
+ const { route, data, components, navigation } = props;
160
312
  const [fresh, setFresh] = useState(undefined);
161
313
  const { busy, error, act } = useAction();
162
314
  if (route.name === "people") {
163
315
  const me = data.me?.person ?? undefined;
164
316
  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
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
166
318
  ? (person, role) => void act(() => data.actions.updatePerson(person.id, { role }))
167
- : undefined })] }));
319
+ : undefined, agentsHref: administrator ? (person) => personAgentsHref(person.id) : undefined })] }));
320
+ }
321
+ if (route.name === "person-agents") {
322
+ return _jsx(PersonAgentsPage, { id: route.id, data: data, components: components });
168
323
  }
169
- if (route.name === "tokens") {
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";
170
330
  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 () => {
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 () => {
172
332
  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."] })] })] }));
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 })] }));
175
335
  }
176
- return _jsx(EmptyState, { title: "There is nothing at this address" });
336
+ return _jsx(NothingHere, {});
177
337
  }
178
338
  export function createConsole(config = {}) {
179
339
  const components = { ...DEFAULT_COMPONENTS, ...config.components };
180
340
  const client = config.client ?? createClient({ baseUrl: config.baseUrl ?? "" });
181
341
  const fallbackTitle = config.title ?? "Console";
342
+ const languages = config.languages ?? DEFAULT_LANGUAGES;
182
343
  function App(props) {
183
- const { location, navigate } = useLocation(props.initialPath);
184
- 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]);
185
347
  const projectKey = projectOf(route);
186
348
  const data = useConsoleData(client, projectKey);
187
349
  const failure = signInFailureOf(location.search);
188
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]);
189
361
  useEffect(() => {
190
362
  if (typeof document !== "undefined") {
191
363
  document.title = data.project === undefined ? title : `${data.project.name} · ${title}`;
364
+ document.documentElement.lang = pack.tag;
192
365
  }
193
- }, [title, data.project]);
366
+ }, [title, data.project, pack.tag]);
367
+ let content;
194
368
  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 })) }));
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 })) }));
196
370
  }
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 }));
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 }));
199
373
  }
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
- },
374
+ else {
375
+ const project = data.project;
376
+ const waiting = data.decisions.filter((decision) => decision.answer === null).length;
377
+ const workspaceNav = [
221
378
  {
222
- href: projectHref(projectKey, "feed"),
223
- label: "Feed",
224
- current: route.name === "feed",
379
+ href: PATHS.projects,
380
+ label: words.nav.projects,
381
+ current: route.name === "projects" || route.name === "new-project",
225
382
  },
226
383
  {
227
- href: projectHref(projectKey, "skills"),
228
- label: "Skills",
229
- current: route.name === "skills",
384
+ href: PATHS.people,
385
+ label: words.nav.people,
386
+ current: route.name === "people" || route.name === "person-agents",
230
387
  },
231
388
  {
232
- href: projectHref(projectKey, "members"),
233
- label: "Members",
234
- current: route.name === "members",
389
+ href: PATHS.agents,
390
+ label: words.nav.agents,
391
+ current: route.name === "agents" || route.name === "new-token",
235
392
  },
236
- ...(project?.role === "owner"
237
- ? [
238
- {
239
- href: projectHref(projectKey, "settings"),
240
- label: "Settings",
241
- current: route.name === "settings",
242
- },
243
- ]
244
- : []),
245
393
  ];
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 }));
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 }));
265
466
  }
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 }));
467
+ return _jsx(LanguageContext.Provider, { value: pack, children: content });
269
468
  }
270
469
  return {
271
470
  App,