@shivaedev/work-board 0.14.2 → 0.16.0

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 (279) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/assets/agent.js +1 -1
  4. package/assets/native.js +25 -25
  5. package/assets/react.css +1 -1
  6. package/assets/react.js +37 -36
  7. package/dist/browser/diagnostics/schema.d.ts +15 -12
  8. package/dist/browser/diagnostics/schema.d.ts.map +1 -1
  9. package/dist/browser/diagnostics/schema.js +12 -2
  10. package/dist/browser/diagnostics/schema.js.map +1 -1
  11. package/dist/browser/views/remaining.d.ts +20 -12
  12. package/dist/browser/views/remaining.d.ts.map +1 -1
  13. package/dist/client/client.d.ts +1 -0
  14. package/dist/client/client.d.ts.map +1 -1
  15. package/dist/client/client.js +1 -1
  16. package/dist/client/client.js.map +1 -1
  17. package/dist/client/document/Mermaid.d.ts.map +1 -1
  18. package/dist/client/document/Mermaid.js +4 -10
  19. package/dist/client/document/Mermaid.js.map +1 -1
  20. package/dist/client/document/View.js +1 -1
  21. package/dist/client/document/View.js.map +1 -1
  22. package/dist/client/formStatus.d.ts +12 -0
  23. package/dist/client/formStatus.d.ts.map +1 -0
  24. package/dist/client/formStatus.js +27 -0
  25. package/dist/client/formStatus.js.map +1 -0
  26. package/dist/client/react/BoardShell.d.ts.map +1 -1
  27. package/dist/client/react/BoardShell.js +36 -37
  28. package/dist/client/react/BoardShell.js.map +1 -1
  29. package/dist/client/react/DocumentContext.d.ts.map +1 -1
  30. package/dist/client/react/DocumentContext.js +12 -7
  31. package/dist/client/react/DocumentContext.js.map +1 -1
  32. package/dist/client/react/FormContext.d.ts +5 -0
  33. package/dist/client/react/FormContext.d.ts.map +1 -0
  34. package/dist/client/react/FormContext.js +10 -0
  35. package/dist/client/react/FormContext.js.map +1 -0
  36. package/dist/client/react/LiveControls.d.ts +4 -3
  37. package/dist/client/react/LiveControls.d.ts.map +1 -1
  38. package/dist/client/react/LiveControls.js +5 -24
  39. package/dist/client/react/LiveControls.js.map +1 -1
  40. package/dist/client/react/MainLocationContext.d.ts +3 -0
  41. package/dist/client/react/MainLocationContext.d.ts.map +1 -0
  42. package/dist/client/react/MainLocationContext.js +7 -0
  43. package/dist/client/react/MainLocationContext.js.map +1 -0
  44. package/dist/client/react/PhoneNavigation.d.ts +3 -1
  45. package/dist/client/react/PhoneNavigation.d.ts.map +1 -1
  46. package/dist/client/react/PhoneNavigation.js +8 -13
  47. package/dist/client/react/PhoneNavigation.js.map +1 -1
  48. package/dist/client/react/ProjectionFrame.d.ts +1 -2
  49. package/dist/client/react/ProjectionFrame.d.ts.map +1 -1
  50. package/dist/client/react/ProjectionFrame.js +10 -4
  51. package/dist/client/react/ProjectionFrame.js.map +1 -1
  52. package/dist/client/react/ReadingLibrary.d.ts.map +1 -1
  53. package/dist/client/react/ReadingLibrary.js +9 -14
  54. package/dist/client/react/ReadingLibrary.js.map +1 -1
  55. package/dist/client/react/ReadingReadyContext.d.ts +6 -0
  56. package/dist/client/react/ReadingReadyContext.d.ts.map +1 -0
  57. package/dist/client/react/ReadingReadyContext.js +7 -0
  58. package/dist/client/react/ReadingReadyContext.js.map +1 -0
  59. package/dist/client/react/SessionContext.d.ts +3 -0
  60. package/dist/client/react/SessionContext.d.ts.map +1 -0
  61. package/dist/client/react/SessionContext.js +3 -0
  62. package/dist/client/react/SessionContext.js.map +1 -0
  63. package/dist/client/react/SystemMain.d.ts.map +1 -1
  64. package/dist/client/react/SystemMain.js +3 -1
  65. package/dist/client/react/SystemMain.js.map +1 -1
  66. package/dist/client/react/SystemPage.d.ts.map +1 -1
  67. package/dist/client/react/SystemPage.js +2 -1
  68. package/dist/client/react/SystemPage.js.map +1 -1
  69. package/dist/client/react/ThemeContext.d.ts +2 -0
  70. package/dist/client/react/ThemeContext.d.ts.map +1 -0
  71. package/dist/client/react/ThemeContext.js +3 -0
  72. package/dist/client/react/ThemeContext.js.map +1 -0
  73. package/dist/client/react/TodayOptions.d.ts.map +1 -1
  74. package/dist/client/react/TodayOptions.js +23 -14
  75. package/dist/client/react/TodayOptions.js.map +1 -1
  76. package/dist/client/react/VisualViewer.d.ts.map +1 -1
  77. package/dist/client/react/VisualViewer.js +6 -5
  78. package/dist/client/react/VisualViewer.js.map +1 -1
  79. package/dist/client/react/boardRouter.d.ts +3 -2
  80. package/dist/client/react/boardRouter.d.ts.map +1 -1
  81. package/dist/client/react/boardRouter.js +2 -1
  82. package/dist/client/react/boardRouter.js.map +1 -1
  83. package/dist/client/react/createResponse.d.ts +32 -4
  84. package/dist/client/react/createResponse.d.ts.map +1 -1
  85. package/dist/client/react/createResponse.js +7 -28
  86. package/dist/client/react/createResponse.js.map +1 -1
  87. package/dist/client/react/diagnosticsValues.d.ts.map +1 -1
  88. package/dist/client/react/diagnosticsValues.js +5 -7
  89. package/dist/client/react/diagnosticsValues.js.map +1 -1
  90. package/dist/client/react/entry.d.ts.map +1 -1
  91. package/dist/client/react/entry.js +2 -34
  92. package/dist/client/react/entry.js.map +1 -1
  93. package/dist/client/react/githubLimited.d.ts +3 -0
  94. package/dist/client/react/githubLimited.d.ts.map +1 -0
  95. package/dist/client/react/githubLimited.js +4 -0
  96. package/dist/client/react/githubLimited.js.map +1 -0
  97. package/dist/client/react/loadProjection.d.ts +135 -135
  98. package/dist/client/react/loadRemaining.d.ts +20 -12
  99. package/dist/client/react/loadRemaining.d.ts.map +1 -1
  100. package/dist/client/react/mainLocation.d.ts +4 -2
  101. package/dist/client/react/mainLocation.d.ts.map +1 -1
  102. package/dist/client/react/mainLocation.js +1 -8
  103. package/dist/client/react/mainLocation.js.map +1 -1
  104. package/dist/client/react/mountBoard.d.ts +3 -0
  105. package/dist/client/react/mountBoard.d.ts.map +1 -0
  106. package/dist/client/react/mountBoard.js +45 -0
  107. package/dist/client/react/mountBoard.js.map +1 -0
  108. package/dist/client/react/navigationLocation.d.ts +3 -1
  109. package/dist/client/react/navigationLocation.d.ts.map +1 -1
  110. package/dist/client/react/navigationLocation.js +11 -1
  111. package/dist/client/react/navigationLocation.js.map +1 -1
  112. package/dist/client/react/retryPage.d.ts +2 -1
  113. package/dist/client/react/retryPage.d.ts.map +1 -1
  114. package/dist/client/react/retryPage.js +3 -4
  115. package/dist/client/react/retryPage.js.map +1 -1
  116. package/dist/client/react/scheduleReading.d.ts.map +1 -1
  117. package/dist/client/react/scheduleReading.js +1 -10
  118. package/dist/client/react/scheduleReading.js.map +1 -1
  119. package/dist/client/react/useAppearance.d.ts +2 -1
  120. package/dist/client/react/useAppearance.d.ts.map +1 -1
  121. package/dist/client/react/useAppearance.js +1 -2
  122. package/dist/client/react/useAppearance.js.map +1 -1
  123. package/dist/client/react/useBoardNavigation.d.ts +1 -1
  124. package/dist/client/react/useBoardNavigation.d.ts.map +1 -1
  125. package/dist/client/react/useBoardNavigation.js +28 -22
  126. package/dist/client/react/useBoardNavigation.js.map +1 -1
  127. package/dist/client/react/useDiagnosticsPolling.d.ts +4 -1
  128. package/dist/client/react/useDiagnosticsPolling.d.ts.map +1 -1
  129. package/dist/client/react/useDiagnosticsPolling.js +9 -7
  130. package/dist/client/react/useDiagnosticsPolling.js.map +1 -1
  131. package/dist/client/react/useDocumentPage.d.ts +1 -1
  132. package/dist/client/react/useDocumentPage.d.ts.map +1 -1
  133. package/dist/client/react/useDocumentPage.js +9 -6
  134. package/dist/client/react/useDocumentPage.js.map +1 -1
  135. package/dist/client/react/useHandoffForm.d.ts +56 -30
  136. package/dist/client/react/useHandoffForm.d.ts.map +1 -1
  137. package/dist/client/react/useHandoffForm.js +19 -30
  138. package/dist/client/react/useHandoffForm.js.map +1 -1
  139. package/dist/client/react/useResponseForm.d.ts +72 -43
  140. package/dist/client/react/useResponseForm.d.ts.map +1 -1
  141. package/dist/client/react/useResponseForm.js +20 -17
  142. package/dist/client/react/useResponseForm.js.map +1 -1
  143. package/dist/client/react/useSidebar.d.ts.map +1 -1
  144. package/dist/client/react/useSidebar.js +5 -3
  145. package/dist/client/react/useSidebar.js.map +1 -1
  146. package/dist/client/react/visualSources.d.ts.map +1 -1
  147. package/dist/client/react/visualSources.js +10 -8
  148. package/dist/client/react/visualSources.js.map +1 -1
  149. package/dist/client/saveFailure.d.ts +8 -0
  150. package/dist/client/saveFailure.d.ts.map +1 -0
  151. package/dist/client/saveFailure.js +8 -0
  152. package/dist/client/saveFailure.js.map +1 -0
  153. package/dist/diagnostics/daemonContract.d.ts +65 -38
  154. package/dist/diagnostics/daemonContract.d.ts.map +1 -1
  155. package/dist/diagnostics/daemonContract.js +2 -1
  156. package/dist/diagnostics/daemonContract.js.map +1 -1
  157. package/dist/diagnostics/health.d.ts +10 -0
  158. package/dist/diagnostics/health.d.ts.map +1 -1
  159. package/dist/diagnostics/health.js +2 -0
  160. package/dist/diagnostics/health.js.map +1 -1
  161. package/dist/diagnostics/source.d.ts +55 -36
  162. package/dist/diagnostics/source.d.ts.map +1 -1
  163. package/dist/diagnostics/source.js +1 -2
  164. package/dist/diagnostics/source.js.map +1 -1
  165. package/dist/render/{failed.d.ts → Failed.d.ts} +1 -1
  166. package/dist/render/Failed.d.ts.map +1 -0
  167. package/dist/render/{failed.js → Failed.js} +1 -1
  168. package/dist/render/Failed.js.map +1 -0
  169. package/dist/render/code.d.ts +2 -2
  170. package/dist/render/code.d.ts.map +1 -1
  171. package/dist/render/code.js +15 -13
  172. package/dist/render/code.js.map +1 -1
  173. package/dist/render/footnoteIds.d.ts +3 -0
  174. package/dist/render/footnoteIds.d.ts.map +1 -0
  175. package/dist/render/{footnotes.js → footnoteIds.js} +11 -9
  176. package/dist/render/footnoteIds.js.map +1 -0
  177. package/dist/rpc/contract.d.ts +2346 -2322
  178. package/dist/rpc/contract.d.ts.map +1 -1
  179. package/dist/rpc/handoffContract.d.ts +33 -33
  180. package/dist/rpc/responseContract.d.ts +56 -56
  181. package/dist/server/board.d.ts +1 -1
  182. package/dist/server/diagnostics/daemon.d.ts +41 -25
  183. package/dist/server/diagnostics/daemon.d.ts.map +1 -1
  184. package/dist/server/diagnostics/fleetHealth.d.ts +10 -0
  185. package/dist/server/diagnostics/fleetHealth.d.ts.map +1 -1
  186. package/dist/server/diagnostics/githubLimits.d.ts +12 -13
  187. package/dist/server/diagnostics/githubLimits.d.ts.map +1 -1
  188. package/dist/server/diagnostics/githubLimits.js +6 -6
  189. package/dist/server/diagnostics/githubLimits.js.map +1 -1
  190. package/dist/server/integration/diagnostics.d.ts +74 -38
  191. package/dist/server/integration/diagnostics.d.ts.map +1 -1
  192. package/dist/server/mcp/client.d.ts +1155 -1147
  193. package/dist/server/mcp/client.d.ts.map +1 -1
  194. package/dist/server/mcp/queries.d.ts +60 -36
  195. package/dist/server/mcp/queries.d.ts.map +1 -1
  196. package/dist/server/metadata/fields.js +1 -1
  197. package/dist/server/metadata/fields.js.map +1 -1
  198. package/dist/server/packageInfo.d.ts +1 -1
  199. package/dist/server/packageInfo.js +1 -1
  200. package/dist/server/render/Highlighter.d.ts +1 -1
  201. package/dist/server/render/Highlighter.js +1 -1
  202. package/dist/server/render/markdown.d.ts +1 -1
  203. package/dist/server/render/markdown.js +2 -2
  204. package/dist/server/render/markdown.js.map +1 -1
  205. package/dist/server/serve.d.ts +1 -1
  206. package/dist/server/views/remainingQueries.d.ts +20 -12
  207. package/dist/server/views/remainingQueries.d.ts.map +1 -1
  208. package/package.json +9 -39
  209. package/src/browser/diagnostics/schema.ts +12 -2
  210. package/src/client/client.ts +2 -1
  211. package/src/client/document/Mermaid.ts +4 -10
  212. package/src/client/document/View.ts +1 -1
  213. package/src/client/formStatus.ts +35 -0
  214. package/src/client/react/BoardShell.tsx +49 -43
  215. package/src/client/react/DocumentContext.tsx +12 -7
  216. package/src/client/react/FormContext.ts +13 -0
  217. package/src/client/react/LiveControls.tsx +5 -38
  218. package/src/client/react/MainLocationContext.ts +8 -0
  219. package/src/client/react/PhoneNavigation.tsx +20 -15
  220. package/src/client/react/ProjectionFrame.tsx +13 -5
  221. package/src/client/react/ReadingLibrary.tsx +9 -14
  222. package/src/client/react/ReadingReadyContext.ts +11 -0
  223. package/src/client/react/SessionContext.ts +4 -0
  224. package/src/client/react/SystemMain.tsx +3 -1
  225. package/src/client/react/SystemPage.tsx +2 -1
  226. package/src/client/react/ThemeContext.ts +3 -0
  227. package/src/client/react/TodayOptions.tsx +40 -33
  228. package/src/client/react/VisualViewer.tsx +6 -6
  229. package/src/client/react/boardRouter.ts +3 -2
  230. package/src/client/react/createResponse.ts +10 -35
  231. package/src/client/react/diagnosticsValues.tsx +20 -12
  232. package/src/client/react/entry.tsx +2 -37
  233. package/src/client/react/githubLimited.ts +5 -0
  234. package/src/client/react/mainLocation.ts +5 -8
  235. package/src/client/react/mountBoard.tsx +49 -0
  236. package/src/client/react/navigationLocation.ts +14 -2
  237. package/src/client/react/retryPage.ts +4 -4
  238. package/src/client/react/scheduleReading.ts +1 -10
  239. package/src/client/react/useAppearance.ts +2 -3
  240. package/src/client/react/useBoardNavigation.ts +30 -23
  241. package/src/client/react/useDiagnosticsPolling.ts +14 -7
  242. package/src/client/react/useDocumentPage.ts +12 -6
  243. package/src/client/react/useHandoffForm.ts +25 -33
  244. package/src/client/react/useResponseForm.ts +20 -18
  245. package/src/client/react/useSidebar.ts +5 -3
  246. package/src/client/react/visualSources.ts +10 -8
  247. package/src/client/saveFailure.ts +14 -0
  248. package/src/diagnostics/daemonContract.ts +2 -1
  249. package/src/diagnostics/health.ts +2 -0
  250. package/src/diagnostics/source.ts +1 -2
  251. package/src/render/code.ts +3 -4
  252. package/src/render/{footnotes.ts → footnoteIds.ts} +12 -10
  253. package/src/server/board.ts +1 -1
  254. package/src/server/diagnostics/githubLimits.ts +9 -11
  255. package/src/server/metadata/fields.ts +1 -1
  256. package/src/server/packageInfo.ts +1 -1
  257. package/src/server/render/Highlighter.ts +1 -1
  258. package/src/server/render/markdown.ts +2 -2
  259. package/dist/client/handoffs/failureStatus.d.ts +0 -2
  260. package/dist/client/handoffs/failureStatus.d.ts.map +0 -1
  261. package/dist/client/handoffs/failureStatus.js +0 -7
  262. package/dist/client/handoffs/failureStatus.js.map +0 -1
  263. package/dist/client/responses/failure.d.ts +0 -5
  264. package/dist/client/responses/failure.d.ts.map +0 -1
  265. package/dist/client/responses/failure.js +0 -13
  266. package/dist/client/responses/failure.js.map +0 -1
  267. package/dist/render/failed.d.ts.map +0 -1
  268. package/dist/render/failed.js.map +0 -1
  269. package/dist/render/footnotes.d.ts +0 -3
  270. package/dist/render/footnotes.d.ts.map +0 -1
  271. package/dist/render/footnotes.js.map +0 -1
  272. package/dist/server/diagnostics/readCommand.d.ts +0 -4
  273. package/dist/server/diagnostics/readCommand.d.ts.map +0 -1
  274. package/dist/server/diagnostics/readCommand.js +0 -20
  275. package/dist/server/diagnostics/readCommand.js.map +0 -1
  276. package/src/client/handoffs/failureStatus.ts +0 -6
  277. package/src/client/responses/failure.ts +0 -13
  278. package/src/server/diagnostics/readCommand.ts +0 -24
  279. /package/src/render/{failed.ts → Failed.ts} +0 -0
@@ -1,21 +1,26 @@
1
1
  import { useRouterState } from "@tanstack/react-router";
2
2
  import { Option } from "effect";
3
3
  import type * as AtomRegistry from "effect/reactivity/AtomRegistry";
4
- import { useCallback, useEffect, useState } from "react";
4
+ import { useCallback, useContext, useEffect, useState } from "react";
5
5
  import { useQuery } from "@shivaedev/effect-react/result-state.ts";
6
6
  import type { BrowserClient } from "#client/client.ts";
7
7
  import { navigationQuery } from "#client/page/routeQuery.ts";
8
8
  import { AgeTicker } from "./Age.tsx";
9
9
  import { FleetMain } from "./FleetMain.tsx";
10
+ import { FormContext } from "./FormContext.ts";
10
11
  import { Header } from "./Header.tsx";
11
12
  import { LiveControls } from "./LiveControls.tsx";
12
- import { displayedLocation, type MainLocation, sameMainLocation } from "./mainLocation.ts";
13
+ import { MainLocationContext, useMainLocation } from "./MainLocationContext.ts";
14
+ import { emptyMainLocation, type MainLocation } from "./mainLocation.ts";
13
15
  import { PhoneNavigation } from "./PhoneNavigation.tsx";
14
16
  import { ReadingLibrary } from "./ReadingLibrary.tsx";
17
+ import { ReadingReadyContext, readingReady } from "./ReadingReadyContext.ts";
15
18
  import { SearchDialog } from "./SearchDialog.tsx";
19
+ import { SessionContext } from "./SessionContext.ts";
16
20
  import type { ShellApi } from "./ShellApi.ts";
17
21
  import { Sidebar } from "./Sidebar.tsx";
18
22
  import { shellIds } from "./shellIds.ts";
23
+ import { ThemeContext } from "./ThemeContext.ts";
19
24
  import { UpdatesToggle } from "./UpdatesToggle.tsx";
20
25
  import { useAppearance } from "./useAppearance.ts";
21
26
  import { useBoardNavigation } from "./useBoardNavigation.ts";
@@ -30,61 +35,61 @@ export interface ShellOptions {
30
35
  readonly registry: AtomRegistry.AtomRegistry;
31
36
  }
32
37
  export function BoardShell(options: ShellOptions) {
33
- const { api, ready, client } = options;
38
+ const [main, onPage] = useState(emptyMainLocation);
34
39
  const appearance = useAppearance();
40
+ const [element, setElement] = useState<HTMLElement | null>(null);
41
+ const report = useCallback((next: HTMLElement, ready: boolean) => {
42
+ setElement((current) => {
43
+ if (ready) {
44
+ return next;
45
+ }
46
+ return current === next ? null : current;
47
+ });
48
+ }, []);
49
+ return (
50
+ <MainLocationContext.Provider value={main}>
51
+ <ReadingReadyContext.Provider value={{ element, report }}>
52
+ <SessionContext.Provider value={options.client}>
53
+ <ThemeContext.Provider value={appearance.scheme}>
54
+ <BoardWorkspace {...options} appearance={appearance} onPage={onPage} />
55
+ </ThemeContext.Provider>
56
+ </SessionContext.Provider>
57
+ </ReadingReadyContext.Provider>
58
+ </MainLocationContext.Provider>
59
+ );
60
+ }
61
+ function BoardWorkspace(
62
+ options: ShellOptions & { readonly appearance: ReturnType<typeof useAppearance>; readonly onPage: (location: MainLocation) => void },
63
+ ) {
64
+ const { api, ready, client, onPage, appearance } = options;
65
+ const main = useMainLocation();
35
66
  const sidebar = useSidebar(appearance);
36
- const status = useBoardNavigation(options);
67
+ const [phoneOpen, setPhoneOpen] = useState(false);
68
+ const closeCurrentDestination = useCallback(() => {
69
+ sidebar.close();
70
+ setPhoneOpen(false);
71
+ }, [sidebar.close]);
72
+ const status = useBoardNavigation(options, closeCurrentDestination);
37
73
  const search = useSearchDialog();
38
74
  const [libraryOpen, setLibraryOpen] = useState(false);
39
75
  const location = useRouterState({ select: (state) => state.location });
40
- const [main, setMain] = useState<MainLocation>({ file: "", view: "" });
41
76
  const query = useQuery(navigationQuery(api), { keepPreviousData: true });
42
77
  const data = Option.getOrUndefined(query.data);
43
- const onPage = useCallback((next: MainLocation) => setMain((previous) => (sameMainLocation(previous, next) ? previous : next)), []);
78
+ const { element } = useContext(ReadingReadyContext);
44
79
  useEffect(() => {
45
- function start() {
46
- const doc = document.getElementById("doc");
47
- const context = doc?.querySelector<HTMLElement>(".item-context");
48
- if (data && doc?.dataset.route && (!context || context.dataset.readingReady === "true")) {
49
- ready?.();
50
- }
80
+ const doc = document.getElementById("doc");
81
+ if (data && main.route && doc && readingReady(doc, element)) {
82
+ ready?.();
51
83
  }
52
- start();
53
- document.addEventListener("board-page", start);
54
- document.addEventListener("board-reading-ready", start);
55
- return () => {
56
- document.removeEventListener("board-page", start);
57
- document.removeEventListener("board-reading-ready", start);
58
- };
59
- }, [ready, data]);
60
- useEffect(() => {
61
- function sync() {
62
- const doc = document.getElementById("doc");
63
- if (doc) {
64
- onPage(displayedLocation(doc));
65
- }
66
- }
67
- document.addEventListener("board-page", sync);
68
- return () => document.removeEventListener("board-page", sync);
69
- }, [onPage]);
70
- useEffect(() => {
71
- if (data) {
72
- document.dispatchEvent(new Event("board-index-change"));
73
- }
74
- }, [data]);
84
+ }, [ready, data, main, element]);
75
85
  const live = client ? (
76
- <LiveControls
77
- client={client}
78
- current={location.pathname + location.searchStr}
79
- navigationFailed={Option.isSome(query.cause)}
80
- navigationPending={query.pending}
81
- />
86
+ <LiveControls client={client} navigationFailed={Option.isSome(query.cause)} navigationPending={query.pending} />
82
87
  ) : (
83
88
  <span className="font-mono text-muted-foreground text-xs" id={shellIds.live} role="status">
84
89
  connecting
85
90
  </span>
86
91
  );
87
- return (
92
+ const content = (
88
93
  <AgeTicker>
89
94
  <ReadingLibrary api={api} close={() => setLibraryOpen(false)} open={libraryOpen}>
90
95
  <a className="sr-only rounded-md bg-background p-3 focus:not-sr-only focus:absolute focus:z-50" href="#doc">
@@ -117,7 +122,7 @@ export function BoardShell(options: ShellOptions) {
117
122
  updates={client ? <UpdatesToggle client={client} /> : null}
118
123
  />
119
124
  <FleetMain api={api} home={data?.home} onPage={onPage} url={location.pathname + location.searchStr} />
120
- <PhoneNavigation main={main} openSearch={search.show} />
125
+ <PhoneNavigation main={main} more={phoneOpen} onMoreChange={setPhoneOpen} openSearch={search.show} />
121
126
  </div>
122
127
  </div>
123
128
  {Option.isSome(query.cause) && <p role="status">Navigation refresh failed. Your displayed files are retained.</p>}
@@ -133,4 +138,5 @@ export function BoardShell(options: ShellOptions) {
133
138
  </ReadingLibrary>
134
139
  </AgeTicker>
135
140
  );
141
+ return client ? <FormContext.Provider value={client}>{content}</FormContext.Provider> : content;
136
142
  }
@@ -1,15 +1,22 @@
1
- import { useEffect, useLayoutEffect, useRef, useState } from "react";
1
+ import { useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import type { Document } from "#browser/document/schema.ts";
3
3
  import { Tabs } from "#client/react/components/ui/Tabs.tsx";
4
4
  import { Age } from "./Age.tsx";
5
5
  import { DocumentMetadata } from "./DocumentMetadata.tsx";
6
6
  import { DocumentSource } from "./DocumentSource.tsx";
7
+ import { useMainLocation } from "./MainLocationContext.ts";
8
+ import { ReadingReadyContext } from "./ReadingReadyContext.ts";
7
9
 
8
10
  export function DocumentContext({ document: data }: { readonly document: Document }) {
11
+ const page = useMainLocation();
12
+ const { report } = useContext(ReadingReadyContext);
9
13
  const [headings, setHeadings] = useState<readonly { readonly id: string; readonly title: string; readonly depth: number }[]>([]);
10
14
  const contextRef = useRef<HTMLElement>(null);
11
15
  const [tab, setTab] = useState("details");
12
16
  useEffect(() => {
17
+ if (page.route && page.projection === "document" && page.file !== data.file) {
18
+ return;
19
+ }
13
20
  function outline() {
14
21
  const entries = [
15
22
  ...(contextRef.current
@@ -21,10 +28,8 @@ export function DocumentContext({ document: data }: { readonly document: Documen
21
28
  .map((heading) => ({ depth: Number(heading.tagName.slice(1)), id: heading.id, title: heading.textContent?.replace(/#$/u, "") ?? "" }));
22
29
  setHeadings((current) => (JSON.stringify(current) === JSON.stringify(entries) ? current : entries));
23
30
  }
24
- document.addEventListener("board-page", outline);
25
31
  outline();
26
- return () => document.removeEventListener("board-page", outline);
27
- }, []);
32
+ }, [page, data]);
28
33
 
29
34
  useLayoutEffect(() => {
30
35
  const context = contextRef.current;
@@ -39,9 +44,9 @@ export function DocumentContext({ document: data }: { readonly document: Documen
39
44
  if (count !== headings.length) {
40
45
  return;
41
46
  }
42
- context.dataset.readingReady = "true";
43
- document.dispatchEvent(new Event("board-reading-ready"));
44
- }, [headings]);
47
+ report(context, true);
48
+ return () => report(context, false);
49
+ }, [headings, report]);
45
50
  const tabs = [
46
51
  { content: <Outline headings={headings} />, label: "Details", value: "details" },
47
52
  { content: <SourcePanel document={data} />, label: "Source", value: "source" },
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { BrowserClient } from "#client/client.ts";
3
+
4
+ export type FormClient = Pick<BrowserClient, "handoffs" | "responses" | "runtime">;
5
+ export const FormContext = createContext<FormClient | undefined>(undefined);
6
+
7
+ export function useFormClient() {
8
+ const client = useContext(FormContext);
9
+ if (!client) {
10
+ throw new Error("Board forms require the shell's RPC client");
11
+ }
12
+ return client;
13
+ }
@@ -1,26 +1,21 @@
1
1
  import { useAtomValue } from "@effect/atom-react";
2
2
  import { Option } from "effect";
3
3
  import * as Atom from "effect/reactivity/Atom";
4
- import { useEffect, useMemo, useState } from "react";
4
+ import { useMemo } from "react";
5
5
  import { useQuery } from "@shivaedev/effect-react/result-state.ts";
6
6
  import type { BrowserClient } from "#client/client.ts";
7
7
  import { activeRead } from "./activeRead.ts";
8
8
  import { liveStatus } from "./liveStatus.ts";
9
+ import { useMainLocation } from "./MainLocationContext.ts";
9
10
  import { shellIds } from "./shellIds.ts";
10
11
 
11
12
  const opening = Atom.make({ failed: false, pending: true });
12
- interface Page {
13
- readonly kind: string;
14
- readonly url: string;
15
- }
16
13
  export function LiveControls({
17
14
  client,
18
- current,
19
15
  navigationPending,
20
16
  navigationFailed,
21
17
  }: {
22
- readonly client: BrowserClient;
23
- readonly current: string;
18
+ readonly client: Pick<BrowserClient, "api" | "registry"> & { readonly updates: Pick<BrowserClient["updates"], "paused" | "status"> };
24
19
  readonly navigationPending: boolean;
25
20
  readonly navigationFailed: boolean;
26
21
  }) {
@@ -28,23 +23,8 @@ export function LiveControls({
28
23
  const paused = useAtomValue(client.updates.paused);
29
24
  const watcher = useQuery(client.api.watcher.query());
30
25
  const watching = Option.getOrUndefined(watcher.data)?.watching === true;
31
- const [page, setPage] = useState<Page>();
32
- useEffect(() => {
33
- function sync() {
34
- const doc = document.getElementById("doc");
35
- if (doc?.dataset.route !== current) {
36
- return;
37
- }
38
- setPage({
39
- kind: doc?.dataset.projection ?? "document",
40
- url: current,
41
- });
42
- }
43
- sync();
44
- document.addEventListener("board-page", sync);
45
- return () => document.removeEventListener("board-page", sync);
46
- }, [current]);
47
- const atom = useMemo(() => (page ? activeRead(client.api, page.kind, page.url) : opening), [client, page]);
26
+ const page = useMainLocation();
27
+ const atom = useMemo(() => (page.route ? activeRead(client.api, page.projection, page.route) : opening), [client, page.projection, page.route]);
48
28
  const reading = useAtomValue(atom);
49
29
  const displayed = liveStatus({
50
30
  connection: status.connection,
@@ -55,19 +35,6 @@ export function LiveControls({
55
35
  waiting: navigationPending || reading.pending,
56
36
  watching,
57
37
  });
58
- useEffect(
59
- () =>
60
- client.registry.subscribe(
61
- client.updates.paused,
62
- (value) => {
63
- if (!value) {
64
- document.dispatchEvent(new Event("board-updates-resumed"));
65
- }
66
- },
67
- { immediate: true },
68
- ),
69
- [client],
70
- );
71
38
  return (
72
39
  <span
73
40
  className="font-mono text-muted-foreground text-xs data-[state=down]:text-destructive data-[state=stale]:text-destructive"
@@ -0,0 +1,8 @@
1
+ import { createContext, useContext } from "react";
2
+ import { emptyMainLocation } from "./mainLocation.ts";
3
+
4
+ export const MainLocationContext = createContext(emptyMainLocation);
5
+
6
+ export function useMainLocation() {
7
+ return useContext(MainLocationContext);
8
+ }
@@ -1,5 +1,6 @@
1
+ import { useRouter } from "@tanstack/react-router";
1
2
  import { CalendarDays, Ellipsis, Flag, Search, Zap } from "lucide-react";
2
- import { useEffect, useState } from "react";
3
+ import { useEffect } from "react";
3
4
  import { Button } from "#client/react/components/ui/Button.tsx";
4
5
  import { Modal } from "./Modal.tsx";
5
6
  import type { MainLocation } from "./mainLocation.ts";
@@ -11,21 +12,25 @@ const icons = [CalendarDays, Flag, Zap];
11
12
  const TAB_CLASS =
12
13
  "flex min-h-12 min-w-0 flex-col items-center justify-center gap-1 rounded-md border-0 bg-transparent p-0 text-foreground text-xs no-underline aria-[current=page]:bg-accent aria-[current=page]:text-primary";
13
14
 
14
- export function PhoneNavigation({ main, openSearch }: { readonly main: MainLocation; readonly openSearch: () => void }) {
15
- const [more, setMore] = useState(false);
15
+ export function PhoneNavigation({
16
+ main,
17
+ more,
18
+ onMoreChange,
19
+ openSearch,
20
+ }: {
21
+ readonly main: MainLocation;
22
+ readonly more: boolean;
23
+ readonly onMoreChange: (open: boolean) => void;
24
+ readonly openSearch: () => void;
25
+ }) {
26
+ const router = useRouter();
16
27
  const narrow = useMedia("(max-width: 767px)");
17
28
  useEffect(() => {
18
29
  if (!narrow) {
19
- setMore(false);
30
+ onMoreChange(false);
20
31
  }
21
- }, [narrow]);
22
- useEffect(() => {
23
- function close() {
24
- setMore(false);
25
- }
26
- document.addEventListener("board-navigate", close);
27
- return () => document.removeEventListener("board-navigate", close);
28
- }, []);
32
+ }, [narrow, onMoreChange]);
33
+ useEffect(() => router.subscribe("onBeforeNavigate", () => onMoreChange(false)), [router, onMoreChange]);
29
34
  return (
30
35
  <>
31
36
  <nav
@@ -50,19 +55,19 @@ export function PhoneNavigation({ main, openSearch }: { readonly main: MainLocat
50
55
  aria-expanded={more}
51
56
  aria-haspopup="dialog"
52
57
  className={TAB_CLASS}
53
- onClick={() => setMore(true)}
58
+ onClick={() => onMoreChange(true)}
54
59
  type="button"
55
60
  >
56
61
  <Ellipsis aria-hidden="true" className="size-5" />
57
62
  More
58
63
  </button>
59
64
  </nav>
60
- <Modal className="phone-more" close={() => setMore(false)} id={shellIds.phoneMore} open={more} title={shellIds.phoneMoreTitle}>
65
+ <Modal className="phone-more" close={() => onMoreChange(false)} id={shellIds.phoneMore} open={more} title={shellIds.phoneMoreTitle}>
61
66
  <div className="mb-3 flex items-center justify-between">
62
67
  <h2 className="font-semibold" id={shellIds.phoneMoreTitle}>
63
68
  More
64
69
  </h2>
65
- <Button onClick={() => setMore(false)} variant="ghost">
70
+ <Button onClick={() => onMoreChange(false)} variant="ghost">
66
71
  Close
67
72
  </Button>
68
73
  </div>
@@ -21,7 +21,7 @@ export function ProjectionFrame<A, E>({
21
21
  url,
22
22
  }: {
23
23
  readonly atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>;
24
- readonly location: (data: A) => MainLocation & { readonly identity?: string | undefined; readonly status: number };
24
+ readonly location: (data: A) => Omit<MainLocation, "route" | "projection"> & { readonly identity?: string | undefined };
25
25
  readonly onPage: (location: MainLocation) => void;
26
26
  readonly render: (data: A) => ReactNode;
27
27
  readonly source?: (data: A) => string | undefined;
@@ -36,7 +36,7 @@ export function ProjectionFrame<A, E>({
36
36
  const failed = Option.isSome(query.cause);
37
37
  useLayoutEffect(() => {
38
38
  const element = hostRef.current;
39
- if (element && element.dataset.route !== url) {
39
+ if (element) {
40
40
  delete element.dataset.route;
41
41
  }
42
42
  if (!element || query.pending || (data === undefined && !failed)) {
@@ -56,8 +56,16 @@ export function ProjectionFrame<A, E>({
56
56
  }
57
57
  renderedRef.current = projectionChanges(mainElement, renderedRef.current, url);
58
58
  mainElement.dataset.route = url;
59
- flushSync(() => onPage({ file: main.file, title: mainElement.querySelector("h1")?.textContent?.trim(), view: main.view }));
60
- document.dispatchEvent(new Event("board-page"));
59
+ flushSync(() =>
60
+ onPage({
61
+ file: main.file,
62
+ projection: route,
63
+ route: url,
64
+ status: main.status,
65
+ title: mainElement.querySelector("h1")?.textContent?.trim(),
66
+ view: main.view,
67
+ }),
68
+ );
61
69
  });
62
70
  }
63
71
  const observer = new MutationObserver((records) => {
@@ -78,7 +86,7 @@ export function ProjectionFrame<A, E>({
78
86
  active = false;
79
87
  observer.disconnect();
80
88
  };
81
- }, [data, failed, query.pending, expectedSource, main.file, main.view, onPage, url]);
89
+ }, [data, failed, query.pending, expectedSource, main.file, main.status, main.view, onPage, route, url]);
82
90
  return (
83
91
  <>
84
92
  <main
@@ -1,5 +1,6 @@
1
- import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
1
+ import { type ReactNode, useCallback, useLayoutEffect, useRef, useState } from "react";
2
2
  import { LibraryContext, restoreLibrary } from "#client/react/LibraryContext.ts";
3
+ import { useMainLocation } from "./MainLocationContext.ts";
3
4
  import { Modal } from "./Modal.tsx";
4
5
  import { ReadingHistory } from "./ReadingHistory.tsx";
5
6
  import type { ShellApi } from "./ShellApi.ts";
@@ -16,6 +17,7 @@ export function ReadingLibrary({
16
17
  readonly close: () => void;
17
18
  readonly children: ReactNode;
18
19
  }) {
20
+ const page = useMainLocation();
19
21
  const key = `work-board:library:${document.documentElement.dataset.workspace}`;
20
22
  const [saved, setSaved] = useState(() => restoreLibrary(key));
21
23
  const [current, setCurrent] = useState("");
@@ -34,18 +36,17 @@ export function ReadingLibrary({
34
36
  },
35
37
  [key],
36
38
  );
37
- useEffect(() => {
39
+ useLayoutEffect(() => {
38
40
  function sync() {
39
- const doc = document.getElementById("doc");
40
- if (!doc?.dataset.route) {
41
+ if (!page.route) {
41
42
  return;
42
43
  }
43
- const file = doc.dataset.file ?? "";
44
- const title = doc.querySelector("h1")?.textContent?.trim() || file;
44
+ const file = page.file;
45
+ const title = page.title || file;
45
46
  document.title = title ? `${title} · Work Board` : "Work Board";
46
47
 
47
48
  setCurrent(file);
48
- if (file && doc.querySelector("#favorite-toggle") && visitedRef.current !== `${file}:${title}`) {
49
+ if (file && page.view === "" && visitedRef.current !== `${file}:${title}`) {
49
50
  visitedRef.current = `${file}:${title}`;
50
51
  save({
51
52
  favorites: savedRef.current.favorites,
@@ -54,13 +55,7 @@ export function ReadingLibrary({
54
55
  }
55
56
  }
56
57
  sync();
57
- document.addEventListener("board-page", sync);
58
- document.addEventListener("board-index-change", sync);
59
- return () => {
60
- document.removeEventListener("board-page", sync);
61
- document.removeEventListener("board-index-change", sync);
62
- };
63
- }, [save]);
58
+ }, [save, page]);
64
59
 
65
60
  return (
66
61
  <LibraryContext.Provider
@@ -0,0 +1,11 @@
1
+ import { createContext } from "react";
2
+
3
+ export const ReadingReadyContext = createContext<{
4
+ readonly element: HTMLElement | null;
5
+ readonly report: (element: HTMLElement, ready: boolean) => void;
6
+ }>({ element: null, report: () => undefined });
7
+
8
+ export function readingReady(doc: HTMLElement, element: HTMLElement | null) {
9
+ const context = doc.querySelector(".item-context");
10
+ return !context || context === element;
11
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext } from "react";
2
+ import type { BrowserClient } from "#client/client.ts";
3
+
4
+ export const SessionContext = createContext<BrowserClient | undefined>(undefined);
@@ -1,6 +1,8 @@
1
+ import { useContext } from "react";
1
2
  import { routeQueryOptions } from "#client/page/routeQuery.ts";
2
3
  import type { MainLocation } from "./mainLocation.ts";
3
4
  import { ProjectionFrame } from "./ProjectionFrame.tsx";
5
+ import { SessionContext } from "./SessionContext.ts";
4
6
  import type { ShellApi } from "./ShellApi.ts";
5
7
  import { SystemPage } from "./SystemPage.tsx";
6
8
  import { useDiagnosticsPolling } from "./useDiagnosticsPolling.ts";
@@ -15,7 +17,7 @@ export function SystemMain({
15
17
  readonly url: string;
16
18
  }) {
17
19
  const atom = api.diagnostics.query(undefined, routeQueryOptions);
18
- useDiagnosticsPolling(atom, api.fleetItems.query(undefined, routeQueryOptions));
20
+ useDiagnosticsPolling(useContext(SessionContext), atom, api.fleetItems.query(undefined, routeQueryOptions));
19
21
  return (
20
22
  <ProjectionFrame
21
23
  atom={atom}
@@ -4,6 +4,7 @@ import type { DiagnosticsProjection } from "#browser/views/remaining.ts";
4
4
  import { Card as Surface } from "#client/react/components/ui/Card.tsx";
5
5
  import { DiagnosticsGithub, DiagnosticsWindows, ObservedTime } from "./diagnosticsValues.tsx";
6
6
  import { FleetItemsContext } from "./FleetItemsContext.ts";
7
+ import { githubLimited } from "./githubLimited.ts";
7
8
  import { SystemFleetLane } from "./SystemFleetLane.tsx";
8
9
  import { SystemHealth } from "./SystemHealth.tsx";
9
10
  export function SystemPage({ data: { sources: data, now } }: { readonly data: DiagnosticsProjection }) {
@@ -24,7 +25,7 @@ export function SystemPage({ data: { sources: data, now } }: { readonly data: Di
24
25
  source={data.codex}
25
26
  />
26
27
  <Card
27
- attention={data.github.state === "ready" && [data.github.value.core, data.github.value.graphql].some((window) => window.remaining === 0)}
28
+ attention={data.github.state === "ready" && data.github.value.some((window) => githubLimited(window, now))}
28
29
  name="GitHub"
29
30
  render={(value) => <DiagnosticsGithub now={now} value={value} />}
30
31
  source={data.github}
@@ -0,0 +1,3 @@
1
+ import { createContext } from "react";
2
+
3
+ export const ThemeContext = createContext("light");
@@ -1,6 +1,5 @@
1
1
  import { RegistryContext } from "@effect/atom-react";
2
- import { Effect, Option } from "effect";
3
- import * as Atom from "effect/reactivity/Atom";
2
+ import { Cause, Effect, Option } from "effect";
4
3
  import { useContext, useMemo, useState } from "react";
5
4
  import { useAction, useQuery } from "@shivaedev/effect-react/result-state.ts";
6
5
  import type { Document, DocumentRequest } from "#browser/document/schema.ts";
@@ -11,8 +10,10 @@ import { documentQuery, navigationQuery, routeQueryOptions } from "#client/page/
11
10
  import { Button } from "#client/react/components/ui/Button.tsx";
12
11
  import { Popover } from "#client/react/components/ui/Popover.tsx";
13
12
  import { authorLabel } from "#client/responses/authorLabel.ts";
13
+ import { saveFailure } from "#client/saveFailure.ts";
14
14
  import { fileUrl } from "#files/url.ts";
15
15
  import { createResponse } from "./createResponse.ts";
16
+ import { useFormClient } from "./FormContext.ts";
16
17
  import type { ShellApi } from "./ShellApi.ts";
17
18
 
18
19
  export function TodayOptions({ api, item }: { readonly api: ShellApi; readonly item: InboxProjection["you"][number] }) {
@@ -42,42 +43,48 @@ export function TodayOptions({ api, item }: { readonly api: ShellApi; readonly i
42
43
 
43
44
  function InlineOptions({ api, document: data, request }: { readonly api: ShellApi; readonly document: Document; readonly request: DocumentRequest }) {
44
45
  const registry = useContext(RegistryContext);
45
- const [status, setStatus] = useState("");
46
- const [outcome, setOutcome] = useState("");
46
+ const client = useFormClient();
47
47
  const [label] = useState(() => authorLabel(() => localStorage, document.documentElement.dataset.workspace));
48
48
  const [author, setAuthor] = useState(() => label.read("") || "maintainer");
49
49
  const actionAtom = useMemo(
50
50
  () =>
51
- Atom.fn<string>()((choice) => {
52
- const preview = request.preview;
53
- const prompt = request.prompts?.[0];
54
- if (!(preview && prompt && author.trim())) {
55
- return Effect.fail("Add your local author label.");
56
- }
57
- const answers = [{ prompt: prompt.id, selected: [choice], text: "" }];
58
- const submission = {
59
- answers,
60
- author: author.trim(),
61
- body: answerBody([prompt], answers, ""),
62
- id: `response.${crypto.randomUUID()}`,
63
- question: preview.id,
64
- revision: preview.reviewedRevision,
65
- type: "answer" as const,
66
- };
67
- return createResponse(preview, submission, () => submission, setOutcome, setStatus).pipe(
68
- Effect.tap(() =>
69
- Effect.sync(() => {
70
- label.save(author);
71
- registry.refresh(api.inbox.query(undefined, routeQueryOptions));
72
- registry.refresh(navigationQuery(api));
73
- registry.refresh(documentQuery(api, fileUrl(data.file)));
74
- }),
75
- ),
76
- );
77
- }),
78
- [api, registry, request, author, data.file, label.save],
51
+ client.runtime.fn(
52
+ Effect.fn("Board.answerOption")(function* (choice: string) {
53
+ const preview = request.preview;
54
+ const prompt = request.prompts?.[0];
55
+ if (!(preview && prompt && author.trim())) {
56
+ return yield* Effect.fail("Add your local author label.");
57
+ }
58
+ const answers = [{ prompt: prompt.id, selected: [choice], text: "" }];
59
+ const submission = {
60
+ answers,
61
+ author: author.trim(),
62
+ body: answerBody([prompt], answers, ""),
63
+ id: `response.${crypto.randomUUID()}`,
64
+ question: preview.id,
65
+ revision: preview.reviewedRevision,
66
+ type: "answer" as const,
67
+ };
68
+ const saved = yield* createResponse(client, preview, submission);
69
+ label.save(author);
70
+ registry.refresh(api.inbox.query(undefined, routeQueryOptions));
71
+ registry.refresh(navigationQuery(api));
72
+ registry.refresh(documentQuery(api, fileUrl(data.file)));
73
+ return saved;
74
+ }),
75
+ ),
76
+ [client, api, registry, request, author, data.file, label.save],
79
77
  );
80
78
  const action = useAction(actionAtom);
79
+ const failure = Option.getOrUndefined(Option.flatMap(action.cause, Cause.findErrorOption));
80
+ const saved = Option.getOrUndefined(action.data);
81
+ let status = saved ? `Saved: responses/${saved.id}.md. This records feedback, not acceptance.` : "";
82
+ if (failure) {
83
+ status = typeof failure === "string" ? failure : saveFailure(failure, "response").text;
84
+ }
85
+ if (action.pending) {
86
+ status = "Pending: registering reviewed context, then recording response…";
87
+ }
81
88
  const prompt = request.prompts?.[0];
82
89
  return (
83
90
  <fieldset aria-label={`Options for ${data.identity}`} className="m-0 min-w-0 space-y-2 border-0 px-2 pb-2">
@@ -86,7 +93,7 @@ function InlineOptions({ api, document: data, request }: { readonly api: ShellAp
86
93
  const text = data.metadata.options?.[index]?.label ?? optionLabel(option.id, option.source, request.preview?.reason ?? "");
87
94
  return (
88
95
  <Button
89
- disabled={!(data.writable && author.trim()) || action.pending || outcome === "pending" || outcome === "saved"}
96
+ disabled={!(data.writable && author.trim()) || action.pending || Boolean(saved)}
90
97
  key={option.id}
91
98
  onClick={() => action.dispatch(option.id)}
92
99
  size="sm"