@starlove/orbit 1.4.1 → 2.0.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 (329) hide show
  1. package/Dockerfile +10 -8
  2. package/README.md +146 -164
  3. package/dist/full/csp.txt +1 -0
  4. package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
  5. package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
  6. package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
  7. package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
  8. package/dist/full/fonts/Mona_Sans/README.txt +10 -0
  9. package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
  10. package/dist/full/fonts/Newsreader/README.txt +8 -0
  11. package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
  12. package/dist/full/index.html +36 -653
  13. package/dist/full/js/app.BmuponPJ.js +32 -0
  14. package/dist/full/js/app.BmuponPJ.js.map +1 -0
  15. package/dist/full/js/chunk.1fJGQGbW.js +2 -0
  16. package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
  17. package/dist/full/js/chunk.5D7JO2zp.js +2 -0
  18. package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
  19. package/dist/full/js/chunk.BrNn_D79.js +2 -0
  20. package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
  21. package/dist/full/js/chunk.CokJXIqM.js +2 -0
  22. package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
  23. package/dist/full/js/chunk.D_Haqy6R.js +2 -0
  24. package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
  25. package/dist/full/js/chunk.RO0jsH6I.js +15 -0
  26. package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
  27. package/dist/full/js/chunk.dqWOEtBM.js +2 -0
  28. package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
  29. package/dist/full/js/warm.f260bbbc.js +15 -0
  30. package/dist/full/orbits-favicon.png +0 -0
  31. package/dist/full/orbits-logo-bg-wide.png +0 -0
  32. package/dist/full/orbits-logo-bg.png +0 -0
  33. package/dist/full/orbits-logo.png +0 -0
  34. package/dist/full/styles.2b154ffe.css +1 -0
  35. package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
  36. package/dist/full/version.json +2 -2
  37. package/docs/AGENT_GUIDE.md +114 -0
  38. package/docs/UPDATING.md +249 -64
  39. package/lib/adapters/indexeddb/blobs.d.ts +3 -0
  40. package/lib/adapters/indexeddb/blobs.js +54 -0
  41. package/lib/adapters/indexeddb/database.d.ts +60 -0
  42. package/lib/adapters/indexeddb/database.js +126 -0
  43. package/lib/adapters/indexeddb/index.d.ts +5 -0
  44. package/lib/adapters/indexeddb/index.js +3 -0
  45. package/lib/adapters/indexeddb/locks.d.ts +14 -0
  46. package/lib/adapters/indexeddb/locks.js +40 -0
  47. package/lib/adapters/indexeddb/store.d.ts +40 -0
  48. package/lib/adapters/indexeddb/store.js +424 -0
  49. package/lib/adapters/indexeddb/tables.d.ts +43 -0
  50. package/lib/adapters/indexeddb/tables.js +127 -0
  51. package/lib/adapters/memory/blobs.d.ts +3 -0
  52. package/lib/adapters/memory/blobs.js +44 -0
  53. package/lib/adapters/memory/board.d.ts +41 -0
  54. package/lib/adapters/memory/board.js +521 -0
  55. package/lib/adapters/memory/index.d.ts +2 -0
  56. package/lib/adapters/memory/index.js +2 -0
  57. package/lib/adapters/memory/search.d.ts +6 -0
  58. package/lib/adapters/memory/search.js +30 -0
  59. package/lib/adapters/memory/store.d.ts +34 -0
  60. package/lib/adapters/memory/store.js +266 -0
  61. package/lib/adapters/sqlite/backups.d.ts +53 -0
  62. package/lib/adapters/sqlite/backups.js +232 -0
  63. package/lib/adapters/sqlite/blobs.d.ts +14 -0
  64. package/lib/adapters/sqlite/blobs.js +109 -0
  65. package/lib/adapters/sqlite/board.d.ts +77 -0
  66. package/lib/adapters/sqlite/board.js +328 -0
  67. package/lib/adapters/sqlite/database.d.ts +67 -0
  68. package/lib/adapters/sqlite/database.js +285 -0
  69. package/lib/adapters/sqlite/index.d.ts +3 -0
  70. package/lib/adapters/sqlite/index.js +2 -0
  71. package/lib/adapters/sqlite/lock.d.ts +11 -0
  72. package/lib/adapters/sqlite/lock.js +34 -0
  73. package/lib/adapters/sqlite/records.d.ts +54 -0
  74. package/lib/adapters/sqlite/records.js +546 -0
  75. package/lib/adapters/sqlite/schema.d.ts +54 -0
  76. package/lib/adapters/sqlite/schema.js +276 -0
  77. package/lib/adapters/sqlite/store.d.ts +39 -0
  78. package/lib/adapters/sqlite/store.js +385 -0
  79. package/lib/browser/in-page.d.ts +31 -0
  80. package/lib/browser/in-page.js +46 -0
  81. package/lib/browser/old-preview.d.ts +13 -0
  82. package/lib/browser/old-preview.js +80 -0
  83. package/lib/browser/preview-board.d.ts +8 -0
  84. package/lib/browser/preview-board.js +69 -0
  85. package/lib/cli/main.d.ts +7 -0
  86. package/lib/cli/main.js +343 -0
  87. package/lib/cli/orbit.d.ts +8 -0
  88. package/lib/cli/orbit.js +25 -0
  89. package/lib/client/http.d.ts +38 -0
  90. package/lib/client/http.js +334 -0
  91. package/lib/client/local.d.ts +26 -0
  92. package/lib/client/local.js +190 -0
  93. package/lib/client/object-url.d.ts +8 -0
  94. package/lib/client/object-url.js +21 -0
  95. package/lib/client/types.d.ts +268 -0
  96. package/lib/client/types.js +8 -0
  97. package/lib/commands/agent-tools.d.ts +31 -0
  98. package/lib/commands/agent-tools.js +100 -0
  99. package/lib/commands/index.d.ts +56 -0
  100. package/lib/commands/index.js +298 -0
  101. package/lib/commands/parse.d.ts +73 -0
  102. package/lib/commands/parse.js +231 -0
  103. package/lib/commands/present.d.ts +134 -0
  104. package/lib/commands/present.js +198 -0
  105. package/lib/commands/session.d.ts +85 -0
  106. package/lib/commands/session.js +138 -0
  107. package/lib/commands/verbs.d.ts +35 -0
  108. package/lib/commands/verbs.js +842 -0
  109. package/lib/core/actors.d.ts +18 -0
  110. package/lib/core/actors.js +45 -0
  111. package/lib/core/agent.d.ts +128 -0
  112. package/lib/core/agent.js +323 -0
  113. package/lib/core/boards.d.ts +113 -0
  114. package/lib/core/boards.js +288 -0
  115. package/lib/core/bootstrap.d.ts +47 -0
  116. package/lib/core/bootstrap.js +100 -0
  117. package/lib/core/bytes.d.ts +10 -0
  118. package/lib/core/bytes.js +52 -0
  119. package/lib/core/card-paths.d.ts +25 -0
  120. package/lib/core/card-paths.js +44 -0
  121. package/lib/core/context.d.ts +60 -0
  122. package/lib/core/context.js +42 -0
  123. package/lib/core/cross-board.d.ts +81 -0
  124. package/lib/core/cross-board.js +228 -0
  125. package/lib/core/describe.d.ts +21 -0
  126. package/lib/core/describe.js +81 -0
  127. package/lib/core/errors.d.ts +159 -0
  128. package/lib/core/errors.js +160 -0
  129. package/lib/core/files.d.ts +79 -0
  130. package/lib/core/files.js +262 -0
  131. package/lib/core/history.d.ts +20 -0
  132. package/lib/core/history.js +53 -0
  133. package/lib/core/image-types.d.ts +16 -0
  134. package/lib/core/image-types.js +36 -0
  135. package/lib/core/model.d.ts +263 -0
  136. package/lib/core/model.js +34 -0
  137. package/lib/core/nebula-link.d.ts +14 -0
  138. package/lib/core/nebula-link.js +39 -0
  139. package/lib/core/relations.d.ts +58 -0
  140. package/lib/core/relations.js +112 -0
  141. package/lib/core/search.d.ts +38 -0
  142. package/lib/core/search.js +92 -0
  143. package/lib/core/sessions.d.ts +44 -0
  144. package/lib/core/sessions.js +158 -0
  145. package/lib/core/snapshot-schema.d.ts +295 -0
  146. package/lib/core/snapshot-schema.js +82 -0
  147. package/lib/core/snapshots.d.ts +75 -0
  148. package/lib/core/snapshots.js +409 -0
  149. package/lib/core/states.d.ts +25 -0
  150. package/lib/core/states.js +126 -0
  151. package/lib/core/text.d.ts +40 -0
  152. package/lib/core/text.js +113 -0
  153. package/lib/core/tickets.d.ts +69 -0
  154. package/lib/core/tickets.js +311 -0
  155. package/lib/core/trello.d.ts +17 -0
  156. package/lib/core/trello.js +269 -0
  157. package/lib/core/verdicts.d.ts +28 -0
  158. package/lib/core/verdicts.js +92 -0
  159. package/lib/core/views.d.ts +95 -0
  160. package/lib/core/views.js +186 -0
  161. package/lib/http/app.d.ts +46 -0
  162. package/lib/http/app.js +491 -0
  163. package/lib/mcp-server.d.ts +10 -0
  164. package/lib/mcp-server.js +120 -0
  165. package/lib/node/assets.d.ts +7 -0
  166. package/lib/node/assets.js +68 -0
  167. package/lib/node/data-folder.d.ts +21 -0
  168. package/lib/node/data-folder.js +34 -0
  169. package/lib/node/env.d.ts +38 -0
  170. package/lib/node/env.js +49 -0
  171. package/lib/node/host-check.d.ts +24 -0
  172. package/lib/node/host-check.js +53 -0
  173. package/lib/node/host-services.d.ts +9 -0
  174. package/lib/node/host-services.js +150 -0
  175. package/lib/node/local.d.ts +22 -0
  176. package/lib/node/local.js +61 -0
  177. package/lib/node/migrate.d.ts +53 -0
  178. package/lib/node/migrate.js +635 -0
  179. package/lib/node/package.d.ts +7 -0
  180. package/lib/node/package.js +14 -0
  181. package/lib/node/repo-board.d.ts +49 -0
  182. package/lib/node/repo-board.js +245 -0
  183. package/lib/node/serve.d.ts +8 -0
  184. package/lib/node/serve.js +106 -0
  185. package/lib/orbit.d.ts +47 -0
  186. package/lib/orbit.js +44 -0
  187. package/lib/quiet-sqlite-warning.d.ts +1 -0
  188. package/lib/quiet-sqlite-warning.js +18 -0
  189. package/lib/store/blob-path.d.ts +6 -0
  190. package/lib/store/blob-path.js +12 -0
  191. package/lib/store/conformance.d.ts +52 -0
  192. package/lib/store/conformance.js +1449 -0
  193. package/lib/store/search-words.d.ts +10 -0
  194. package/lib/store/search-words.js +19 -0
  195. package/lib/store/types.d.ts +452 -0
  196. package/lib/store/types.js +34 -0
  197. package/llms.txt +63 -0
  198. package/package.json +109 -25
  199. package/src/cli/orbit.js +11 -658
  200. package/src/mcp-server.js +16 -876
  201. package/SKILL-ORBIT.md +0 -151
  202. package/build.mjs +0 -210
  203. package/dist/full/js/app.4e6fbe20.js +0 -952
  204. package/dist/full/js/app.4e6fbe20.js.map +0 -7
  205. package/dist/full/styles.4e60d824.css +0 -1
  206. package/docs/AGENT_PROTOCOL.md +0 -424
  207. package/docs/MCP_SETUP.md +0 -54
  208. package/docs/ORBIT_DISPATCH.md +0 -280
  209. package/docs/SS-Orbit-Dark.png +0 -0
  210. package/docs/SS-Orbit-Light.png +0 -0
  211. package/docs/mcp-client.example.json +0 -13
  212. package/docs/setup/orbit-init-wizard.md +0 -174
  213. package/public/apple-touch-icon.png +0 -0
  214. package/public/cursors/live_arrow.cur +0 -0
  215. package/public/cursors/live_link.cur +0 -0
  216. package/public/cursors/live_move.cur +0 -0
  217. package/public/cursors/live_select.cur +0 -0
  218. package/public/cursors/live_unavail.cur +0 -0
  219. package/public/favicon-32x32.png +0 -0
  220. package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
  221. package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
  222. package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
  223. package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
  224. package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
  225. package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
  226. package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
  227. package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
  228. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
  229. package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
  230. package/public/fonts/Chakra_Petch/OFL.txt +0 -93
  231. package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
  232. package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
  233. package/public/fonts/Inter/OFL.txt +0 -93
  234. package/public/fonts/Inter/README.txt +0 -118
  235. package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
  236. package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
  237. package/public/fonts/JetBrains_Mono/README.txt +0 -79
  238. package/public/fonts/OpenRing-Regular.ttf +0 -0
  239. package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  240. package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  241. package/public/fonts/Open_Sans/README.txt +0 -100
  242. package/public/fonts/Orbitron/OFL.txt +0 -93
  243. package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
  244. package/public/fonts/Orbitron/README.txt +0 -68
  245. package/public/fonts/Rajdhani/OFL.txt +0 -93
  246. package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
  247. package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
  248. package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
  249. package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
  250. package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
  251. package/public/fonts/runescape-uf.ttf +0 -0
  252. package/public/index.html +0 -769
  253. package/public/js/actor-labels.js +0 -65
  254. package/public/js/api.js +0 -39
  255. package/public/js/app.js +0 -45
  256. package/public/js/artifact-drawer.js +0 -79
  257. package/public/js/board-export.js +0 -161
  258. package/public/js/board-menu.js +0 -374
  259. package/public/js/card-actions.js +0 -545
  260. package/public/js/config.js +0 -20
  261. package/public/js/create-card.js +0 -190
  262. package/public/js/dom.js +0 -19
  263. package/public/js/drawer.js +0 -273
  264. package/public/js/font-preference.js +0 -230
  265. package/public/js/format.js +0 -433
  266. package/public/js/help-menu.js +0 -31
  267. package/public/js/icons.js +0 -16
  268. package/public/js/kanban.js +0 -694
  269. package/public/js/local-backend.js +0 -1015
  270. package/public/js/main.js +0 -161
  271. package/public/js/modal.js +0 -33
  272. package/public/js/motion-preference.js +0 -64
  273. package/public/js/panel.js +0 -58
  274. package/public/js/router.js +0 -134
  275. package/public/js/search.js +0 -56
  276. package/public/js/settings-tabs.js +0 -7
  277. package/public/js/settings.js +0 -1634
  278. package/public/js/sse.js +0 -159
  279. package/public/js/starscape.js +0 -332
  280. package/public/js/state.js +0 -129
  281. package/public/js/text-editing.js +0 -128
  282. package/public/js/theme-preference.js +0 -95
  283. package/public/js/ticket-detail.js +0 -1516
  284. package/public/js/ticket-typeahead.js +0 -49
  285. package/public/js/toast.js +0 -89
  286. package/public/js/topbar-notes.js +0 -155
  287. package/public/js/transport.js +0 -22
  288. package/public/js/typeahead.js +0 -206
  289. package/public/js/unread.js +0 -121
  290. package/public/js/update-check.js +0 -58
  291. package/public/js/url-routes.js +0 -94
  292. package/public/orbits-favicon.png +0 -0
  293. package/public/orbits-logo-bg-wide.png +0 -0
  294. package/public/orbits-logo-bg.png +0 -0
  295. package/public/orbits-logo.png +0 -0
  296. package/public/styles.css +0 -7709
  297. package/src/core/agent.js +0 -387
  298. package/src/core/agents-md.js +0 -109
  299. package/src/core/artifacts.js +0 -149
  300. package/src/core/attachments.js +0 -238
  301. package/src/core/auth.js +0 -85
  302. package/src/core/backups.js +0 -142
  303. package/src/core/board.js +0 -148
  304. package/src/core/boards.js +0 -409
  305. package/src/core/bootstrap.js +0 -170
  306. package/src/core/css-bundle.js +0 -30
  307. package/src/core/db.js +0 -315
  308. package/src/core/delete-board.js +0 -163
  309. package/src/core/dispatch.js +0 -1007
  310. package/src/core/events.js +0 -314
  311. package/src/core/http.js +0 -115
  312. package/src/core/migrate.js +0 -198
  313. package/src/core/paths.js +0 -55
  314. package/src/core/provision-repo-board.js +0 -119
  315. package/src/core/queries.js +0 -246
  316. package/src/core/registry.js +0 -211
  317. package/src/core/relations.js +0 -163
  318. package/src/core/review-verdicts.js +0 -138
  319. package/src/core/router.js +0 -556
  320. package/src/core/search.js +0 -183
  321. package/src/core/seed.js +0 -234
  322. package/src/core/snapshots.js +0 -692
  323. package/src/core/states.js +0 -167
  324. package/src/core/system-picker.js +0 -109
  325. package/src/core/tickets.js +0 -400
  326. package/src/core/util.js +0 -106
  327. package/src/core/version-check.js +0 -154
  328. package/src/mcp/orbit-client.js +0 -287
  329. package/src/server.js +0 -115
@@ -1,49 +0,0 @@
1
- // Shared ticket typeahead: the styled suggestion dropdown used wherever a
2
- // ticket is picked by typing (ticket-detail's Related / parent-epic / add-child
3
- // fields and the New-card flyout's parent-epic field). Rows mirror the topbar
4
- // search results — bold #number, title, type badge, state badge — and selecting
5
- // a row fills the input with the canonical "#12 — Title" label so the host
6
- // form's resolve-on-submit logic keeps working unchanged.
7
-
8
- import { escapeHtml, ticketLabel, stateClassFor, typeLabel, canonicalTicketType } from "./format.js";
9
- import { attachTypeahead } from "./typeahead.js";
10
-
11
- // Canonical "#12 — Title" label the resolve-on-submit logic expects.
12
- export function ticketCanonicalLabel(t) {
13
- return `#${t.number} — ${t.title}`;
14
- }
15
-
16
- // A typeahead row for a ticket: bold #number, title, then trailing type and
17
- // state badges — the same shape as the topbar search results.
18
- export function renderTicketHitRow(t) {
19
- const type = canonicalTicketType(t.type);
20
- return `
21
- <span class="search-hit-main">
22
- <strong>${escapeHtml(ticketLabel(t))}</strong>
23
- <span class="search-hit-title">${escapeHtml(t.title)}</span>
24
- </span>
25
- <span class="search-hit-type type-pill-${escapeHtml(type)}">${escapeHtml(typeLabel(t.type))}</span>
26
- <span class="search-hit-state" data-variant="${escapeHtml(stateClassFor(t))}">${escapeHtml(t.state_name || "State")}</span>
27
- `;
28
- }
29
-
30
- // Matches a ticket against a query by number ("12", "#12") or title substring.
31
- export function ticketMatchesQuery(t, query) {
32
- const q = query.replace(/^#/, "");
33
- return String(t.number).startsWith(q) || t.title.toLowerCase().includes(query);
34
- }
35
-
36
- // Wire the styled typeahead onto a ticket-picker input. `getCandidates` returns
37
- // the eligible tickets; `onSelect` (optional) fires after the value is filled,
38
- // e.g. to record the chosen ticket's id in a hidden field.
39
- export function attachTicketTypeahead(input, getCandidates, { onSelect } = {}) {
40
- if (!input) return;
41
- attachTypeahead(input, {
42
- getItems: getCandidates,
43
- match: ticketMatchesQuery,
44
- renderItem: renderTicketHitRow,
45
- valueOf: ticketCanonicalLabel,
46
- onSelect,
47
- emptyText: "No matching tickets",
48
- });
49
- }
@@ -1,89 +0,0 @@
1
- // Toast notifications + small UI utilities (downloadJson, debounce).
2
-
3
- import { escapeHtml } from "./format.js";
4
- import { closeIconSvg, trashIconSvg } from "./icons.js";
5
-
6
- const toastIcons = {
7
- info: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd"/></svg>`,
8
- success: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd"/></svg>`,
9
- warning: `<svg viewBox="0 0 24 24" fill="currentColor"><path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clip-rule="evenodd"/></svg>`,
10
- error: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd"/></svg>`,
11
- removed: trashIconSvg
12
- };
13
-
14
- let toastContainer = null;
15
-
16
- function ensureToastContainer() {
17
- if (toastContainer && toastContainer.isConnected) return toastContainer;
18
- toastContainer = document.createElement("div");
19
- toastContainer.className = "toast-container";
20
- document.body.append(toastContainer);
21
- return toastContainer;
22
- }
23
-
24
- export function toast(message, type = "info", duration = 4000, options = {}) {
25
- const container = ensureToastContainer();
26
-
27
- const node = document.createElement("div");
28
- node.className = `toast toast-${type}`;
29
- node.setAttribute("role", "status");
30
-
31
- const icon = toastIcons[type];
32
- node.innerHTML = `<span class="toast-icon">${icon}</span><span class="toast-msg">${escapeHtml(message)}</span><button class="toast-close" aria-label="Dismiss">${closeIconSvg}</button>`;
33
-
34
- node.querySelector(".toast-close").addEventListener("click", (event) => {
35
- event.stopPropagation();
36
- dismissToast(node);
37
- });
38
-
39
- // Optional whole-toast click action (e.g. "Update available → open Settings").
40
- if (typeof options.onClick === "function") {
41
- node.classList.add("toast-clickable");
42
- node.addEventListener("click", () => {
43
- options.onClick();
44
- dismissToast(node);
45
- });
46
- }
47
-
48
- container.prepend(node);
49
- requestAnimationFrame(() => requestAnimationFrame(() => node.classList.add("toast-enter")));
50
-
51
- const timer = setTimeout(() => dismissToast(node), duration);
52
- node.addEventListener("mouseenter", () => clearTimeout(timer));
53
- node.addEventListener("mouseleave", () => {
54
- const t2 = setTimeout(() => dismissToast(node), 1500);
55
- node.addEventListener("mouseenter", () => clearTimeout(t2), { once: true });
56
- });
57
- }
58
-
59
- toast.info = (msg, dur) => toast(msg, "info", dur);
60
- toast.success = (msg, dur) => toast(msg, "success", dur);
61
- toast.warning = (msg, dur) => toast(msg, "warning", dur);
62
- toast.error = (msg, dur) => toast(msg, "error", dur);
63
- toast.removed = (msg, dur) => toast(msg, "removed", dur);
64
-
65
- function dismissToast(node) {
66
- if (node.classList.contains("toast-exit")) return;
67
- node.classList.add("toast-exit");
68
- node.addEventListener("animationend", () => node.remove(), { once: true });
69
- }
70
-
71
- export function downloadJson(filename, value) {
72
- const blob = new Blob([JSON.stringify(value, null, 2)], { type: "application/json" });
73
- const url = URL.createObjectURL(blob);
74
- const anchor = document.createElement("a");
75
- anchor.href = url;
76
- anchor.download = filename;
77
- document.body.append(anchor);
78
- anchor.click();
79
- anchor.remove();
80
- URL.revokeObjectURL(url);
81
- }
82
-
83
- export function debounce(fn, wait) {
84
- let timer = 0;
85
- return (...args) => {
86
- clearTimeout(timer);
87
- timer = setTimeout(() => fn(...args), wait);
88
- };
89
- }
@@ -1,155 +0,0 @@
1
- import { cleanText, escapeHtml, renderPreservedText } from "./format.js";
2
-
3
- const NOTE_FALLBACK = "No board notes yet";
4
- const NOTES_PLACEHOLDER = "Add board notes for yourself and your agents…";
5
-
6
- function compactText(value) {
7
- return cleanText(value || "")
8
- .split("\n")
9
- .map((line) => line.trim())
10
- .filter(Boolean)
11
- .join(" ")
12
- .replace(/\s+/g, " ")
13
- .trim();
14
- }
15
-
16
- export function boardNotesSummary(board = {}) {
17
- return compactText(board.project_notes) || NOTE_FALLBACK;
18
- }
19
-
20
- function updateCachedBoard(board, appState) {
21
- if (!board?.id || !appState?.data?.boards) return;
22
- const index = appState.data.boards.findIndex((item) => item.id === board.id);
23
- if (index >= 0) appState.data.boards.splice(index, 1, { ...appState.data.boards[index], ...board });
24
- }
25
-
26
- let lastRenderedBoard = {};
27
-
28
- function renderNotesBody(board) {
29
- const notes = board?.project_notes || "";
30
- const hasNotes = Boolean(notes.trim());
31
- const placeholderClass = hasNotes ? "" : " is-placeholder";
32
- const inner = hasNotes ? renderPreservedText(notes) : escapeHtml(NOTES_PLACEHOLDER);
33
- return `
34
- <div
35
- class="inline-md-field-body preserved-text-body editable-field settings-notes-body brand-focus-notes-edit${placeholderClass}"
36
- data-edit-field="project_notes"
37
- tabindex="0"
38
- title="Click to edit"
39
- role="button"
40
- aria-label="Edit Notes For You"
41
- >${inner}</div>
42
- `;
43
- }
44
-
45
- export function renderTopbarFocus(board = lastRenderedBoard) {
46
- lastRenderedBoard = board || {};
47
- const summary = boardNotesSummary(lastRenderedBoard);
48
- const notesButton = document.getElementById("brandNotesBtn");
49
- const notesText = document.getElementById("brandNotesText");
50
- const notesBody = document.getElementById("brandFocusNotes");
51
-
52
- if (notesButton) {
53
- notesButton.hidden = false;
54
- notesButton.dataset.empty = summary === NOTE_FALLBACK ? "true" : "false";
55
- notesButton.setAttribute("aria-label", `Show board notes: ${summary}`);
56
- }
57
- if (notesText) notesText.textContent = summary === NOTE_FALLBACK ? "" : summary;
58
- if (notesBody) notesBody.innerHTML = renderNotesBody(lastRenderedBoard);
59
- }
60
-
61
- function setPopoverOpen(open) {
62
- const buttons = [
63
- document.getElementById("brandFocusBtn"),
64
- document.getElementById("brandNotesBtn")
65
- ].filter(Boolean);
66
- const popover = document.getElementById("brandFocusPopover");
67
- if (!popover) return;
68
- for (const button of buttons) {
69
- button.setAttribute("aria-expanded", open ? "true" : "false");
70
- button.dataset.state = open ? "open" : "closed";
71
- }
72
- popover.hidden = !open;
73
- }
74
-
75
- function togglePopover() {
76
- const popover = document.getElementById("brandFocusPopover");
77
- renderTopbarFocus();
78
- setPopoverOpen(Boolean(popover?.hidden));
79
- }
80
-
81
- async function startBoardNotesEdit(sourceEvent) {
82
- const notesEl = document.querySelector('#brandFocusPopover [data-edit-field="project_notes"]');
83
- if (!notesEl || !lastRenderedBoard?.id) return;
84
- const { startInlineEdit } = await import("./ticket-detail.js");
85
- startInlineEdit(notesEl, {
86
- fieldName: "project_notes",
87
- multiline: true,
88
- initialValue: lastRenderedBoard.project_notes || "",
89
- ariaLabel: "Edit Notes For You",
90
- sourceEvent,
91
- rerender: async () => {
92
- renderTopbarFocus(lastRenderedBoard);
93
- setPopoverOpen(true);
94
- },
95
- commit: async (next) => {
96
- const [{ api }, { state }] = await Promise.all([
97
- import("./api.js"),
98
- import("./state.js")
99
- ]);
100
- const updatedBoard = await api(`/api/boards/${encodeURIComponent(lastRenderedBoard.id || state.boardId)}`, {
101
- method: "PATCH",
102
- body: { project_notes: cleanText(next) }
103
- });
104
- lastRenderedBoard = { ...lastRenderedBoard, ...updatedBoard };
105
- updateCachedBoard(lastRenderedBoard, state);
106
- renderTopbarFocus(lastRenderedBoard);
107
- setPopoverOpen(true);
108
- }
109
- });
110
- }
111
-
112
- export function wireTopbarFocus() {
113
- const logoButton = document.getElementById("brandFocusBtn");
114
- const notesButton = document.getElementById("brandNotesBtn");
115
- const popover = document.getElementById("brandFocusPopover");
116
- if (!logoButton || !notesButton || !popover) return;
117
-
118
- logoButton.addEventListener("click", (event) => {
119
- event.stopPropagation();
120
- togglePopover();
121
- });
122
-
123
- notesButton.addEventListener("click", (event) => {
124
- event.stopPropagation();
125
- togglePopover();
126
- });
127
-
128
- popover.addEventListener("click", (event) => {
129
- event.stopPropagation();
130
- if (event.target.closest('[data-edit-field="project_notes"]')) {
131
- void startBoardNotesEdit(event);
132
- }
133
- });
134
-
135
- popover.addEventListener("keydown", (event) => {
136
- if (event.key !== "Enter" && event.key !== " ") return;
137
- if (!event.target.closest('[data-edit-field="project_notes"]')) return;
138
- event.preventDefault();
139
- void startBoardNotesEdit(event);
140
- });
141
-
142
- document.addEventListener("click", (event) => {
143
- if (
144
- event.target.closest("#brandFocusBtn") ||
145
- event.target.closest("#brandNotesBtn") ||
146
- event.target.closest("#brandFocusPopover")
147
- ) return;
148
- setPopoverOpen(false);
149
- });
150
-
151
- document.addEventListener("keydown", (event) => {
152
- if (event.key !== "Escape") return;
153
- setPopoverOpen(false);
154
- });
155
- }
@@ -1,22 +0,0 @@
1
- // Request transport. The full edition talks to the Orbit HTTP server; a
2
- // preview build will replace this module (or branch on `edition`) to dispatch
3
- // the same path/body shapes to a local IndexedDB backend. Keep this file
4
- // I/O-only — UI concerns (toasts, redirects) belong in api.js callers.
5
-
6
- export async function request({ method = "GET", path, body, headers }) {
7
- const finalHeaders = { "Content-Type": "application/json", ...(headers || {}) };
8
- const response = await fetch(path, {
9
- method,
10
- headers: finalHeaders,
11
- body: body !== undefined ? JSON.stringify(body) : undefined
12
- });
13
- const text = await response.text();
14
- const payload = text ? JSON.parse(text) : {};
15
- if (!response.ok) {
16
- const error = new Error(payload.error || response.statusText);
17
- error.payload = payload;
18
- error.status = response.status;
19
- throw error;
20
- }
21
- return payload;
22
- }
@@ -1,206 +0,0 @@
1
- // Custom typeahead dropdown for the ticket-detail drawer inputs (Add label,
2
- // Set parent epic, Add related ticket). Replaces the native <datalist>, which
3
- // browsers render in their own un-styleable popup, with a results list that
4
- // reuses the topbar search styling (bold label + title + state badge). The
5
- // component only fills the input on selection — the surrounding form's Add/Set
6
- // button and Enter handler still own the actual mutation, so swapping the
7
- // datalist for this is a pure presentation change.
8
-
9
- const ACTIVE_CLASS = "is-active";
10
-
11
- /**
12
- * Wire a styled suggestion dropdown onto a text input.
13
- *
14
- * Options:
15
- * - getItems(): returns the full candidate list (re-read on each keystroke).
16
- * - match(item, q): whether an item matches the lowercased query string.
17
- * - renderItem(item): inner HTML for the .search-hit row.
18
- * - valueOf(item): string written into the input when the item is chosen.
19
- * - onSelect(item): optional hook fired after the value is filled.
20
- * - emptyText: message shown when a non-empty query matches nothing.
21
- * - minChars: minimum query length before the list opens (default 0).
22
- * - maxItems: cap on rendered rows (default 8).
23
- *
24
- * Returns a teardown function (rarely needed — the drawer is re-rendered wholesale).
25
- */
26
- export function attachTypeahead(
27
- input,
28
- {
29
- getItems,
30
- match,
31
- renderItem,
32
- valueOf,
33
- onSelect,
34
- emptyText = "No matches",
35
- minChars = 0,
36
- maxItems = 8,
37
- },
38
- ) {
39
- if (!input) return () => {};
40
-
41
- // Appended to <body> (not the input's wrapper) so the ancestor
42
- // .settings-card overflow:hidden can't clip it; positioned as fixed from the
43
- // input's viewport rect and re-anchored on scroll/resize while open.
44
- const list = document.createElement("div");
45
- list.className = "search-results typeahead-results";
46
- list.hidden = true;
47
- list.setAttribute("role", "listbox");
48
- list._ownerInput = input;
49
- document.body.appendChild(list);
50
-
51
- // The drawer re-renders wholesale, detaching the old inputs but leaving their
52
- // body-level dropdowns behind. Sweep those orphans (owner no longer in the
53
- // document) whenever a fresh typeahead mounts.
54
- for (const el of document.querySelectorAll(".typeahead-results")) {
55
- if (el !== list && el._ownerInput && !el._ownerInput.isConnected) {
56
- el.remove();
57
- }
58
- }
59
-
60
- let visible = [];
61
- let activeIndex = -1;
62
- let blurTimer = 0;
63
-
64
- const position = () => {
65
- const r = input.getBoundingClientRect();
66
- list.style.left = `${r.left}px`;
67
- list.style.width = `${r.width}px`;
68
- list.style.top = `${r.bottom + 4}px`;
69
- };
70
-
71
- const onReposition = () => {
72
- if (list.hidden) return;
73
- position();
74
- };
75
-
76
- const close = () => {
77
- list.hidden = true;
78
- list.innerHTML = "";
79
- visible = [];
80
- activeIndex = -1;
81
- window.removeEventListener("scroll", onReposition, true);
82
- window.removeEventListener("resize", onReposition);
83
- };
84
-
85
- const open = () => {
86
- if (!list.hidden) return;
87
- list.hidden = false;
88
- window.addEventListener("scroll", onReposition, true);
89
- window.addEventListener("resize", onReposition);
90
- };
91
-
92
- const render = () => {
93
- const query = input.value.trim().toLowerCase();
94
- if (query.length < minChars) {
95
- close();
96
- return;
97
- }
98
- const all = getItems() || [];
99
- visible = (query ? all.filter((item) => match(item, query)) : all).slice(
100
- 0,
101
- maxItems,
102
- );
103
- if (visible.length === 0) {
104
- // Only surface the empty state once the user has typed something —
105
- // an empty focused field just shows the full (capped) list below.
106
- if (query) {
107
- open();
108
- list.innerHTML = `<div class="search-hit typeahead-empty">${emptyText}</div>`;
109
- position();
110
- } else {
111
- close();
112
- }
113
- return;
114
- }
115
- activeIndex = -1;
116
- open();
117
- list.innerHTML = visible
118
- .map(
119
- (item, i) =>
120
- `<div class="search-hit" role="option" data-index="${i}">${renderItem(item)}</div>`,
121
- )
122
- .join("");
123
- position();
124
- };
125
-
126
- const setActive = (next) => {
127
- const rows = list.querySelectorAll("[data-index]");
128
- if (!rows.length) return;
129
- activeIndex = (next + rows.length) % rows.length;
130
- rows.forEach((row, i) => {
131
- const on = i === activeIndex;
132
- row.classList.toggle(ACTIVE_CLASS, on);
133
- if (on) row.scrollIntoView({ block: "nearest" });
134
- });
135
- };
136
-
137
- const choose = (item) => {
138
- if (!item) return;
139
- input.value = valueOf(item);
140
- // Let the host form react (e.g. the clear-button has-value toggle).
141
- input.dispatchEvent(new Event("input", { bubbles: true }));
142
- close();
143
- input.focus();
144
- if (onSelect) onSelect(item);
145
- };
146
-
147
- const onInput = () => render();
148
- const onFocus = () => render();
149
-
150
- const onKeydown = (event) => {
151
- if (list.hidden) return;
152
- if (event.key === "ArrowDown") {
153
- event.preventDefault();
154
- setActive(activeIndex + 1);
155
- } else if (event.key === "ArrowUp") {
156
- event.preventDefault();
157
- setActive(activeIndex - 1);
158
- } else if (event.key === "Enter") {
159
- // Only swallow Enter when a suggestion is highlighted; otherwise let the
160
- // form submit the typed value as before.
161
- if (activeIndex >= 0 && visible[activeIndex]) {
162
- event.preventDefault();
163
- // stopImmediatePropagation so a sibling keydown listener on the same
164
- // input (e.g. the label field's add-on-Enter) doesn't also fire.
165
- event.stopImmediatePropagation();
166
- choose(visible[activeIndex]);
167
- }
168
- } else if (event.key === "Escape") {
169
- if (!list.hidden) {
170
- event.stopPropagation();
171
- close();
172
- }
173
- }
174
- };
175
-
176
- const onBlur = () => {
177
- blurTimer = window.setTimeout(close, 120);
178
- };
179
-
180
- // mousedown beats the input's blur, so the row click registers before close().
181
- const onMouseDown = (event) => {
182
- const row = event.target.closest("[data-index]");
183
- if (!row) return;
184
- event.preventDefault();
185
- window.clearTimeout(blurTimer);
186
- choose(visible[Number(row.dataset.index)]);
187
- };
188
-
189
- input.addEventListener("input", onInput);
190
- input.addEventListener("focus", onFocus);
191
- input.addEventListener("keydown", onKeydown);
192
- input.addEventListener("blur", onBlur);
193
- list.addEventListener("mousedown", onMouseDown);
194
-
195
- return () => {
196
- window.clearTimeout(blurTimer);
197
- window.removeEventListener("scroll", onReposition, true);
198
- window.removeEventListener("resize", onReposition);
199
- input.removeEventListener("input", onInput);
200
- input.removeEventListener("focus", onFocus);
201
- input.removeEventListener("keydown", onKeydown);
202
- input.removeEventListener("blur", onBlur);
203
- list.removeEventListener("mousedown", onMouseDown);
204
- list.remove();
205
- };
206
- }
@@ -1,121 +0,0 @@
1
- // Per-card "unread updates" tracker. Lives client-side in localStorage so
2
- // no backend changes are needed. Each entry tracks both:
3
- // readAt — ISO timestamp the human last acknowledged this card
4
- // count — number of foreign-actor events received since that ack
5
- // Render uses a dot for count===1 and a numeric pill for count>=2.
6
- //
7
- // Acknowledgement happens on three paths:
8
- // - opening the card (renderDetail in ticket-detail.js)
9
- // - own action via SSE self-event suppression (sse.js)
10
- // - the one-time per-board seed below, so the very first load doesn't
11
- // flash badges on every existing card
12
-
13
- import { state } from "./state.js";
14
-
15
- const READ_KEY = (boardId) => `mab_card_read:${boardId}`;
16
- const SEEDED_KEY = (boardId) => `mab_card_read_seeded:${boardId}`;
17
-
18
- const cache = new Map();
19
-
20
- // Normalize a stored value into { readAt, count }. Tolerates the old
21
- // string-only shape so existing localStorage entries upgrade in place.
22
- function normalize(raw) {
23
- if (!raw) return { readAt: null, count: 0 };
24
- if (typeof raw === "string") return { readAt: raw, count: 0 };
25
- return { readAt: raw.readAt || null, count: Number(raw.count) || 0 };
26
- }
27
-
28
- function getMap(boardId) {
29
- if (!boardId) return new Map();
30
- let m = cache.get(boardId);
31
- if (m) return m;
32
- m = new Map();
33
- try {
34
- const raw = localStorage.getItem(READ_KEY(boardId));
35
- if (raw) {
36
- const obj = JSON.parse(raw);
37
- for (const [k, v] of Object.entries(obj)) m.set(k, normalize(v));
38
- }
39
- } catch { /* ignore */ }
40
- cache.set(boardId, m);
41
- return m;
42
- }
43
-
44
- function persist(boardId) {
45
- const m = cache.get(boardId);
46
- if (!m) return;
47
- try {
48
- const obj = {};
49
- for (const [k, v] of m) obj[k] = v;
50
- localStorage.setItem(READ_KEY(boardId), JSON.stringify(obj));
51
- } catch { /* ignore */ }
52
- }
53
-
54
- function entryFor(m, ticketId) {
55
- let e = m.get(ticketId);
56
- if (!e) {
57
- e = { readAt: null, count: 0 };
58
- m.set(ticketId, e);
59
- }
60
- return e;
61
- }
62
-
63
- export function unreadCount(ticket) {
64
- const boardId = ticket?.board_id || state.boardId;
65
- if (!boardId || !ticket?.id) return 0;
66
- const m = getMap(boardId);
67
- const e = m.get(ticket.id);
68
- if (!e) {
69
- // Absence: if seeded, the card appeared post-seed → flag with count 1.
70
- // If never seeded, treat as read so first-ever load is quiet.
71
- return localStorage.getItem(SEEDED_KEY(boardId)) === "1" ? 1 : 0;
72
- }
73
- return e.count;
74
- }
75
-
76
- export function isUnread(ticket) {
77
- return unreadCount(ticket) > 0;
78
- }
79
-
80
- export function markRead(ticketId, timestamp) {
81
- const boardId = state.boardId;
82
- if (!boardId || !ticketId) return;
83
- const m = getMap(boardId);
84
- const e = entryFor(m, ticketId);
85
- if (e.count === 0 && e.readAt && timestamp && e.readAt >= timestamp) return;
86
- e.count = 0;
87
- if (timestamp) e.readAt = timestamp;
88
- persist(boardId);
89
- }
90
-
91
- export function bumpUnread(ticketId) {
92
- const boardId = state.boardId;
93
- if (!boardId || !ticketId) return;
94
- const m = getMap(boardId);
95
- const e = entryFor(m, ticketId);
96
- e.count = (e.count || 0) + 1;
97
- persist(boardId);
98
- }
99
-
100
- export function clearUnread(ticketId) {
101
- const boardId = state.boardId;
102
- if (!boardId || !ticketId) return;
103
- const m = getMap(boardId);
104
- if (!m.has(ticketId)) return;
105
- m.delete(ticketId);
106
- persist(boardId);
107
- }
108
-
109
- // One-time per-board seed so existing cards start "read" the first time the
110
- // user runs this version. Runs again per-board the first time each board is
111
- // visited.
112
- export function seedReadIfNeeded(boardId, tickets) {
113
- if (!boardId) return;
114
- if (localStorage.getItem(SEEDED_KEY(boardId)) === "1") return;
115
- const m = getMap(boardId);
116
- for (const t of tickets || []) {
117
- if (!m.has(t.id) && t.updated_at) m.set(t.id, { readAt: t.updated_at, count: 0 });
118
- }
119
- persist(boardId);
120
- try { localStorage.setItem(SEEDED_KEY(boardId), "1"); } catch { /* ignore */ }
121
- }