@heycys/gallery 0.2.1 → 0.2.3

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 (169) hide show
  1. package/THIRD-PARTY-NOTICES.md +1 -1
  2. package/browser/editor/page/annotate/index.mjs +3 -2
  3. package/browser/editor/page/board/index.mjs +2 -1
  4. package/browser/editor/page/board/settings.mjs +2 -1
  5. package/browser/editor/page/core.mjs +3 -2
  6. package/browser/editor/page/custom-page.mjs +4 -3
  7. package/browser/editor/page/editor.mjs +13 -7
  8. package/browser/editor/page/emoji-picker/support.mjs +3 -2
  9. package/browser/editor/page/links/index.mjs +1 -12
  10. package/browser/editor/page/links/pages.mjs +3 -2
  11. package/browser/editor/page/outline/folds.mjs +3 -0
  12. package/browser/editor/page/post.mjs +2 -1
  13. package/browser/editor/page/present-links.mjs +4 -3
  14. package/browser/editor/page/present-shell.mjs +3 -58
  15. package/browser/editor/page/session.mjs +3 -37
  16. package/browser/editor/page/storage.mjs +5 -5
  17. package/browser/shared/component-catalog.mjs +2 -2
  18. package/browser/shared/drafts.mjs +26 -51
  19. package/browser/shared/emoji/icon-value.mjs +2 -1
  20. package/browser/shared/extension-manifest.mjs +90 -0
  21. package/browser/shared/extensions.mjs +26 -16
  22. package/browser/shared/http.mjs +1 -1
  23. package/browser/shared/i18n/en.mjs +46 -15
  24. package/browser/shared/media.mjs +2 -1
  25. package/browser/shared/outline.mjs +11 -51
  26. package/browser/shared/page-address.mjs +8 -15
  27. package/browser/shared/paths.mjs +2 -1
  28. package/browser/shared/preferences.mjs +3 -1
  29. package/browser/shared/preview-server.mjs +51 -63
  30. package/browser/shared/service-location.mjs +18 -0
  31. package/browser/shared/session.mjs +3 -6
  32. package/browser/shared/workspace-address.mjs +22 -0
  33. package/browser/shared/workspace-columns.mjs +1 -2
  34. package/browser/shared/workspace-registry.mjs +6 -40
  35. package/browser/shared/workspace.mjs +3 -6
  36. package/browser/shared/writing-preferences.mjs +6 -4
  37. package/dist/{chunk-5MEXZ2UF.mjs → chunk-225ALKYI.mjs} +1 -1
  38. package/dist/{chunk-7N2LPG33.mjs → chunk-2HS7ZBPU.mjs} +1 -1
  39. package/dist/chunk-3F57LEN5.mjs +2 -0
  40. package/dist/{chunk-MKTX5PGV.mjs → chunk-43B75J5T.mjs} +1 -1
  41. package/dist/{chunk-VKSRM7EY.mjs → chunk-4B2TB5K2.mjs} +1 -1
  42. package/dist/{chunk-LV24RY2E.mjs → chunk-4K5KC6V2.mjs} +1 -1
  43. package/dist/chunk-5GK7HS62.mjs +2 -0
  44. package/dist/chunk-6GV3CBB2.mjs +2 -0
  45. package/dist/{chunk-TOE6DPLH.mjs → chunk-6NLXUKAY.mjs} +1 -1
  46. package/dist/{chunk-ZCP7BTOZ.mjs → chunk-6QBHU4DF.mjs} +1 -1
  47. package/dist/{chunk-QXVNUASA.mjs → chunk-6QWQXHAV.mjs} +1 -1
  48. package/dist/chunk-6XD6U3QA.mjs +5 -0
  49. package/dist/{chunk-DKR7LFIY.mjs → chunk-6Y3HYOBM.mjs} +1 -1
  50. package/dist/{chunk-36FIRJIF.mjs → chunk-742D2WWJ.mjs} +2 -2
  51. package/dist/{chunk-7M7UPWU6.mjs → chunk-7EFHNQ5B.mjs} +1 -1
  52. package/dist/{chunk-2GKGEVMR.mjs → chunk-7UZUQQ7Q.mjs} +25 -24
  53. package/dist/{chunk-VPVOAJ7X.mjs → chunk-AGDILNWB.mjs} +1 -1
  54. package/dist/{chunk-5IIH335D.mjs → chunk-ALZ6ZENZ.mjs} +1 -1
  55. package/dist/{chunk-G4YP7GVC.mjs → chunk-BFJGHSQN.mjs} +1 -1
  56. package/dist/{chunk-AMDGVDNV.mjs → chunk-BQNTTFPL.mjs} +1 -1
  57. package/dist/chunk-BX5AKINW.mjs +15 -0
  58. package/dist/{chunk-PAFSFVYB.mjs → chunk-BYNPEUDS.mjs} +1 -1
  59. package/dist/{chunk-74RPP2M2.mjs → chunk-C564NU2H.mjs} +1 -1
  60. package/dist/{chunk-DH6GMDNN.mjs → chunk-CRDG6EF2.mjs} +1 -1
  61. package/dist/{chunk-7XDYK3UC.mjs → chunk-E7656LR7.mjs} +1 -1
  62. package/dist/{chunk-KFZBYDIS.mjs → chunk-ECZBLQKK.mjs} +1 -1
  63. package/dist/{chunk-ZEY2RLBT.mjs → chunk-ERLAIBOA.mjs} +2 -2
  64. package/dist/{chunk-YFJU3O7G.mjs → chunk-FJLM7YE7.mjs} +1 -1
  65. package/dist/{chunk-AFGUR7SY.mjs → chunk-FNCCVGKX.mjs} +1 -1
  66. package/dist/{chunk-VNUWREXU.mjs → chunk-FOJ3K5Z3.mjs} +1 -1
  67. package/dist/{chunk-WCWYNFDF.mjs → chunk-FQ72LZHB.mjs} +1 -1
  68. package/dist/chunk-G7N7YFVD.mjs +2 -0
  69. package/dist/chunk-GIRXJBGC.mjs +2 -0
  70. package/dist/{chunk-VA3KAQZD.mjs → chunk-GKOBB6DH.mjs} +1 -1
  71. package/dist/chunk-GMJD7ID3.mjs +2 -0
  72. package/dist/chunk-GQJMVSSN.mjs +2 -0
  73. package/dist/{chunk-QZ35UAB4.mjs → chunk-GRAXKKG4.mjs} +1 -1
  74. package/dist/{chunk-SAHS56QM.mjs → chunk-HX4DLE3J.mjs} +1 -1
  75. package/dist/{chunk-2RKIHJI2.mjs → chunk-HYMEEXKN.mjs} +1 -1
  76. package/dist/{chunk-QK3VISCA.mjs → chunk-IA4PSIMD.mjs} +1 -1
  77. package/dist/chunk-IS7N3N6Q.mjs +168 -0
  78. package/dist/{chunk-CZ5KJSGK.mjs → chunk-JJNHKGTD.mjs} +1 -1
  79. package/dist/chunk-JPG2LBQM.mjs +11 -0
  80. package/dist/chunk-JYK5WJPM.mjs +2 -0
  81. package/dist/{chunk-LE44MD3O.mjs → chunk-KLQP46Y3.mjs} +1 -1
  82. package/dist/chunk-L55NOXBE.mjs +4 -0
  83. package/dist/{chunk-FXQBADMN.mjs → chunk-LCB6MARS.mjs} +1 -1
  84. package/dist/{chunk-SOD23RGL.mjs → chunk-LJLVCU6W.mjs} +8 -8
  85. package/dist/{chunk-NAGMRVW4.mjs → chunk-LKX55H76.mjs} +1 -1
  86. package/dist/chunk-LMUZQK5O.mjs +2 -0
  87. package/dist/{chunk-CG3UHDKH.mjs → chunk-M4NRDYI5.mjs} +1 -1
  88. package/dist/{chunk-HM4E43VM.mjs → chunk-MAFIGVXY.mjs} +1 -1
  89. package/dist/chunk-NLDF35P5.mjs +2 -0
  90. package/dist/{chunk-3OFXMN6N.mjs → chunk-NMF3IPYA.mjs} +1 -1
  91. package/dist/chunk-ONKC4BCC.mjs +2 -0
  92. package/dist/{chunk-CPS4SRPF.mjs → chunk-OU6ZZVBX.mjs} +2 -2
  93. package/dist/{chunk-NWRZXELO.mjs → chunk-PAREWQGB.mjs} +1 -1
  94. package/dist/{chunk-BYSF5UMM.mjs → chunk-PKVK6WFC.mjs} +2 -2
  95. package/dist/{chunk-4AMTKWDO.mjs → chunk-PUOQ65NC.mjs} +1 -1
  96. package/dist/{chunk-L7W624FD.mjs → chunk-QJAVCTY7.mjs} +1 -1
  97. package/dist/{chunk-6MPKCH3P.mjs → chunk-QNECWMGR.mjs} +1 -1
  98. package/dist/{chunk-HBAUKKQH.mjs → chunk-QP7BOHE4.mjs} +1 -1
  99. package/dist/chunk-QVGOPHOF.mjs +2 -0
  100. package/dist/{chunk-NFABH3QW.mjs → chunk-RRKJMHNX.mjs} +2 -2
  101. package/dist/chunk-S3SX3COT.mjs +20 -0
  102. package/dist/chunk-SQ3FQCCN.mjs +8 -0
  103. package/dist/{chunk-ZO7SFB2E.mjs → chunk-SQ6Y2ZMA.mjs} +2 -2
  104. package/dist/{chunk-3A5I2GJK.mjs → chunk-T4Y2ZSVX.mjs} +1 -1
  105. package/dist/{chunk-VG5JEIUQ.mjs → chunk-TKWEX4FD.mjs} +2 -2
  106. package/dist/{chunk-CCSGFCLZ.mjs → chunk-U3SSPL4K.mjs} +1 -1
  107. package/dist/{chunk-DHB7UZ7F.mjs → chunk-U5XL7D6A.mjs} +1 -1
  108. package/dist/{chunk-JXLSHVJ2.mjs → chunk-UMHUSOPJ.mjs} +1 -1
  109. package/dist/chunk-UTF7GBQU.mjs +4 -0
  110. package/dist/chunk-UTMAY3WK.mjs +2 -0
  111. package/dist/{chunk-S2Y7VI4H.mjs → chunk-V4ZHRALW.mjs} +2 -2
  112. package/dist/{chunk-GWOYMENX.mjs → chunk-V6XZ4VIR.mjs} +1 -1
  113. package/dist/{chunk-WTEGUPFG.mjs → chunk-VRPVVMRB.mjs} +1 -1
  114. package/dist/{chunk-XKOHV4H6.mjs → chunk-W5NPH5UA.mjs} +1 -1
  115. package/dist/{chunk-VGTMLQWZ.mjs → chunk-WD4U6XXT.mjs} +1 -1
  116. package/dist/{chunk-4DNHHJSL.mjs → chunk-XBNFXWAW.mjs} +1 -1
  117. package/dist/chunk-XZV72SQ4.mjs +2 -0
  118. package/dist/{chunk-QJ3TFJLQ.mjs → chunk-Y34NR5JL.mjs} +1 -1
  119. package/dist/{chunk-YBB5LWZK.mjs → chunk-YFJB534W.mjs} +1 -1
  120. package/dist/{chunk-V5JRSGZF.mjs → chunk-YN37ZDLW.mjs} +1 -1
  121. package/dist/chunk-Z6OKLRHC.mjs +2 -0
  122. package/dist/chunk-ZLS2FRS6.mjs +3 -0
  123. package/dist/{chunk-JI6T3A4Y.mjs → chunk-ZMHUDYLV.mjs} +1 -1
  124. package/dist/{chunk-KH24LWAT.mjs → chunk-ZTHICSSF.mjs} +1 -1
  125. package/dist/cli.mjs +1 -1
  126. package/dist/server.mjs +1 -1
  127. package/dist/worker.mjs +1 -1
  128. package/kit/math.js +1 -1
  129. package/kit/runtime.js +13 -2
  130. package/kit/state.js +2 -0
  131. package/kit/ui.js +5 -1
  132. package/package.json +1 -1
  133. package/release.json +7 -7
  134. package/skill/SKILL.md +13 -13
  135. package/skill/references/building-blocks.md +1 -1
  136. package/skill/references/extensions.md +45 -9
  137. package/web/assets/index-D8P8uhDW.js +15 -0
  138. package/web/assets/{index-xP0uw3a0.css → index-FLb9jCfl.css} +1 -1
  139. package/web/index.html +2 -2
  140. package/dist/chunk-46ACXOU5.mjs +0 -2
  141. package/dist/chunk-5DPABPBI.mjs +0 -2
  142. package/dist/chunk-5L26JUYA.mjs +0 -2
  143. package/dist/chunk-AEMVZYZQ.mjs +0 -166
  144. package/dist/chunk-ASAXY2DJ.mjs +0 -2
  145. package/dist/chunk-DAMVDXR3.mjs +0 -2
  146. package/dist/chunk-DLFZJJEE.mjs +0 -2
  147. package/dist/chunk-EKNHM3P4.mjs +0 -2
  148. package/dist/chunk-FCK6NXON.mjs +0 -3
  149. package/dist/chunk-FX42XU6J.mjs +0 -2
  150. package/dist/chunk-HATA2W7X.mjs +0 -2
  151. package/dist/chunk-JJNRIFND.mjs +0 -8
  152. package/dist/chunk-JL6R4FG4.mjs +0 -4
  153. package/dist/chunk-KUILYI7H.mjs +0 -4
  154. package/dist/chunk-NIW7F7LU.mjs +0 -4
  155. package/dist/chunk-OU3S466R.mjs +0 -20
  156. package/dist/chunk-QCEATQC4.mjs +0 -2
  157. package/dist/chunk-TDWJKOLN.mjs +0 -2
  158. package/dist/chunk-UGO5N3EI.mjs +0 -15
  159. package/dist/chunk-XMKHCUI6.mjs +0 -15
  160. package/dist/chunk-XQRUG4G3.mjs +0 -2
  161. package/dist/chunk-YPPJVDQI.mjs +0 -2
  162. package/dist/chunk-ZANMKAWS.mjs +0 -2
  163. package/web/assets/index-BHC5ZOMh.js +0 -15
  164. /package/dist/{chunk-N3DSBH5S.mjs → chunk-A55HN5OK.mjs} +0 -0
  165. /package/dist/{chunk-KNZJT4ET.mjs → chunk-QAIHRH3I.mjs} +0 -0
  166. /package/dist/{chunk-ZW2A6GBW.mjs → chunk-QR6A54KJ.mjs} +0 -0
  167. /package/dist/{chunk-74UW7QSL.mjs → chunk-UH4SIXTR.mjs} +0 -0
  168. /package/dist/{chunk-EKGYYOSP.mjs → chunk-W7XHE4CT.mjs} +0 -0
  169. /package/dist/{chunk-5MMN3N5J.mjs → chunk-YEY3RSFQ.mjs} +0 -0
@@ -1,6 +1,6 @@
1
1
  # Third-party notices
2
2
 
3
- @heycys/gallery 0.2.1 carries the following code and data of others, bundled into dist/ and web/, built into kit/, or as files in vendor/ and browser/.
3
+ @heycys/gallery 0.2.3 carries the following code and data of others, bundled into dist/ and web/, built into kit/, or as files in vendor/ and browser/.
4
4
 
5
5
  ## @asamuzakjp/css-color 5.1.11
6
6
 
@@ -1,3 +1,4 @@
1
+ import { documentKey, workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { CARD, RAIL, cardTops, markerLeft, markerTop, newAnnotationId, sameWords, squash, stackItems, tidy } from "../../../shared/annotations.mjs";
2
3
  import { t } from "../../../shared/i18n/index.mjs";
3
4
  import { SHARES_HEIGHT, standsAsPicture } from "../../components/registry.mjs";
@@ -106,7 +107,7 @@ const NOTICE_ROOM = 70;
106
107
  const IN_ROW = [...SHARES_HEIGHT].map((kind) => `[data-ge-unit-kind="${kind}"] > *`).join();
107
108
 
108
109
  export function createAnnotate({ slug, token, isEditing, picked, unpick = () => {}, wholePage = () => [], crossTexts, idFor, changed = () => {}, removed = () => {}, say }) {
109
- const stashKey = `gallery-edit:${slug}:notes`; // what could not be saved (offline), sent by the next page that reads them
110
+ const stashKey = `gallery-edit:${documentKey(slug)}:notes`; // what could not be saved (offline), sent by the next page that reads them
110
111
  let reading = null; // the first read from the server, under way or done (taken back when it failed)
111
112
  let counted = false; // that read is in and what it brought found: the page can say how many it has
112
113
  let items = new Map(); // id -> { id, created, updated, text, anchor } in page order, as the server last said, with the page's own changes
@@ -189,7 +190,7 @@ export function createAnnotate({ slug, token, isEditing, picked, unpick = () =>
189
190
  chain = run.catch(() => {});
190
191
  return run;
191
192
  };
192
- const fetchAll = () => fetch(`${ROUTE}?slug=${encodeURIComponent(slug)}`, { headers: { "X-Gallery-Token": token }, cache: "no-store" })
193
+ const fetchAll = () => fetch(workspaceAddress(`${ROUTE}?slug=${encodeURIComponent(slug)}`), { headers: { "X-Gallery-Token": token }, cache: "no-store" })
193
194
  .then((response) => response.json()).catch(() => ({ ok: false, reason: "offline" }));
194
195
  const wire = (item) => ({ id: item.id, created: item.created, updated: item.updated, text: item.text, anchor: item.anchor });
195
196
 
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { GAP, GRID, cellAt, fitHeight, gridFor, moved, pushDown, sizeFor, spanOf } from "../../../shared/board-places.mjs";
2
3
  import { cellsOf } from "../../../shared/card-size.mjs";
3
4
  import { language, t } from "../../../shared/i18n/index.mjs";
@@ -793,7 +794,7 @@ export function startBoard({ map, reload, hosted = false }) {
793
794
  menu.close();
794
795
  let listed = [];
795
796
  try {
796
- listed = (await (await fetch(`/__gallery/pages/${slug}/library?language=${language}`)).json()).blocks ?? [];
797
+ listed = (await (await fetch(workspaceAddress(`/__gallery/pages/${slug}/library?language=${language}`))).json()).blocks ?? [];
797
798
  } catch {
798
799
  say(t("连不上本地服务"), "error");
799
800
  return false;
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { Z } from "../../../shared/layers.mjs";
2
3
  import { language, t } from "../../../shared/i18n/index.mjs";
3
4
  import { createMenu } from "../menu.mjs";
@@ -59,7 +60,7 @@ export const formOpen = () => shown !== null;
59
60
  /** The workspace's documents, by title: [{ slug, title }] (none where the outline cannot be read). */
60
61
  async function documents() {
61
62
  try {
62
- const answer = await (await fetch("/__gallery/workspace/outline")).json();
63
+ const answer = await (await fetch(workspaceAddress("/__gallery/workspace/outline"))).json();
63
64
  return Object.values(answer.reports ?? {}).filter((report) => !report.instructions && report.kind !== "link").map(({ slug, title }) => ({ slug, title: title || slug }))
64
65
  .sort((a, b) => a.title.localeCompare(b.title, "zh-CN"));
65
66
  } catch {
@@ -1,3 +1,4 @@
1
+ import { localPath } from "../../shared/workspace-address.mjs";
1
2
  import { escapeAttribute, escapeText, escapeTyped, unescapeText } from "../../shared/escape.mjs";
2
3
  import { formatRange, isInkClass, isUnderlineClass, markValue } from "./marks.mjs";
3
4
  import { ZWSP, inlineItems, spaceAround } from "../../shared/inline-spacing.mjs";
@@ -294,13 +295,13 @@ export function newMention(doc, { href, title, icon = null, drawn = null }, asse
294
295
  export function mentionOf(chip) {
295
296
  const img = chip.querySelector(`img.${MENTION_ICON}`);
296
297
  const src = img?.getAttribute("data-ge-src") ?? img?.getAttribute("src") ?? "";
297
- const served = /^\/__gallery\/edit\/asset\/([^/]+)\/(assets\/[^/?#]+)$/.exec(src);
298
+ const served = /^\/__gallery\/edit\/asset\/([^/]+)\/(assets\/[^/?#]+)$/.exec(localPath(src));
298
299
  return {
299
300
  href: chip.getAttribute("href") ?? "",
300
301
  title: chip.textContent.replace(/\s+/g, " ").trim(),
301
302
  icon: served ? served[2] : /^assets\/[^/]+$/.test(src) ? src : null,
302
303
  slug: served ? decodeURIComponent(served[1]) : null,
303
- drawn: src.startsWith("/__gallery/edit/icon?") ? src : null,
304
+ drawn: localPath(src).startsWith("/__gallery/edit/icon?") ? src : null,
304
305
  };
305
306
  }
306
307
 
@@ -1,3 +1,4 @@
1
+ import { localPath } from "../../shared/workspace-address.mjs";
1
2
  import { startBoard } from "./board/index.mjs";
2
3
  import { relayTo } from "./notices.mjs";
3
4
  import { connectWorkbench } from "./session.mjs";
@@ -21,7 +22,7 @@ import { connectWorkbench } from "./session.mjs";
21
22
  // document or another page, or a block's context.workspace.open: doc-open-request) and key. Opened on its own (not in a frame of the workbench), it has no
22
23
  // session: its board edits all the same, and the live script reloads it.
23
24
 
24
- const slug = decodeURIComponent(location.pathname.split("/")[2] ?? "");
25
+ const slug = decodeURIComponent(localPath(location.pathname).split("/")[2] ?? "");
25
26
  const IDLE = { editing: false, pending: false, saving: false, offline: false, conflicts: 0, unfit: 0, changed: false };
26
27
  // Nothing stands over a custom page as a modal.
27
28
  const NO_MODALS = { open: () => [], listen() {}, closeTop: () => false };
@@ -94,8 +95,8 @@ if (session) {
94
95
  window.open(url.href, "_blank", "noopener");
95
96
  return;
96
97
  }
97
- const custom = /^\/pages\/([a-z0-9-]+)\/?$/.exec(url.pathname)?.[1];
98
- const page = /^\/generated\/([a-z0-9-]+)\.html$/.exec(url.pathname)?.[1] ?? /^\/([a-z0-9-]+)\/(?:doc|report)\.html$/.exec(url.pathname)?.[1];
98
+ const custom = /^\/pages\/([a-z0-9-]+)\/?$/.exec(localPath(url.pathname))?.[1];
99
+ const page = /^\/generated\/([a-z0-9-]+)\.html$/.exec(localPath(url.pathname))?.[1] ?? /^\/([a-z0-9-]+)\/(?:doc|report)\.html$/.exec(localPath(url.pathname))?.[1];
99
100
  if (custom) {
100
101
  event.preventDefault();
101
102
  session.open({ hash: `@${custom}` });
@@ -1,7 +1,9 @@
1
+ import { documentKey, workspaceStoragePrefix } from "../../shared/workspace-address.mjs";
2
+ import { workspaceAddress } from "../../shared/workspace-address.mjs";
1
3
  // First: what the browser keeps under the names before 2026-10-28 carried to the new ones (the
2
4
  // editor's gallery-edit:… keys, src/shared/renamed-storage.mjs), before any module below reads them.
3
5
  import "../../shared/renamed-storage.mjs";
4
- import { OUTER, can, captureLayout, editableAhead, firstHost, isHost, limitText, markFrames, markKinds, matchesSource, newHost, newMention, ownHTML, plainText, prepareBlock, splitBlock, textEnd, tidyEmptied, unitElement, withLeads } from "./core.mjs";
6
+ import { OUTER, can, captureLayout, editableAhead, firstByAttribute, firstHost, isHost, limitText, markFrames, markKinds, matchesSource, newHost, newMention, ownHTML, plainText, prepareBlock, splitBlock, textEnd, tidyEmptied, unitElement, withLeads } from "./core.mjs";
5
7
  import { createBlocks } from "./blocks.mjs";
6
8
  import { hiddenByFold, reveal } from "./folded.mjs";
7
9
  import { createFlags } from "./flags.mjs";
@@ -61,7 +63,7 @@ import { decoded, fullAddress, isFileLink } from "../../shared/page-address.mjs"
61
63
  import { goBackTo, placeIn, unmoved } from "../../shared/reading-place.mjs";
62
64
  import { heldList, holdPages } from "./links/pages.mjs";
63
65
  import { hideBlocks } from "./offered.mjs";
64
- import { local, storageOf } from "./storage.mjs";
66
+ import { storageOf } from "./storage.mjs";
65
67
  import { createTodoWrites } from "./todo-writes.mjs";
66
68
  import { language, t } from "../../shared/i18n/index.mjs";
67
69
 
@@ -131,7 +133,9 @@ let conflicts = null; // what the file refused, for the reader to decide (confli
131
133
  let unsaveable = { blank: 0, unplaced: 0 }; // blocks the page cannot save as they are
132
134
  let notice = null; // { text, time }: what a commit just said, to say again if the page reloads by itself
133
135
 
134
- const session = storageOf(() => sessionStorage);
136
+ const storageScope = { prefix: workspaceStoragePrefix() };
137
+ const local = storageOf(() => localStorage, storageScope);
138
+ const session = storageOf(() => sessionStorage, storageScope);
135
139
 
136
140
  // This tab, kept over reloads (sessionStorage). Each tab keeps what waits to be saved (the crash
137
141
  // buffer, autosave.mjs) under its own id, so two tabs of a report never overwrite each other's; the
@@ -140,7 +144,7 @@ const session = storageOf(() => sessionStorage);
140
144
  const newTab = () => Math.random().toString(36).slice(2, 10);
141
145
  let TAB = session.get(keys.tab) ?? newTab();
142
146
  session.set(keys.tab, TAB);
143
- const channel = (() => { try { return new BroadcastChannel(`gallery-edit:${MAP.slug}`); } catch { return null; } })();
147
+ const channel = (() => { try { return new BroadcastChannel(`gallery-edit:${documentKey(MAP.slug)}`); } catch { return null; } })();
144
148
  channel?.addEventListener("message", (event) => {
145
149
  if (event.data?.type === "who") channel.postMessage({ type: "here", tab: TAB });
146
150
  });
@@ -397,10 +401,12 @@ function enter() {
397
401
  if (editing) return;
398
402
  // Two passes: every block made editable (or not) as the page was served, then every block rebuilt
399
403
  // (core.mjs editableAhead: a contenteditable set after blocks were rebuilt lays the page out again).
404
+ // Resolve the initial hosts in one scan; a query for each block repeatedly walks a long page.
405
+ const marked = firstByAttribute(document, "data-ge-block");
400
406
  const nodes = new Map();
401
407
  for (const block of MAP.blocks) {
402
408
  if (HOSTS[block.kind]?.closed) continue;
403
- const node = document.querySelector(`[data-ge-block="${block.id}"]`);
409
+ const node = marked.get(String(block.id));
404
410
  if (!node) continue;
405
411
  if (HOSTS[block.kind]?.tex) node.setAttribute("contenteditable", "true"); // a display formula (components/math/)
406
412
  else if (!matchesSource(node, block.inner)) {
@@ -1411,7 +1417,7 @@ async function goTo({ slug = null, block = null, hash = null }, href) {
1411
1417
  if (block) jumpTo(block);
1412
1418
  } else if (SOLO) window.open(href, "_blank", "noopener");
1413
1419
  else if (window.parent !== window) workbench?.open(hash === null ? { target: slug, block } : { hash });
1414
- else if (hash === null || (await leadsToPage(hash))) COMMANDS["prepare-close"]().then(() => location.assign(hash === null ? `/generated/${slug}.html${block ? `#${encodeURIComponent(block)}` : ""}` : href));
1420
+ else if (hash === null || (await leadsToPage(hash))) COMMANDS["prepare-close"]().then(() => location.assign(hash === null ? workspaceAddress( `/generated/${slug}.html${block ? `#${encodeURIComponent(block)}` : ""}` ) : href));
1415
1421
  }
1416
1422
 
1417
1423
  function connectParts() {
@@ -1500,7 +1506,7 @@ function connect() {
1500
1506
  event.preventDefault();
1501
1507
  if (page) {
1502
1508
  if (window.parent !== window && !SOLO) workbench?.open({ hash: `@${page}` });
1503
- else COMMANDS["prepare-close"]().then(() => location.assign(`/pages/${page}`));
1509
+ else COMMANDS["prepare-close"]().then(() => location.assign(workspaceAddress(`/pages/${page}`)));
1504
1510
  } else goTo({ slug, block }, `/generated/${slug}.html${block ? `#${encodeURIComponent(block)}` : ""}`);
1505
1511
  });
1506
1512
  relayTo(workbench ? workbench.notice : null, workbench ? workbench.noticeRoom : null); // the workbench shows the page's notices, above the page's bottom panels
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { Z } from "../../../shared/layers.mjs";
2
3
  import { escapeMarkup } from "../../../shared/escape.mjs";
3
4
  import { icon } from "./icons.mjs";
@@ -198,7 +199,7 @@ export function connectPicker({ slug, token, notify }) {
198
199
  server = { slug, token };
199
200
  say = notify;
200
201
  setEmojiLibraryStorage({
201
- loadEmojiLibrary: () => fetch(`/__gallery/edit/emoji-library`).then((response) => response.json()).then((answer) => answer.list ?? []).catch(() => []),
202
- addEmoji: async ({ name, url }) => (await post("emoji-library", { name, url })).entry,
202
+ loadEmojiLibrary: () => fetch(workspaceAddress(`/__gallery/edit/emoji-library`)).then((response) => response.json()).then((answer) => (answer.list ?? []).map(entry => ({ ...entry, url: workspaceAddress(entry.url) }))).catch(() => []),
203
+ addEmoji: async ({ name, url }) => { const entry = (await post("emoji-library", { name, url })).entry; return { ...entry, url: workspaceAddress(entry.url) }; },
203
204
  });
204
205
  }
@@ -752,17 +752,6 @@ function opens(event) {
752
752
  // workspace's page of one folder name, opened as any web address. A tab
753
753
  // of its own opens with the click, the list read after, for only what the click itself opens does the
754
754
  // browser leave behind the page.
755
- async function followElsewhere(address, tab) {
756
- const opened = tab ? window.open("", "_blank") : null;
757
- const list = await readPages(use.own());
758
- const page = list && placeIn(list, address);
759
- const here = page && fullAddress(location.origin, page, list.links);
760
- if (opened) {
761
- opened.opener = null;
762
- opened.location.replace(here ?? address);
763
- } else if (page) use.go(page, here);
764
- else openTab(address);
765
- }
766
755
 
767
756
  // Go where `href`, a link of this page that is no anchor, leads, read as its file reads it, so that
768
757
  // doc.html beside it is this document: a document's address there, with `tab` in a tab of its own, a
@@ -771,7 +760,7 @@ async function followElsewhere(address, tab) {
771
760
  function follow(href, tab) {
772
761
  const address = fromFile(href, use.slug, location.origin);
773
762
  if (!address) return;
774
- if (elsewhere(address, location.origin)) return void followElsewhere(address, tab);
763
+ if (elsewhere(address, location.origin)) return void openTab(address);
775
764
  const place = placeOf(address);
776
765
  const file = referencedFile(href);
777
766
  if (place && !tab) use.go(place, address);
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../../shared/workspace-address.mjs";
1
2
  import { iconUrl } from "../../../shared/emoji/icon-value.mjs";
2
3
  import { escapeAttribute } from "../../../shared/escape.mjs";
3
4
  import { addressOn, decoded, hashAddress } from "../../../shared/page-address.mjs";
@@ -41,7 +42,7 @@ let shown = () => null; // this page, as readPages takes it
41
42
  * null when the list cannot be read (`webs`: the links of the sidebar's web links, which are no pages). `own`: this page, { slug, title } as it shows it.
42
43
  */
43
44
  export async function readPages(own = null) {
44
- const listed = await fetch(OUTLINE).then((answer) => answer.json()).catch(() => null);
45
+ const listed = await fetch(workspaceAddress(OUTLINE)).then((answer) => answer.json()).catch(() => null);
45
46
  if (!listed?.reports) return null;
46
47
  const titleOf = (slug) => (slug === own?.slug && own.title ? own.title : listed.reports[slug]?.title);
47
48
  const pages = [];
@@ -80,7 +81,7 @@ export function heldList() {
80
81
 
81
82
  /** Whether document `slug` is in the workspace's trash (not when the trash cannot be read). */
82
83
  export async function trashed(slug) {
83
- const listed = await fetch(TRASH).then((answer) => answer.json()).catch(() => null);
84
+ const listed = await fetch(workspaceAddress(TRASH)).then((answer) => answer.json()).catch(() => null);
84
85
  return inTrash(listed?.entries ?? [], slug);
85
86
  }
86
87
 
@@ -3,6 +3,7 @@
3
3
  // heading's block id, a tab's area and tab), the reports folded last kept and the oldest let go. The
4
4
  // headings folded on the page (fold/) are kept the same way under a key of their own (`key`).
5
5
 
6
+ import { documentKey } from "../../../shared/workspace-address.mjs";
6
7
  import { KEYS } from "../../../shared/renamed-storage.mjs";
7
8
 
8
9
  export const FOLDS_KEY = KEYS.outlineFolds;
@@ -18,6 +19,7 @@ function read(storage, key) {
18
19
 
19
20
  /** The keys folded in report `slug` (`storage`: { get, set }, page/storage.mjs), kept under `key`. */
20
21
  export function foldsOf(storage, slug, key = FOLDS_KEY) {
22
+ slug = documentKey(slug);
21
23
  const keys = read(storage, key).folds[slug];
22
24
  return new Set(Array.isArray(keys) ? keys.filter((key) => typeof key === "string") : []);
23
25
  }
@@ -27,6 +29,7 @@ export function foldsOf(storage, slug, key = FOLDS_KEY) {
27
29
  * folded last; none folded forgets the report. At most KEEP reports, the oldest going first.
28
30
  */
29
31
  export function keepFolds(storage, slug, folded, present = null, key = FOLDS_KEY) {
32
+ slug = documentKey(slug);
30
33
  const value = read(storage, key);
31
34
  const keys = [...folded].filter((each) => !present || present.has(each));
32
35
  delete value.folds[slug]; // put back last: the report folded last
@@ -1,3 +1,4 @@
1
+ import { workspaceAddress } from "../../shared/workspace-address.mjs";
1
2
  import { t, told } from "../../shared/i18n/index.mjs";
2
3
  // The page's one way to talk to the preview server: a POST of JSON with the report's token
3
4
  // (postJson: the request, a network error rejecting as fetch does; postAnswer: its answer read, a
@@ -5,7 +6,7 @@ import { t, told } from "../../shared/i18n/index.mjs";
5
6
  // says (failure) and how an upload carries a file (blobBase64).
6
7
 
7
8
  /** POST `body` as JSON to `route` with `token` (the map's); resolves with the Response. */
8
- export const postJson = (route, token, body, { keepalive = false } = {}) => fetch(route, {
9
+ export const postJson = (route, token, body, { keepalive = false } = {}) => fetch(workspaceAddress(route), {
9
10
  method: "POST",
10
11
  headers: { "Content-Type": "application/json", "X-Gallery-Token": token },
11
12
  body: JSON.stringify(body),
@@ -1,8 +1,9 @@
1
+ import { workspaceAddress } from "../../shared/workspace-address.mjs";
1
2
  import { t } from "../../shared/i18n/index.mjs";
2
3
  import { addressOn, elsewhere, fromFile, isPageAddress, mayBePage, pageAddress, workbenchHash } from "../../shared/page-address.mjs";
3
4
  import { inTrash, linkedPages, pageLinks } from "../../shared/page-links.mjs";
4
5
  import { entryOfKey } from "../../shared/extension-sidebar.mjs";
5
- import { SHELL, tell } from "./present-shell.mjs";
6
+ import { tell } from "./present-shell.mjs";
6
7
 
7
8
  // The links of a page shown in 演示视图 (?present: src/server/routes.mjs), which has no editor and whose
8
9
  // frame the presentation holds (src/workbench/app/features/present/). A link to a document or a block
@@ -21,11 +22,11 @@ import { SHELL, tell } from "./present-shell.mjs";
21
22
 
22
23
  const own = pageAddress(location.href)?.slug ?? null;
23
24
  const openTab = (address) => window.open(address, "_blank", "noopener");
24
- const read = (route) => fetch(`/__gallery/workspace/${route}`).then((answer) => answer.json()).catch(() => null);
25
+ const read = (route) => fetch(workspaceAddress(`/__gallery/workspace/${route}`)).then((answer) => answer.json()).catch(() => null);
25
26
 
26
27
  function go({ slug, block }) {
27
28
  const fragment = block ? `#${encodeURIComponent(block)}` : "";
28
- if (slug !== own) location.assign(`/generated/${slug}.html?present${SHELL ? `&shell=${encodeURIComponent(SHELL)}` : ""}${fragment}`); // a draft's presentation keeps the workbench around it (present-shell.mjs)
29
+ if (slug !== own) location.assign(workspaceAddress(`/generated/${slug}.html?present${fragment}`)); // a draft's presentation keeps the workbench around it (present-shell.mjs)
29
30
  else if (block) dispatchEvent(new CustomEvent("doc-jump-request", { detail: { hash: fragment } })); // the kit's jump, a step of the history as a link #<block id> is
30
31
  }
31
32
 
@@ -1,63 +1,8 @@
1
1
  import { pageAddress } from "../../shared/page-address.mjs";
2
- import { KEEPS_KEYS, stepSection } from "../../shared/present-step.mjs";
3
- import { PROTOCOL, PROTOCOLS, VERSION } from "../../shared/session.mjs";
2
+ import { PROTOCOL, VERSION } from "../../shared/session.mjs";
4
3
 
5
- // What a page shown in 演示视图 (?present: src/server/routes.mjs, no editor) says to the presentation around it and
6
- // hears from it. The presentation of a document of this workspace is a frame of the same origin, which it reaches into (its
7
- // scroll, its keys, its theme); that of a draft of another workspace (docs/workbench.md "Drafts of other workspaces") is a frame
8
- // of that workspace's server, named by the page's address (?shell=<the workbench's origin>) as the editor's page names it
9
- // (session.mjs): this page then asks its own server whether that is a Gallery server of this computer (GET
10
- // /__gallery/workspace/shell) and talks to it alone once it is, holding what it says until then; one that is not hears nothing.
11
- // It tells where the page is scrolled (`present-view`: { x, y }, with `ready` once the page can be spoken to) and the keys pressed
12
- // in it that are the presentation's (`present-key`: { key, ctrlKey, metaKey, altKey, kept (a control holds the key), prevented }),
13
- // and does what it is asked: `theme` (the workbench's, light or dark), `go-section` (上一节 / 下一节) and `scroll` ({ left, top }: the wheel). Every message is the session
14
- // protocol's envelope (src/shared/session.mjs).
15
-
16
- /** The origin of a workbench of another origin showing this page (?shell=<origin>), or null: this one's. */
17
- export const SHELL = (() => {
18
- const named = new URLSearchParams(location.search).get("shell");
19
- return named && named !== location.origin ? named : null;
20
- })();
21
-
22
- const vouched = (origin) => fetch(`/__gallery/workspace/shell?origin=${encodeURIComponent(origin)}`, { cache: "no-store" })
23
- .then((answer) => answer.json()).then((answer) => answer?.shell === true, () => false);
24
-
25
- let parentOrigin = SHELL ? null : location.origin;
26
- let held = SHELL ? [] : null;
27
-
28
- /** Tell the presentation around the page (`window.parent`) something; held until its origin is vouched for, dropped when it is not. */
4
+ // Presentations and their documents share one origin; links may notify their parent.
29
5
  export function tell(type, fields = {}) {
30
6
  if (window.parent === window) return;
31
- if (held) return void held.push([type, fields]);
32
- if (!parentOrigin) return;
33
- window.parent.postMessage({ protocol: PROTOCOL, v: VERSION, type, slug: pageAddress(location.href)?.slug ?? "", ...fields }, parentOrigin);
34
- }
35
-
36
- function bridge() {
37
- let frame = 0;
38
- const view = (ready = false) => tell("present-view", { x: scrollX, y: scrollY, ...(ready ? { ready: true } : {}) });
39
- addEventListener("scroll", () => { frame ||= requestAnimationFrame(() => { frame = 0; view(); }); }, { passive: true });
40
- addEventListener("keydown", (event) => {
41
- if (event.isComposing) return;
42
- tell("present-key", { key: event.key, ctrlKey: event.ctrlKey, metaKey: event.metaKey, altKey: event.altKey, kept: Boolean(event.target?.closest?.(KEEPS_KEYS)), prevented: event.defaultPrevented });
43
- });
44
- addEventListener("message", (event) => {
45
- const message = event.data;
46
- if (event.source !== window.parent || event.origin !== parentOrigin || !PROTOCOLS.includes(message?.protocol) || message.v !== VERSION || message.type !== "command") return;
47
- if (message.name === "theme") dispatchEvent(new CustomEvent("doc-theme-request", { detail: { theme: message.theme === "dark" ? "dark" : "light" } })); // the kit's ui.js
48
- else if (message.name === "go-section") stepSection(window, message.direction > 0 ? 1 : -1);
49
- else if (message.name === "scroll") scrollBy({ left: Number(message.left) || 0, top: Number(message.top) || 0 }); // the wheel over the presentation's ink
50
- });
51
- view(true);
52
- }
53
-
54
- if (SHELL && window.parent !== window) {
55
- vouched(SHELL).then((ok) => {
56
- const waiting = held;
57
- held = null;
58
- if (!ok) return;
59
- parentOrigin = SHELL;
60
- for (const [type, fields] of waiting) tell(type, fields);
61
- bridge();
62
- });
7
+ window.parent.postMessage({ protocol: PROTOCOL, v: VERSION, type, slug: pageAddress(location.href)?.slug ?? "", ...fields }, location.origin);
63
8
  }
@@ -5,15 +5,7 @@ import { lookOf, lookOfElement } from "../../shared/document-look.mjs";
5
5
  // tells the workbench its state, never the report's content. Nothing happens when the page is not
6
6
  // embedded. The protocol is src/shared/session.mjs; how a workbench uses it, docs/workbench.md.
7
7
  //
8
- // A workbench of another workspace shows a draft of this one from its own origin (docs/workbench.md
9
- // "Drafts of other workspaces"), naming that origin in the frame's address (?shell=<origin>): the page
10
- // then asks its own server whether that is a Gallery server of this computer (GET
11
- // /__gallery/workspace/shell: on the registry, running, its token and process as it announces them),
12
- // and talks to it alone once it is, holding what it says until then; one that is not hears nothing and
13
- // is heard by nothing, so that no other page of this computer drives a document by framing it. Going,
14
- // such a page says `bye` (the workbench of another origin cannot see it go).
15
- //
16
- // A message counts only from the parent window, of this origin (or the shell's, vouched for) and protocol. A command names the
8
+ // A message counts only from the parent window, of this exact origin and protocol. A command names the
17
9
  // report it is meant for: one for another report (the iframe went to another page meanwhile) is
18
10
  // refused. Every command gets exactly one reply, once it is done. Besides the editor's commands the
19
11
  // session itself answers five: `theme` (show the report light or dark, or as the reader chose, without
@@ -27,16 +19,6 @@ import { lookOf, lookOfElement } from "../../shared/document-look.mjs";
27
19
 
28
20
  /** A page opened in a frame on its own (?solo), not as the workbench's report: no session, its own notices. */
29
21
  export const SOLO = new URLSearchParams(location.search).has("solo");
30
- /** The origin of a workbench of another workspace showing this page (?shell=<origin>), or null: this one's. */
31
- const SHELL = (() => {
32
- const named = new URLSearchParams(location.search).get("shell");
33
- return named && named !== location.origin ? named : null;
34
- })();
35
-
36
- // Whether this page's server vouches for the workbench at `origin` (GET /__gallery/workspace/shell).
37
- const vouched = (origin) => fetch(`/__gallery/workspace/shell?origin=${encodeURIComponent(origin)}`, { cache: "no-store" })
38
- .then((answer) => answer.json()).then((answer) => answer?.shell === true, () => false);
39
-
40
22
  /**
41
23
  * slug: the report's; snapshot(): { title, state } (state: STATE_FLAGS of src/shared/session.mjs);
42
24
  * commands: name -> async (message) => result ({ ok, … }); modals: what stands open over the page
@@ -56,27 +38,11 @@ const vouched = (origin) => fetch(`/__gallery/workspace/shell?origin=${encodeURI
56
38
  export function connectWorkbench({ slug, snapshot, commands, modals }) {
57
39
  if (window.parent === window || SOLO) return null;
58
40
  centreCards(modals);
59
- // The workbench's origin: this one's, or the shell's once this page's server vouched for it (null till
60
- // then: what the page says is held, and is dropped when it does not).
61
- let parentOrigin = SHELL ? null : location.origin;
62
- let held = SHELL ? [] : null;
63
- if (SHELL) {
64
- vouched(SHELL).then((ok) => {
65
- const waiting = held;
66
- held = null;
67
- if (!ok) return;
68
- parentOrigin = SHELL;
69
- for (const [type, fields, protocols] of waiting) send(type, fields, protocols);
70
- // A workbench of another origin sees no pagehide of this page: it is told.
71
- addEventListener("pagehide", () => send("bye", {}), { once: true });
72
- });
73
- }
41
+ const parentOrigin = location.origin;
74
42
  // The id the workbench speaks (src/shared/session.mjs, OLD_PROTOCOL): null until its first command,
75
43
  // and meanwhile every message goes under both, so that a workbench of either version hears it.
76
44
  let spoken = null;
77
45
  const send = (type, fields, protocols = spoken ? [spoken] : [PROTOCOL, OLD_PROTOCOL]) => {
78
- if (held) return void held.push([type, fields, protocols]);
79
- if (!parentOrigin) return;
80
46
  for (const protocol of protocols) window.parent.postMessage({ protocol, v: VERSION, type, slug, ...fields }, parentOrigin);
81
47
  };
82
48
  let last = null;
@@ -141,7 +107,7 @@ export function connectWorkbench({ slug, snapshot, commands, modals }) {
141
107
 
142
108
  addEventListener("message", async (event) => {
143
109
  const message = event.data;
144
- if (event.source !== window.parent || !parentOrigin || event.origin !== parentOrigin) return;
110
+ if (event.source !== window.parent || event.origin !== parentOrigin) return;
145
111
  if (!PROTOCOLS.includes(message?.protocol) || message.v !== VERSION || message.type !== "command") return;
146
112
  spoken = message.protocol;
147
113
  const run = Object.hasOwn(all, message.name) ? all[message.name] : null;
@@ -4,11 +4,11 @@
4
4
  // and the page works without (a list used lately is then kept for the page's life only).
5
5
 
6
6
  /** A storage area (`area()`: localStorage or sessionStorage) whose calls never throw. */
7
- export const storageOf = (area) => ({
8
- get(key) { try { return area().getItem(key); } catch { return null; } },
9
- set(key, value) { try { area().setItem(key, value); return true; } catch { return false; } },
10
- remove(key) { try { area().removeItem(key); } catch {} },
11
- keys() { try { return Object.keys(area()); } catch { return []; } },
7
+ export const storageOf = (area, { prefix = "" } = {}) => ({
8
+ get(key) { try { return area().getItem(prefix + key); } catch { return null; } },
9
+ set(key, value) { try { area().setItem(prefix + key, value); return true; } catch { return false; } },
10
+ remove(key) { try { area().removeItem(prefix + key); } catch {} },
11
+ keys() { try { return Object.keys(area()).filter(key => key.startsWith(prefix)).map(key => key.slice(prefix.length)); } catch { return []; } },
12
12
  });
13
13
 
14
14
  /** localStorage, safe. */
@@ -11,7 +11,7 @@ import { attr, walk } from "./html-tree.mjs";
11
11
  import { HOST, NET, netHostsOf } from "./net-policy.mjs";
12
12
  import { inLanguage, refusal, said, saying, told } from "./i18n/index.mjs";
13
13
  import { DEFAULT_PACK, OWN_SCOPE, PACK_FILE, PACK_ID, readPackManifest } from "./pack-manifest.mjs";
14
- import { DEVICE_NAMES, PACKAGE_MANIFEST, WORKSPACE_MARK, findWorkspace, layoutOf, markerOf, packageFolders } from "./workspace.mjs";
14
+ import { DEVICE_NAMES, PACKAGE_MANIFEST, WORKSPACE_MARK, defaultWorkspace, layoutOf, markerOf, packageFolders } from "./workspace.mjs";
15
15
 
16
16
  // 组件包, custom blocks (自定义块) as the UI and the Agent's documents call them: what the workspace's
17
17
  // component packages say of themselves to Gallery, read from
@@ -466,7 +466,7 @@ function readPackage(id, folder) {
466
466
  }
467
467
  }
468
468
 
469
- const workspaceRoot = () => findWorkspace().root;
469
+ const workspaceRoot = () => defaultWorkspace();
470
470
 
471
471
  // A file edited by hand holds what it holds: of its lists, what can be an id is read, once each, and
472
472
  // the rest is passed over, one that names nothing there doing nothing. An id may be of a package that
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { descendantsIn, outlineAndFile, parentIn, placeIn, readOutline, sharedOutline, withOutlineLock, writeOutline, writtenAs } from "./outline.mjs";
5
+ import { descendantsIn, outlineAndFile, parentIn, placeIn, readOutline, withOutlineLock, writeOutline, writtenAs } from "./outline.mjs";
6
6
  import { DOCS, GROUP_LIMITS, isForeign, isTop, newGroupId, topIn, topOf } from "./outline-shape.mjs";
7
7
  import { findPreviewServer } from "./preview-server.mjs";
8
8
  import { idsOf, shownIcon } from "./workspace-columns.mjs";
@@ -10,18 +10,11 @@ import { nameStops, workspaceLabels, workspaceName } from "./workspace-name.mjs"
10
10
  import { isWorkspace, readRegistry, workspaceId, workspaceKey } from "./workspace-registry.mjs";
11
11
  import { defaultWorkspace, documentFiles, isSlugName, listDocumentSlugs, readLink } from "./workspace.mjs";
12
12
 
13
- // The drafts of other workspaces in a workspace's sidebar (notes/plans/workspaces-links-plan-2026-11-08.md
14
- // §2.2, §2.5, §7.2; docs/workbench.md "Drafts of other workspaces"): an entry of the outline written
15
- // "~<ws id>/<slug>" (./outline-shape.mjs), the workspace's id its path's hash on the machine's registry
16
- // (./workspace-registry.mjs), the draft its document <root>/<slug>/doc.html there. Only where the sidebar
17
- // shows it is this workspace's to keep: the draft's folder never moves, and its own workspace's tree is
18
- // never written (its server shows it as it was). Every registered workspace has a column of its own
19
- // (./workspace-columns.mjs, ./outline.mjs), "whole" (all its documents) unless the user picked a document
20
- // of it in 添加其他 Gallery 项目… (POST draft-add) while it had none shown: then it is `picked`, holding that
21
- // document and those under it alone, and what an Agent makes or opens there later (gallery new, gallery
22
- // open in another workspace than the default one: joinDefaultTree). The column the user removed is
23
- // `hidden`; an Agent's gallery new or open there, or the user's pick, shows it again. A draft the user took
24
- // out of a whole column (从文档树移除) is in its `left`, not filled in again until it is put back here.
13
+ // Registered document folders share the default home's sidebar and outline. A foreign entry is
14
+ // ~<workspace-id>/<slug>; its files, history, trash and exports remain in that folder.
15
+ // Every registered folder contributes its documents. Ordering and hierarchy have one global owner.
16
+ // addDraft/addWorkspace register or reveal a folder through the existing outline operations;
17
+ // joinDefaultTree places documents created by CLI commands in that same tree.
25
18
  // A draft whose workspace is not on the registry, two registered folders sharing its id (six hex digits
26
19
  // of a hash: a clash is refused, never guessed), or whose folder is gone stays in the tree, shown grey
27
20
  // (找不到底稿) until the user removes it. Reads other workspaces' files only; writes this one's outline.
@@ -193,7 +186,7 @@ const shownColumn = ({ hidden, ...column }) => column;
193
186
  * (documents: a web link of that workspace is none, nor a draft of a third its tree shows, the drafts under
194
187
  * one going under the draft above it), those the tree does not hold yet. Its workspace's column is shown
195
188
  * again when the user had removed it (hidden). How, `pick` says:
196
- * - false (an Agent's gallery new or open there: joinDefaultTree, POST draft-join): one the outline holds
189
+ * - false (an Agent's gallery new or open there: joinDefaultTree, POST draft-add): one the outline holds
197
190
  * (the file, or a whole column as it is read) stays where it is; else (a picked column) under its own
198
191
  * parent's entry when the tree holds that, else last in its workspace's column;
199
192
  * - true (the user's pick in 添加其他 Gallery 项目…, `after`: the column whose menu it came from): one shown
@@ -228,9 +221,13 @@ export async function addDraft(treeRoot, draftRoot, slug, { changed = () => {},
228
221
  return { parent, column: top === null || top === "" ? DOCS : top.slice(1), under: parent !== null && !isTop(parent) ? parent : null };
229
222
  };
230
223
  const columnOf = (key) => outline.groups.find((each) => topOf(each.id) === topIn(outline.children, key)) ?? null;
231
- if (sharedOutline(treeRoot) && holds(entry)) {
224
+ if (holds(entry)) {
225
+ if (pick && fresh) {
226
+ const column = columnOf(entry);
227
+ if (column) moveAfter(outline, column.id, after);
228
+ }
232
229
  if (!writtenAs(outline, stored)) { await writeOutline(treeRoot, outline); changed(); }
233
- const existing = pick || storedHolds;
230
+ const existing = !fresh && (pick || storedHolds);
234
231
  return { entry, existing, added: existing ? [] : [entry], shown: false, ...where() };
235
232
  }
236
233
  let shown = false;
@@ -356,23 +353,9 @@ export async function projectChoices(treeRoot, { ids = registeredIds(), stops =
356
353
  }
357
354
 
358
355
 
359
- /** The environment variable that keeps gallery new and gallery open from putting their draft in the default workspace's sidebar (the tests set it). */
360
- export const NO_DEFAULT_TREE = "GALLERY_NO_DEFAULT_TREE";
361
- const set = (value) => !["", "0", undefined].includes(value);
362
-
363
- /**
364
- * gallery new and gallery open in a workspace other than the default one (src/shared/workspace.mjs
365
- * defaultWorkspace): draft `slug` of workspace `root` put in the default workspace's sidebar (addDraft,
366
- * with the drafts under it), through the default workspace's server when one runs (POST
367
- * /__gallery/workspace/draft-join: in its queue, its workbench told at once), else here under the
368
- * outline's lock. Null where it is not done: NO_DEFAULT_TREE set, `root` the default workspace, no
369
- * default workspace there yet (nothing is made for it), or `root` not on the registry under its id (a
370
- * workspace in the system's temporary folder, or a clash of ids: an entry would name none). Else
371
- * addDraft's answer with `home` (the default workspace) and `names`: { column: the name of the column
372
- * it is in, under: the title of the draft it is under, or null }; or { failed: why } (nothing written).
373
- */
356
+ /** Put a project document in home's shared outline through the single service's draft-add
357
+ * command when running, otherwise through the same outline lock offline. */
374
358
  export async function joinDefaultTree(root, slug, { env = process.env } = {}) {
375
- if (set(env[NO_DEFAULT_TREE])) return null;
376
359
  const home = defaultWorkspace({ env });
377
360
  if (workspaceKey(home) === workspaceKey(root) || !isWorkspace(home)) return null;
378
361
  const ids = registeredIds();
@@ -380,8 +363,17 @@ export async function joinDefaultTree(root, slug, { env = process.env } = {}) {
380
363
  if (!found.root || workspaceKey(found.root) !== workspaceKey(root)) return null;
381
364
  let done = null;
382
365
  try {
383
- const server = await findPreviewServer({ reportsDirectory: home });
384
- if (server) done = await joinThrough(server, root, slug);
366
+ const server = await findPreviewServer();
367
+ if (server) {
368
+ const origin = `http://127.0.0.1:${server.port}`;
369
+ const response = await fetch(`${origin}/__gallery/workspace/draft-add`, {
370
+ method: "POST", headers: { "Content-Type": "application/json", Origin: origin, "X-Gallery-Token": server.token },
371
+ body: JSON.stringify({ path: path.join(root, slug) }), signal: AbortSignal.timeout(30000),
372
+ });
373
+ const answer = await response.json();
374
+ if (!answer.ok) throw new Error(answer.message ?? "Could not register the document");
375
+ done = answer;
376
+ }
385
377
  done ??= await addDraft(home, root, slug);
386
378
  } catch (error) {
387
379
  return { failed: error.message, home };
@@ -389,23 +381,6 @@ export async function joinDefaultTree(root, slug, { env = process.env } = {}) {
389
381
  return { ...done, home, names: await placeNames(home, done, ids) };
390
382
  }
391
383
 
392
- // POST draft-join to the default workspace's server; null when no server of it takes it (gone, or an older Gallery's).
393
- async function joinThrough(server, root, slug) {
394
- const host = `127.0.0.1:${server.port}`;
395
- try {
396
- const response = await fetch(`http://${host}/__gallery/workspace/draft-join`, {
397
- method: "POST",
398
- headers: { "Content-Type": "application/json", Origin: `http://${host}`, "X-Gallery-Token": server.token },
399
- body: JSON.stringify({ root, slug }),
400
- signal: AbortSignal.timeout(30000),
401
- });
402
- const answer = await response.json().catch(() => null);
403
- return answer?.ok === true && typeof answer.entry === "string" ? answer : null;
404
- } catch {
405
- return null;
406
- }
407
- }
408
-
409
384
  // The names a line says of where a draft went: its column's (the user's title, else its workspace's
410
385
  // name, else 默认文档, the default workspace's own column, or 链接) and the title of the draft or document it is under.
411
386
  async function placeNames(home, { column, under }, ids) {