@immediately-run/sdk 0.64.1 → 0.65.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 (45) hide show
  1. package/dist/boot.cjs +6 -1
  2. package/dist/boot.cjs.map +1 -1
  3. package/dist/boot.js +6 -1
  4. package/dist/boot.js.map +1 -1
  5. package/dist/components/ScrollRestoration.cjs +117 -0
  6. package/dist/components/ScrollRestoration.cjs.map +1 -0
  7. package/dist/components/ScrollRestoration.d.cts +13 -0
  8. package/dist/components/ScrollRestoration.d.ts +13 -0
  9. package/dist/components/ScrollRestoration.js +94 -0
  10. package/dist/components/ScrollRestoration.js.map +1 -0
  11. package/dist/entryState.cjs +99 -0
  12. package/dist/entryState.cjs.map +1 -0
  13. package/dist/entryState.d.cts +42 -0
  14. package/dist/entryState.d.ts +42 -0
  15. package/dist/entryState.js +69 -0
  16. package/dist/entryState.js.map +1 -0
  17. package/dist/index.cjs +10 -1
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +4 -0
  20. package/dist/index.d.ts +4 -0
  21. package/dist/index.js +5 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/routing.cjs +3 -0
  24. package/dist/routing.cjs.map +1 -1
  25. package/dist/routing.js +3 -0
  26. package/dist/routing.js.map +1 -1
  27. package/dist/scrollRestore.cjs +43 -0
  28. package/dist/scrollRestore.cjs.map +1 -0
  29. package/dist/scrollRestore.d.cts +39 -0
  30. package/dist/scrollRestore.d.ts +39 -0
  31. package/dist/scrollRestore.js +18 -0
  32. package/dist/scrollRestore.js.map +1 -0
  33. package/dist/useEntryState.cjs +41 -0
  34. package/dist/useEntryState.cjs.map +1 -0
  35. package/dist/useEntryState.d.cts +28 -0
  36. package/dist/useEntryState.d.ts +28 -0
  37. package/dist/useEntryState.js +17 -0
  38. package/dist/useEntryState.js.map +1 -0
  39. package/dist/version.cjs +1 -1
  40. package/dist/version.cjs.map +1 -1
  41. package/dist/version.d.cts +1 -1
  42. package/dist/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/dist/version.js.map +1 -1
  45. package/package.json +1 -1
@@ -0,0 +1,42 @@
1
+ /** How the browser reached the current entry. `push` is an ordinary forward
2
+ * navigation; `back`/`forward` are traversals, and only a traversal restores. */
3
+ type NavigationDirection = 'push' | 'back' | 'forward';
4
+ /** Serialized cap for the whole scratch. Past this the scratch is dropped with a
5
+ * dev-time warning rather than silently truncated: a half-written bookmark that
6
+ * restores to the wrong place is worse than no bookmark. */
7
+ declare const ENTRY_STATE_MAX_BYTES = 4096;
8
+ interface ArrivedNavigation {
9
+ /** The scratch the leaving app left on this entry, if any. */
10
+ readonly state: Readonly<Record<string, unknown>> | undefined;
11
+ /** How this entry was reached. */
12
+ readonly direction: NavigationDirection;
13
+ }
14
+ type Collector = () => unknown;
15
+ /**
16
+ * Queue a value for the entry being left. The last call before a navigation wins.
17
+ * Prefer {@link registerEntryStateCollector} for values that are only knowable at the
18
+ * instant of navigating (a scroll offset is the motivating case).
19
+ */
20
+ declare const saveEntryState: (key: string, value: unknown) => void;
21
+ /** Register a callback asked for its value at navigation time. Returns its remover.
22
+ * A second registration for the same key replaces the first — one owner per key. */
23
+ declare const registerEntryStateCollector: (key: string, collect: Collector) => (() => void);
24
+ /**
25
+ * Gather the scratch for the entry being left and clear the queue. Called by
26
+ * `navigate()` — not part of the app-facing surface.
27
+ *
28
+ * A collector that throws is skipped: a bookmark is a convenience, and it must never
29
+ * be able to break a navigation.
30
+ */
31
+ declare const takeQueuedEntryState: () => Record<string, unknown> | undefined;
32
+ /** Record what the host said about the entry just arrived at. Called by the boot
33
+ * shell's `urlchange` listener — not part of the app-facing surface. */
34
+ declare const receiveNavigation: (next: ArrivedNavigation) => void;
35
+ /** The current arrival, as one stable object so `useSyncExternalStore` can compare
36
+ * by identity. */
37
+ declare const getArrivedNavigation: () => ArrivedNavigation;
38
+ declare const subscribeNavigation: (listener: () => void) => (() => void);
39
+ /** Test seam: forget every collector, queued value and arrival. */
40
+ declare const resetEntryState: () => void;
41
+
42
+ export { type ArrivedNavigation, ENTRY_STATE_MAX_BYTES, type NavigationDirection, getArrivedNavigation, receiveNavigation, registerEntryStateCollector, resetEntryState, saveEntryState, subscribeNavigation, takeQueuedEntryState };
@@ -0,0 +1,69 @@
1
+ import "./chunk-VHAA22YE.js";
2
+ const ENTRY_STATE_MAX_BYTES = 4096;
3
+ const collectors = /* @__PURE__ */ new Map();
4
+ let queued = {};
5
+ let arrived = { state: void 0, direction: "push" };
6
+ const listeners = /* @__PURE__ */ new Set();
7
+ const notify = () => {
8
+ for (const l of [...listeners]) l();
9
+ };
10
+ const saveEntryState = (key, value) => {
11
+ queued[key] = value;
12
+ };
13
+ const registerEntryStateCollector = (key, collect) => {
14
+ collectors.set(key, collect);
15
+ return () => {
16
+ if (collectors.get(key) === collect) collectors.delete(key);
17
+ };
18
+ };
19
+ const takeQueuedEntryState = () => {
20
+ const out = { ...queued };
21
+ queued = {};
22
+ for (const [key, collect] of collectors) {
23
+ try {
24
+ const value = collect();
25
+ if (value !== void 0) out[key] = value;
26
+ } catch {
27
+ }
28
+ }
29
+ if (Object.keys(out).length === 0) return void 0;
30
+ let size = 0;
31
+ try {
32
+ size = JSON.stringify(out).length;
33
+ } catch {
34
+ return void 0;
35
+ }
36
+ if (size > ENTRY_STATE_MAX_BYTES) {
37
+ console.warn(
38
+ `[Sandbox] entry state is ${size} bytes, over the ${ENTRY_STATE_MAX_BYTES}-byte cap \u2014 dropped. Keep per-entry scratch small (a scroll offset, a few ids), not a cache.`
39
+ );
40
+ return void 0;
41
+ }
42
+ return out;
43
+ };
44
+ const receiveNavigation = (next) => {
45
+ arrived = next;
46
+ notify();
47
+ };
48
+ const getArrivedNavigation = () => arrived;
49
+ const subscribeNavigation = (listener) => {
50
+ listeners.add(listener);
51
+ return () => listeners.delete(listener);
52
+ };
53
+ const resetEntryState = () => {
54
+ collectors.clear();
55
+ queued = {};
56
+ arrived = { state: void 0, direction: "push" };
57
+ listeners.clear();
58
+ };
59
+ export {
60
+ ENTRY_STATE_MAX_BYTES,
61
+ getArrivedNavigation,
62
+ receiveNavigation,
63
+ registerEntryStateCollector,
64
+ resetEntryState,
65
+ saveEntryState,
66
+ subscribeNavigation,
67
+ takeQueuedEntryState
68
+ };
69
+ //# sourceMappingURL=entryState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/entryState.ts"],"sourcesContent":["// Per-history-entry scratch (R3-627): a small opaque value an app attaches to the\n// history entry it is LEAVING, and reads back when the browser returns to that entry.\n//\n// Why this exists. The app frame's realm is replaced by an in-app navigation, so\n// nothing in app memory survives it; the host owns the history stack but is at an\n// opaque origin and cannot look inside the frame. So the only way an app can leave\n// itself a bookmark is to hand the host a value to hold against the entry. The host\n// never parses it — it is the app's own value coming back to the app, which is why\n// this does not weaken `EDITOR_AS_APP_SPEC §2` (\"view state … never crosses the\n// boundary\"): the host is a courier, not a reader.\n//\n// Collected at navigation time, not continuously. The app is the one calling\n// `navigate()`, so the scratch is gathered synchronously inside that call — no scroll\n// reporting channel, no throttling, no traffic for a value needed at one instant.\n\n/** How the browser reached the current entry. `push` is an ordinary forward\n * navigation; `back`/`forward` are traversals, and only a traversal restores. */\nexport type NavigationDirection = 'push' | 'back' | 'forward';\n\n/** Serialized cap for the whole scratch. Past this the scratch is dropped with a\n * dev-time warning rather than silently truncated: a half-written bookmark that\n * restores to the wrong place is worse than no bookmark. */\nexport const ENTRY_STATE_MAX_BYTES = 4096;\n\nexport interface ArrivedNavigation {\n /** The scratch the leaving app left on this entry, if any. */\n readonly state: Readonly<Record<string, unknown>> | undefined;\n /** How this entry was reached. */\n readonly direction: NavigationDirection;\n}\n\ntype Collector = () => unknown;\n\nconst collectors = new Map<string, Collector>();\nlet queued: Record<string, unknown> = {};\nlet arrived: ArrivedNavigation = { state: undefined, direction: 'push' };\nconst listeners = new Set<() => void>();\n\nconst notify = (): void => {\n for (const l of [...listeners]) l();\n};\n\n/**\n * Queue a value for the entry being left. The last call before a navigation wins.\n * Prefer {@link registerEntryStateCollector} for values that are only knowable at the\n * instant of navigating (a scroll offset is the motivating case).\n */\nexport const saveEntryState = (key: string, value: unknown): void => {\n queued[key] = value;\n};\n\n/** Register a callback asked for its value at navigation time. Returns its remover.\n * A second registration for the same key replaces the first — one owner per key. */\nexport const registerEntryStateCollector = (key: string, collect: Collector): (() => void) => {\n collectors.set(key, collect);\n return () => {\n if (collectors.get(key) === collect) collectors.delete(key);\n };\n};\n\n/**\n * Gather the scratch for the entry being left and clear the queue. Called by\n * `navigate()` — not part of the app-facing surface.\n *\n * A collector that throws is skipped: a bookmark is a convenience, and it must never\n * be able to break a navigation.\n */\nexport const takeQueuedEntryState = (): Record<string, unknown> | undefined => {\n const out: Record<string, unknown> = { ...queued };\n queued = {};\n for (const [key, collect] of collectors) {\n try {\n const value = collect();\n if (value !== undefined) out[key] = value;\n } catch {\n /* a collector must not break navigation */\n }\n }\n if (Object.keys(out).length === 0) return undefined;\n let size = 0;\n try {\n size = JSON.stringify(out).length;\n } catch {\n return undefined; // not serializable → nothing to hand the host\n }\n if (size > ENTRY_STATE_MAX_BYTES) {\n console.warn(\n `[Sandbox] entry state is ${size} bytes, over the ${ENTRY_STATE_MAX_BYTES}-byte cap — dropped. ` +\n `Keep per-entry scratch small (a scroll offset, a few ids), not a cache.`,\n );\n return undefined;\n }\n return out;\n};\n\n/** Record what the host said about the entry just arrived at. Called by the boot\n * shell's `urlchange` listener — not part of the app-facing surface. */\nexport const receiveNavigation = (next: ArrivedNavigation): void => {\n arrived = next;\n notify();\n};\n\n/** The current arrival, as one stable object so `useSyncExternalStore` can compare\n * by identity. */\nexport const getArrivedNavigation = (): ArrivedNavigation => arrived;\n\nexport const subscribeNavigation = (listener: () => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n};\n\n/** Test seam: forget every collector, queued value and arrival. */\nexport const resetEntryState = (): void => {\n collectors.clear();\n queued = {};\n arrived = { state: undefined, direction: 'push' };\n listeners.clear();\n};\n"],"mappings":";AAsBO,MAAM,wBAAwB;AAWrC,MAAM,aAAa,oBAAI,IAAuB;AAC9C,IAAI,SAAkC,CAAC;AACvC,IAAI,UAA6B,EAAE,OAAO,QAAW,WAAW,OAAO;AACvE,MAAM,YAAY,oBAAI,IAAgB;AAEtC,MAAM,SAAS,MAAY;AACzB,aAAW,KAAK,CAAC,GAAG,SAAS,EAAG,GAAE;AACpC;AAOO,MAAM,iBAAiB,CAAC,KAAa,UAAyB;AACnE,SAAO,GAAG,IAAI;AAChB;AAIO,MAAM,8BAA8B,CAAC,KAAa,YAAqC;AAC5F,aAAW,IAAI,KAAK,OAAO;AAC3B,SAAO,MAAM;AACX,QAAI,WAAW,IAAI,GAAG,MAAM,QAAS,YAAW,OAAO,GAAG;AAAA,EAC5D;AACF;AASO,MAAM,uBAAuB,MAA2C;AAC7E,QAAM,MAA+B,EAAE,GAAG,OAAO;AACjD,WAAS,CAAC;AACV,aAAW,CAAC,KAAK,OAAO,KAAK,YAAY;AACvC,QAAI;AACF,YAAM,QAAQ,QAAQ;AACtB,UAAI,UAAU,OAAW,KAAI,GAAG,IAAI;AAAA,IACtC,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI,OAAO,KAAK,GAAG,EAAE,WAAW,EAAG,QAAO;AAC1C,MAAI,OAAO;AACX,MAAI;AACF,WAAO,KAAK,UAAU,GAAG,EAAE;AAAA,EAC7B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,uBAAuB;AAChC,YAAQ;AAAA,MACN,4BAA4B,IAAI,oBAAoB,qBAAqB;AAAA,IAE3E;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAIO,MAAM,oBAAoB,CAAC,SAAkC;AAClE,YAAU;AACV,SAAO;AACT;AAIO,MAAM,uBAAuB,MAAyB;AAEtD,MAAM,sBAAsB,CAAC,aAAuC;AACzE,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM,UAAU,OAAO,QAAQ;AACxC;AAGO,MAAM,kBAAkB,MAAY;AACzC,aAAW,MAAM;AACjB,WAAS,CAAC;AACV,YAAU,EAAE,OAAO,QAAW,WAAW,OAAO;AAChD,YAAU,MAAM;AAClB;","names":[]}
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  var index_exports = {};
21
21
  __export(index_exports, {
22
22
  SafeInclude: () => import_SafeInclude.SafeInclude,
23
+ ScrollRestoration: () => import_ScrollRestoration.ScrollRestoration,
23
24
  getInjectedMetadataEmitter: () => import_injectedBundler.getInjectedMetadataEmitter,
24
25
  getInjectedMetadataSnapshot: () => import_injectedBundler.getInjectedMetadataSnapshot
25
26
  });
@@ -84,9 +85,14 @@ __reExport(index_exports, require("./collectHeadings"), module.exports);
84
85
  __reExport(index_exports, require("./agentContext"), module.exports);
85
86
  __reExport(index_exports, require("./fence"), module.exports);
86
87
  __reExport(index_exports, require("./platformLink"), module.exports);
88
+ __reExport(index_exports, require("./entryState"), module.exports);
89
+ __reExport(index_exports, require("./useEntryState"), module.exports);
90
+ __reExport(index_exports, require("./scrollRestore"), module.exports);
91
+ var import_ScrollRestoration = require("./components/ScrollRestoration");
87
92
  // Annotate the CommonJS export names for ESM import in node:
88
93
  0 && (module.exports = {
89
94
  SafeInclude,
95
+ ScrollRestoration,
90
96
  getInjectedMetadataEmitter,
91
97
  getInjectedMetadataSnapshot,
92
98
  ...require("./MDXProvider"),
@@ -146,6 +152,9 @@ __reExport(index_exports, require("./platformLink"), module.exports);
146
152
  ...require("./collectHeadings"),
147
153
  ...require("./agentContext"),
148
154
  ...require("./fence"),
149
- ...require("./platformLink")
155
+ ...require("./platformLink"),
156
+ ...require("./entryState"),
157
+ ...require("./useEntryState"),
158
+ ...require("./scrollRestore")
150
159
  });
151
160
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './bundle';\n// Deprecated `Corpus*` spellings of the above (R3-482); see `src/corpus.ts`.\nexport * from './corpus';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './chromeState';\nexport * from './workspace';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './analytics';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './feed';\nexport * from './secrets';\nexport * from './recents'; // R3-485: the gated recent-projects read (page.home)\nexport * from './openRepository'; // R3-476: host-mediated open-in-a-new-tab (route:read)\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n// R3-489 (GROVE_AGENT_SPEC): the embedded-agent seam every app shares — the tool-use\n// loop ported from agent-demo (`runAgent` over the host chat slot), the MDX metadata\n// query tool, the headings index collector, the deixis context block, and the fence\n// for corpus-derived bytes entering a loop.\nexport * from './agentLoop';\nexport * from './agentSteering';\nexport * from './agentPause';\nexport * from './agentChatClient';\nexport * from './metadataQueryTool';\nexport * from './collectHeadings';\nexport * from './agentContext';\nexport * from './fence';\nexport * from './platformLink';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAQA,yBAA4B;AAC5B,0BAAc,0BATd;AAUA,0BAAc,uCAVd;AAWA,0BAAc,wBAXd;AAYA,0BAAc,qBAZd;AAcA,0BAAc,qBAdd;AAeA,0BAAc,oCAfd;AAgBA,0BAAc,gCAhBd;AAiBA,0BAAc,oBAjBd;AAoBA,0BAAc,6BApBd;AAuBA,6BAAwE;AACxE,0BAAc,mBAxBd;AAyBA,0BAAc,oBAzBd;AA0BA,0BAAc,4BA1Bd;AA2BA,0BAAc,qBA3Bd;AA4BA,0BAAc,yBA5Bd;AA6BA,0BAAc,0BA7Bd;AA8BA,0BAAc,wBA9Bd;AA+BA,0BAAc,4BA/Bd;AAgCA,0BAAc,qBAhCd;AAiCA,0BAAc,qBAjCd;AAkCA,0BAAc,wBAlCd;AAmCA,0BAAc,yBAnCd;AAoCA,0BAAc,sBApCd;AAqCA,0BAAc,kBArCd;AAsCA,0BAAc,kBAtCd;AAuCA,0BAAc,uBAvCd;AAwCA,0BAAc,mBAxCd;AAyCA,0BAAc,sBAzCd;AA0CA,0BAAc,sBA1Cd;AA2CA,0BAAc,6BA3Cd;AA4CA,0BAAc,kBA5Cd;AA6CA,0BAAc,0BA7Cd;AA8CA,0BAAc,kBA9Cd;AA+CA,0BAAc,yBA/Cd;AAgDA,0BAAc,iBAhDd;AAiDA,0BAAc,oBAjDd;AAkDA,0BAAc,oBAlDd;AAmDA,0BAAc,qBAnDd;AAoDA,0BAAc,sBApDd;AAqDA,0BAAc,wBArDd;AAsDA,0BAAc,oBAtDd;AAuDA,0BAAc,sBAvDd;AAwDA,0BAAc,6BAxDd;AAyDA,0BAAc,+BAzDd;AA0DA,0BAAc,2BA1Dd;AA2DA,0BAAc,0BA3Dd;AAgEA,0BAAc,wBAhEd;AAiEA,0BAAc,4BAjEd;AAkEA,0BAAc,yBAlEd;AAmEA,0BAAc,8BAnEd;AAoEA,0BAAc,gCApEd;AAqEA,0BAAc,8BArEd;AAsEA,0BAAc,2BAtEd;AAuEA,0BAAc,oBAvEd;AAwEA,0BAAc,2BAxEd;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './bundle';\n// Deprecated `Corpus*` spellings of the above (R3-482); see `src/corpus.ts`.\nexport * from './corpus';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './chromeState';\nexport * from './workspace';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './analytics';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './feed';\nexport * from './secrets';\nexport * from './recents'; // R3-485: the gated recent-projects read (page.home)\nexport * from './openRepository'; // R3-476: host-mediated open-in-a-new-tab (route:read)\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n// R3-489 (GROVE_AGENT_SPEC): the embedded-agent seam every app shares — the tool-use\n// loop ported from agent-demo (`runAgent` over the host chat slot), the MDX metadata\n// query tool, the headings index collector, the deixis context block, and the fence\n// for corpus-derived bytes entering a loop.\nexport * from './agentLoop';\nexport * from './agentSteering';\nexport * from './agentPause';\nexport * from './agentChatClient';\nexport * from './metadataQueryTool';\nexport * from './collectHeadings';\nexport * from './agentContext';\nexport * from './fence';\nexport * from './platformLink';\n// R3-627: the per-history-entry scratch, and the scroll restoration built on it —\n// Back lands where the reader left, for however many entries deep they go.\nexport * from './entryState';\nexport * from './useEntryState';\nexport * from './scrollRestore';\nexport { ScrollRestoration, type ScrollRestorationProps } from './components/ScrollRestoration';\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,0BAAc,0BAAd;AACA,0BAAc,sBADd;AAEA,0BAAc,mBAFd;AAGA,0BAAc,iCAHd;AAQA,yBAA4B;AAC5B,0BAAc,0BATd;AAUA,0BAAc,uCAVd;AAWA,0BAAc,wBAXd;AAYA,0BAAc,qBAZd;AAcA,0BAAc,qBAdd;AAeA,0BAAc,oCAfd;AAgBA,0BAAc,gCAhBd;AAiBA,0BAAc,oBAjBd;AAoBA,0BAAc,6BApBd;AAuBA,6BAAwE;AACxE,0BAAc,mBAxBd;AAyBA,0BAAc,oBAzBd;AA0BA,0BAAc,4BA1Bd;AA2BA,0BAAc,qBA3Bd;AA4BA,0BAAc,yBA5Bd;AA6BA,0BAAc,0BA7Bd;AA8BA,0BAAc,wBA9Bd;AA+BA,0BAAc,4BA/Bd;AAgCA,0BAAc,qBAhCd;AAiCA,0BAAc,qBAjCd;AAkCA,0BAAc,wBAlCd;AAmCA,0BAAc,yBAnCd;AAoCA,0BAAc,sBApCd;AAqCA,0BAAc,kBArCd;AAsCA,0BAAc,kBAtCd;AAuCA,0BAAc,uBAvCd;AAwCA,0BAAc,mBAxCd;AAyCA,0BAAc,sBAzCd;AA0CA,0BAAc,sBA1Cd;AA2CA,0BAAc,6BA3Cd;AA4CA,0BAAc,kBA5Cd;AA6CA,0BAAc,0BA7Cd;AA8CA,0BAAc,kBA9Cd;AA+CA,0BAAc,yBA/Cd;AAgDA,0BAAc,iBAhDd;AAiDA,0BAAc,oBAjDd;AAkDA,0BAAc,oBAlDd;AAmDA,0BAAc,qBAnDd;AAoDA,0BAAc,sBApDd;AAqDA,0BAAc,wBArDd;AAsDA,0BAAc,oBAtDd;AAuDA,0BAAc,sBAvDd;AAwDA,0BAAc,6BAxDd;AAyDA,0BAAc,+BAzDd;AA0DA,0BAAc,2BA1Dd;AA2DA,0BAAc,0BA3Dd;AAgEA,0BAAc,wBAhEd;AAiEA,0BAAc,4BAjEd;AAkEA,0BAAc,yBAlEd;AAmEA,0BAAc,8BAnEd;AAoEA,0BAAc,gCApEd;AAqEA,0BAAc,8BArEd;AAsEA,0BAAc,2BAtEd;AAuEA,0BAAc,oBAvEd;AAwEA,0BAAc,2BAxEd;AA2EA,0BAAc,yBA3Ed;AA4EA,0BAAc,4BA5Ed;AA6EA,0BAAc,4BA7Ed;AA8EA,+BAA+D;","names":[]}
package/dist/index.d.cts CHANGED
@@ -57,6 +57,10 @@ export { collectHeadings } from './collectHeadings.cjs';
57
57
  export { AgentContextAppFields, AgentContextBlock, renderAgentContext, useAgentContext } from './agentContext.cjs';
58
58
  export { fenceUntrusted } from './fence.cjs';
59
59
  export { PlatformLink, PlatformLinkProps, usePlatformHref } from './platformLink.cjs';
60
+ export { ArrivedNavigation, ENTRY_STATE_MAX_BYTES, NavigationDirection, getArrivedNavigation, receiveNavigation, registerEntryStateCollector, resetEntryState, saveEntryState, subscribeNavigation, takeQueuedEntryState } from './entryState.cjs';
61
+ export { useEntryState, useNavigationDirection } from './useEntryState.cjs';
62
+ export { RESTORE_DEADLINE_MS, RESTORE_EPSILON_PX, RestoreAction, RestoreSample, nextRestoreAction } from './scrollRestore.cjs';
63
+ export { ScrollRestoration, ScrollRestorationProps } from './components/ScrollRestoration.cjs';
60
64
  export { Admonition, AdmonitionType } from './components/Admonition.cjs';
61
65
  export { FS_PREFIX, LinkSpace, ResolvedLinkTarget, normalizeAbsolute, resolveLinkTarget } from '@immediately-run/mdx-plugins';
62
66
  export { GrantRecord, Member, ResolvedUser, Role, SpaceInfo, getSpaceMembers, inviteToSpace, listAllSpaces, listGrants, listSpaces, lookupUser, revokeGrant, setSpaceRole, unshareSpace } from './generated/spaces.cjs';
package/dist/index.d.ts CHANGED
@@ -57,6 +57,10 @@ export { collectHeadings } from './collectHeadings.js';
57
57
  export { AgentContextAppFields, AgentContextBlock, renderAgentContext, useAgentContext } from './agentContext.js';
58
58
  export { fenceUntrusted } from './fence.js';
59
59
  export { PlatformLink, PlatformLinkProps, usePlatformHref } from './platformLink.js';
60
+ export { ArrivedNavigation, ENTRY_STATE_MAX_BYTES, NavigationDirection, getArrivedNavigation, receiveNavigation, registerEntryStateCollector, resetEntryState, saveEntryState, subscribeNavigation, takeQueuedEntryState } from './entryState.js';
61
+ export { useEntryState, useNavigationDirection } from './useEntryState.js';
62
+ export { RESTORE_DEADLINE_MS, RESTORE_EPSILON_PX, RestoreAction, RestoreSample, nextRestoreAction } from './scrollRestore.js';
63
+ export { ScrollRestoration, ScrollRestorationProps } from './components/ScrollRestoration.js';
60
64
  export { Admonition, AdmonitionType } from './components/Admonition.js';
61
65
  export { FS_PREFIX, LinkSpace, ResolvedLinkTarget, normalizeAbsolute, resolveLinkTarget } from '@immediately-run/mdx-plugins';
62
66
  export { GrantRecord, Member, ResolvedUser, Role, SpaceInfo, getSpaceMembers, inviteToSpace, listAllSpaces, listGrants, listSpaces, lookupUser, revokeGrant, setSpaceRole, unshareSpace } from './generated/spaces.js';
package/dist/index.js CHANGED
@@ -59,8 +59,13 @@ export * from "./collectHeadings";
59
59
  export * from "./agentContext";
60
60
  export * from "./fence";
61
61
  export * from "./platformLink";
62
+ export * from "./entryState";
63
+ export * from "./useEntryState";
64
+ export * from "./scrollRestore";
65
+ import { ScrollRestoration } from "./components/ScrollRestoration";
62
66
  export {
63
67
  SafeInclude,
68
+ ScrollRestoration,
64
69
  getInjectedMetadataEmitter,
65
70
  getInjectedMetadataSnapshot
66
71
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './bundle';\n// Deprecated `Corpus*` spellings of the above (R3-482); see `src/corpus.ts`.\nexport * from './corpus';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './chromeState';\nexport * from './workspace';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './analytics';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './feed';\nexport * from './secrets';\nexport * from './recents'; // R3-485: the gated recent-projects read (page.home)\nexport * from './openRepository'; // R3-476: host-mediated open-in-a-new-tab (route:read)\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n// R3-489 (GROVE_AGENT_SPEC): the embedded-agent seam every app shares — the tool-use\n// loop ported from agent-demo (`runAgent` over the host chat slot), the MDX metadata\n// query tool, the headings index collector, the deixis context block, and the fence\n// for corpus-derived bytes entering a loop.\nexport * from './agentLoop';\nexport * from './agentSteering';\nexport * from './agentPause';\nexport * from './agentChatClient';\nexport * from './metadataQueryTool';\nexport * from './collectHeadings';\nexport * from './agentContext';\nexport * from './fence';\nexport * from './platformLink';\n"],"mappings":";AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,mBAAmB;AAC5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAGd,SAAS,4BAA4B,mCAAmC;AACxE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './MDXProvider';\nexport * from './routing';\nexport * from './boot';\nexport * from './components/Include';\n// Only the component is public. `stripFrontmatter`/`appMountRelative` are module-level\n// exports so they can be unit-tested directly, NOT public API — the SDK's surface is\n// backwards-compatible forever, so an internal helper exported for a test's convenience is a\n// permanent commitment made for the wrong reason.\nexport { SafeInclude } from './components/SafeInclude';\nexport * from './sourceCache';\nexport * from './components/MDXComponents';\nexport * from './linkSpace';\nexport * from './bundle';\n// Deprecated `Corpus*` spellings of the above (R3-482); see `src/corpus.ts`.\nexport * from './corpus';\nexport * from './components/MountImage';\nexport * from './components/Routes';\nexport * from './hooks';\n// R3-276: the supported way for a viewer app to provide its own metadata store,\n// replacing a wholesale re-provision of `TinkerableContext` in app code.\nexport * from './metadataSource';\n// The deprecated injected-bundler adapters, re-exported so their deprecation notices\n// are visible in the published docs (R3-278; the window only narrows).\nexport { getInjectedMetadataEmitter, getInjectedMetadataSnapshot } from './injectedBundler';\nexport * from './auth';\nexport * from './theme';\nexport * from './editorContext';\nexport * from './editor';\nexport * from './formFactor';\nexport * from './chromeState';\nexport * from './workspace';\nexport * from './hostAttention';\nexport * from './region';\nexport * from './mounts';\nexport * from './analytics';\nexport * from './contribute';\nexport * from './catalog';\nexport * from './ipc';\nexport * from './dnd';\nexport * from './netFetch';\nexport * from './feed';\nexport * from './secrets';\nexport * from './recents'; // R3-485: the gated recent-projects read (page.home)\nexport * from './openRepository'; // R3-476: host-mediated open-in-a-new-tab (route:read)\nexport * from './llm';\nexport * from './diagnostics';\nexport * from './vcs';\nexport * from './onFsChange';\nexport * from './fs';\nexport * from './debug';\nexport * from './tasks';\nexport * from './launch';\nexport * from './runtime';\nexport * from './irMarkers';\nexport * from './ready';\nexport * from './loading';\nexport * from './protocolStream';\nexport * from './protocolDeadline';\nexport * from './sandboxTypes';\nexport * from './safeContent';\n// R3-489 (GROVE_AGENT_SPEC): the embedded-agent seam every app shares — the tool-use\n// loop ported from agent-demo (`runAgent` over the host chat slot), the MDX metadata\n// query tool, the headings index collector, the deixis context block, and the fence\n// for corpus-derived bytes entering a loop.\nexport * from './agentLoop';\nexport * from './agentSteering';\nexport * from './agentPause';\nexport * from './agentChatClient';\nexport * from './metadataQueryTool';\nexport * from './collectHeadings';\nexport * from './agentContext';\nexport * from './fence';\nexport * from './platformLink';\n// R3-627: the per-history-entry scratch, and the scroll restoration built on it —\n// Back lands where the reader left, for however many entries deep they go.\nexport * from './entryState';\nexport * from './useEntryState';\nexport * from './scrollRestore';\nexport { ScrollRestoration, type ScrollRestorationProps } from './components/ScrollRestoration';\n"],"mappings":";AAAA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,SAAS,mBAAmB;AAC5B,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AAGd,SAAS,4BAA4B,mCAAmC;AACxE,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAKd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAGd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,yBAAsD;","names":[]}
package/dist/routing.cjs CHANGED
@@ -35,6 +35,7 @@ var import_TinkerableContext = require("./TinkerableContext");
35
35
  var import_routeMatch = require("./routeMatch");
36
36
  var import_urlUtils = require("./urlUtils");
37
37
  var import_pathUtils = require("./pathUtils");
38
+ var import_entryState = require("./entryState");
38
39
  var import_protocol = require("./generated/protocol");
39
40
  const useTinkerableLink = (newSandboxLocation) => {
40
41
  const { outerHref, navigationState: navigation } = (0, import_react.use)(import_TinkerableContext.TinkerableContext);
@@ -104,10 +105,12 @@ const navigate = (target, opts) => {
104
105
  } catch {
105
106
  }
106
107
  }
108
+ const entryState = (0, import_entryState.takeQueuedEntryState)();
107
109
  (0, import_sandboxUtils.sendMessage)(import_protocol.URLCHANGE, {
108
110
  url: target,
109
111
  back: false,
110
112
  forward: false,
113
+ ...entryState ? { entryState } : {},
111
114
  ...declared
112
115
  });
113
116
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\nimport { URLCHANGE } from './generated/protocol';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule;\n pathParameters?: Record<string, string>;\n};\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation);\n } else {\n newNavigationState.sandboxPath = newSandboxLocation;\n }\n return constructUrl(outerHref, newNavigationState);\n};\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (\n routingSpec: RoutingSpec,\n navigationState: NavigationState,\n): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n};\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {\n navigationState: { routingRule, pathParameters },\n } = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n *\n * `opts.viewedDocument` (R3-268) optionally declares which WORKING-TREE file\n * this destination renders — a tri-state rider on the navigation event:\n * - omit the option entirely → the host derives the hint from the URL's\n * `files/` suffix convention (the zero-SDK default);\n * - `null` → this view shows no file (clears the highlight — tag pages,\n * search, home views);\n * - a repo-relative path (the CORPUS path under dispatch — only the viewer\n * can map its own key space) → the file explorer highlights it.\n * The hint is highlight-only by contract (it never scrolls, never moves focus\n * or panes, never switches the editor) and is validated host-side for\n * existence — a wrong path degrades to \"no highlight\", never an error. The\n * host remembers declarations per URL, so back/forward reproduces them\n * without re-announcement.\n */\n// R3-268: an app-registered rule mapping a navigation TARGET to its viewed\n// document, consulted by `navigate()` whenever the caller did not declare one\n// explicitly. Registered ONCE (e.g. at boot) so an app whose links all flow\n// through `<Link>`/`navigate` gets correct declarations everywhere without\n// threading an option through every call site. Return `undefined` for \"no\n// declaration\" (the host falls back to the URL convention), `null` for \"this\n// view shows no file\", or a working-tree repo-relative path.\nlet viewedDocumentResolver: ((targetHref: string) => string | null | undefined) | null = null;\n\n/** Register the app's route→viewed-document rule (R3-268); pass `null` to clear. */\nexport const setViewedDocumentResolver = (\n resolver: ((targetHref: string) => string | null | undefined) | null,\n): void => {\n viewedDocumentResolver = resolver;\n};\n\nexport const navigate = (target: string, opts?: { viewedDocument?: string | null }) => {\n console.log(`[Sandbox] Navigating to ${target}`);\n // Explicit option first; else the registered resolver; else nothing on the\n // wire (the host derives from the URL convention). A resolver throw is\n // swallowed to \"no declaration\" — a mapping bug must never break navigation.\n let declared: { viewedDocument: string | null } | Record<string, never> = {};\n if (opts && 'viewedDocument' in opts) {\n declared = { viewedDocument: opts.viewedDocument ?? null };\n } else if (viewedDocumentResolver) {\n try {\n const v = viewedDocumentResolver(target);\n if (v !== undefined) declared = { viewedDocument: v };\n } catch {\n /* no declaration */\n }\n }\n sendMessage(URLCHANGE, {\n url: target,\n back: false,\n forward: false,\n ...declared,\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkDW;AAjDX,mBAAgC;AAEhC,0BAA4B;AAC5B,+BAAmD;AAEnD,wBAA2B;AAC3B,sBAA0D;AAC1D,uBAA0B;AAC1B,sBAA0B;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,QAAI,kBAAI,0CAAiB;AACxE,MAAI,yBAAqB,6BAAY,oBAAoB,UAAU;AACnE,MAAI,KAAC,gCAAe,kBAAkB,GAAG;AACvC,uBAAmB,kBAAc,4BAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,aAAO,8BAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAC9B,aACA,oBACmC;AACnC,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,qBAAiB,8BAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,4CAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,cAAU,yBAAW,0CAAiB;AAC5C,QAAM;AAAA,IACJ,iBAAiB,EAAE,aAAa,eAAe;AAAA,EACjD,IAAI;AACJ,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,UAC3B,kBAAI,0CAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,QAAI,kBAAI,0CAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA2BA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CAAC,QAAgB,SAA8C;AACrF,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAI/C,MAAI,WAAsE,CAAC;AAC3E,MAAI,QAAQ,oBAAoB,MAAM;AACpC,eAAW,EAAE,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,EAC3D,WAAW,wBAAwB;AACjC,QAAI;AACF,YAAM,IAAI,uBAAuB,MAAM;AACvC,UAAI,MAAM,OAAW,YAAW,EAAE,gBAAgB,EAAE;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,uCAAY,2BAAW;AAAA,IACrB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\nimport { takeQueuedEntryState } from './entryState';\nimport { URLCHANGE } from './generated/protocol';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule;\n pathParameters?: Record<string, string>;\n};\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation);\n } else {\n newNavigationState.sandboxPath = newSandboxLocation;\n }\n return constructUrl(outerHref, newNavigationState);\n};\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (\n routingSpec: RoutingSpec,\n navigationState: NavigationState,\n): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n};\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {\n navigationState: { routingRule, pathParameters },\n } = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n *\n * `opts.viewedDocument` (R3-268) optionally declares which WORKING-TREE file\n * this destination renders — a tri-state rider on the navigation event:\n * - omit the option entirely → the host derives the hint from the URL's\n * `files/` suffix convention (the zero-SDK default);\n * - `null` → this view shows no file (clears the highlight — tag pages,\n * search, home views);\n * - a repo-relative path (the CORPUS path under dispatch — only the viewer\n * can map its own key space) → the file explorer highlights it.\n * The hint is highlight-only by contract (it never scrolls, never moves focus\n * or panes, never switches the editor) and is validated host-side for\n * existence — a wrong path degrades to \"no highlight\", never an error. The\n * host remembers declarations per URL, so back/forward reproduces them\n * without re-announcement.\n */\n// R3-268: an app-registered rule mapping a navigation TARGET to its viewed\n// document, consulted by `navigate()` whenever the caller did not declare one\n// explicitly. Registered ONCE (e.g. at boot) so an app whose links all flow\n// through `<Link>`/`navigate` gets correct declarations everywhere without\n// threading an option through every call site. Return `undefined` for \"no\n// declaration\" (the host falls back to the URL convention), `null` for \"this\n// view shows no file\", or a working-tree repo-relative path.\nlet viewedDocumentResolver: ((targetHref: string) => string | null | undefined) | null = null;\n\n/** Register the app's route→viewed-document rule (R3-268); pass `null` to clear. */\nexport const setViewedDocumentResolver = (\n resolver: ((targetHref: string) => string | null | undefined) | null,\n): void => {\n viewedDocumentResolver = resolver;\n};\n\nexport const navigate = (target: string, opts?: { viewedDocument?: string | null }) => {\n console.log(`[Sandbox] Navigating to ${target}`);\n // Explicit option first; else the registered resolver; else nothing on the\n // wire (the host derives from the URL convention). A resolver throw is\n // swallowed to \"no declaration\" — a mapping bug must never break navigation.\n let declared: { viewedDocument: string | null } | Record<string, never> = {};\n if (opts && 'viewedDocument' in opts) {\n declared = { viewedDocument: opts.viewedDocument ?? null };\n } else if (viewedDocumentResolver) {\n try {\n const v = viewedDocumentResolver(target);\n if (v !== undefined) declared = { viewedDocument: v };\n } catch {\n /* no declaration */\n }\n }\n // The scratch for the entry we are LEAVING, gathered synchronously here because\n // this call is the one moment the app knows a navigation is happening (R3-627).\n // The host stamps it on the current entry before pushing the target, and hands it\n // back if the reader ever returns; it never parses it.\n const entryState = takeQueuedEntryState();\n sendMessage(URLCHANGE, {\n url: target,\n back: false,\n forward: false,\n ...(entryState ? { entryState } : {}),\n ...declared,\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmDW;AAlDX,mBAAgC;AAEhC,0BAA4B;AAC5B,+BAAmD;AAEnD,wBAA2B;AAC3B,sBAA0D;AAC1D,uBAA0B;AAC1B,wBAAqC;AACrC,sBAA0B;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,QAAI,kBAAI,0CAAiB;AACxE,MAAI,yBAAqB,6BAAY,oBAAoB,UAAU;AACnE,MAAI,KAAC,gCAAe,kBAAkB,GAAG;AACvC,uBAAmB,kBAAc,4BAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,aAAO,8BAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAC9B,aACA,oBACmC;AACnC,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,qBAAiB,8BAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,4CAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,cAAU,yBAAW,0CAAiB;AAC5C,QAAM;AAAA,IACJ,iBAAiB,EAAE,aAAa,eAAe;AAAA,EACjD,IAAI;AACJ,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,UAC3B,kBAAI,0CAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,QAAI,kBAAI,0CAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA2BA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CAAC,QAAgB,SAA8C;AACrF,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAI/C,MAAI,WAAsE,CAAC;AAC3E,MAAI,QAAQ,oBAAoB,MAAM;AACpC,eAAW,EAAE,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,EAC3D,WAAW,wBAAwB;AACjC,QAAI;AACF,YAAM,IAAI,uBAAuB,MAAM;AACvC,UAAI,MAAM,OAAW,YAAW,EAAE,gBAAgB,EAAE;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF;AAKA,QAAM,iBAAa,wCAAqB;AACxC,uCAAY,2BAAW;AAAA,IACrB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,IACnC,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
package/dist/routing.js CHANGED
@@ -6,6 +6,7 @@ import { TinkerableContext } from "./TinkerableContext";
6
6
  import { matchRoute } from "./routeMatch";
7
7
  import { constructUrl, isAbsolutePath, parseTarget } from "./urlUtils";
8
8
  import { joinPaths } from "./pathUtils";
9
+ import { takeQueuedEntryState } from "./entryState";
9
10
  import { URLCHANGE } from "./generated/protocol";
10
11
  const useTinkerableLink = (newSandboxLocation) => {
11
12
  const { outerHref, navigationState: navigation } = use(TinkerableContext);
@@ -75,10 +76,12 @@ const navigate = (target, opts) => {
75
76
  } catch {
76
77
  }
77
78
  }
79
+ const entryState = takeQueuedEntryState();
78
80
  sendMessage(URLCHANGE, {
79
81
  url: target,
80
82
  back: false,
81
83
  forward: false,
84
+ ...entryState ? { entryState } : {},
82
85
  ...declared
83
86
  });
84
87
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\nimport { URLCHANGE } from './generated/protocol';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule;\n pathParameters?: Record<string, string>;\n};\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation);\n } else {\n newNavigationState.sandboxPath = newSandboxLocation;\n }\n return constructUrl(outerHref, newNavigationState);\n};\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (\n routingSpec: RoutingSpec,\n navigationState: NavigationState,\n): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n};\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {\n navigationState: { routingRule, pathParameters },\n } = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n *\n * `opts.viewedDocument` (R3-268) optionally declares which WORKING-TREE file\n * this destination renders — a tri-state rider on the navigation event:\n * - omit the option entirely → the host derives the hint from the URL's\n * `files/` suffix convention (the zero-SDK default);\n * - `null` → this view shows no file (clears the highlight — tag pages,\n * search, home views);\n * - a repo-relative path (the CORPUS path under dispatch — only the viewer\n * can map its own key space) → the file explorer highlights it.\n * The hint is highlight-only by contract (it never scrolls, never moves focus\n * or panes, never switches the editor) and is validated host-side for\n * existence — a wrong path degrades to \"no highlight\", never an error. The\n * host remembers declarations per URL, so back/forward reproduces them\n * without re-announcement.\n */\n// R3-268: an app-registered rule mapping a navigation TARGET to its viewed\n// document, consulted by `navigate()` whenever the caller did not declare one\n// explicitly. Registered ONCE (e.g. at boot) so an app whose links all flow\n// through `<Link>`/`navigate` gets correct declarations everywhere without\n// threading an option through every call site. Return `undefined` for \"no\n// declaration\" (the host falls back to the URL convention), `null` for \"this\n// view shows no file\", or a working-tree repo-relative path.\nlet viewedDocumentResolver: ((targetHref: string) => string | null | undefined) | null = null;\n\n/** Register the app's route→viewed-document rule (R3-268); pass `null` to clear. */\nexport const setViewedDocumentResolver = (\n resolver: ((targetHref: string) => string | null | undefined) | null,\n): void => {\n viewedDocumentResolver = resolver;\n};\n\nexport const navigate = (target: string, opts?: { viewedDocument?: string | null }) => {\n console.log(`[Sandbox] Navigating to ${target}`);\n // Explicit option first; else the registered resolver; else nothing on the\n // wire (the host derives from the URL convention). A resolver throw is\n // swallowed to \"no declaration\" — a mapping bug must never break navigation.\n let declared: { viewedDocument: string | null } | Record<string, never> = {};\n if (opts && 'viewedDocument' in opts) {\n declared = { viewedDocument: opts.viewedDocument ?? null };\n } else if (viewedDocumentResolver) {\n try {\n const v = viewedDocumentResolver(target);\n if (v !== undefined) declared = { viewedDocument: v };\n } catch {\n /* no declaration */\n }\n }\n sendMessage(URLCHANGE, {\n url: target,\n back: false,\n forward: false,\n ...declared,\n });\n};\n"],"mappings":";AAkDW;AAjDX,SAAS,KAAK,kBAAkB;AAEhC,SAAS,mBAAmB;AAC5B,SAA0B,yBAAyB;AAEnD,SAAS,kBAAkB;AAC3B,SAAS,cAAc,gBAAgB,mBAAmB;AAC1D,SAAS,iBAAiB;AAC1B,SAAS,iBAAiB;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,IAAI,IAAI,iBAAiB;AACxE,MAAI,qBAAqB,YAAY,oBAAoB,UAAU;AACnE,MAAI,CAAC,eAAe,kBAAkB,GAAG;AACvC,uBAAmB,cAAc,UAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,SAAO,aAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAC9B,aACA,oBACmC;AACnC,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,iBAAiB,WAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,oBAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,UAAU,WAAW,iBAAiB;AAC5C,QAAM;AAAA,IACJ,iBAAiB,EAAE,aAAa,eAAe;AAAA,EACjD,IAAI;AACJ,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,MAC3B,IAAI,iBAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,IAAI,IAAI,iBAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA2BA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CAAC,QAAgB,SAA8C;AACrF,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAI/C,MAAI,WAAsE,CAAC;AAC3E,MAAI,QAAQ,oBAAoB,MAAM;AACpC,eAAW,EAAE,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,EAC3D,WAAW,wBAAwB;AACjC,QAAI;AACF,YAAM,IAAI,uBAAuB,MAAM;AACvC,UAAI,MAAM,OAAW,YAAW,EAAE,gBAAgB,EAAE;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,cAAY,WAAW;AAAA,IACrB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/routing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { use, useContext } from 'react';\n\nimport { sendMessage } from './sandboxUtils';\nimport { NavigationState, TinkerableContext } from './TinkerableContext';\nimport { RouteParams, RoutingRule, RoutingSpec } from './RoutingSpec';\nimport { matchRoute } from './routeMatch';\nimport { constructUrl, isAbsolutePath, parseTarget } from './urlUtils';\nimport { joinPaths } from './pathUtils';\nimport { takeQueuedEntryState } from './entryState';\nimport { URLCHANGE } from './generated/protocol';\n\n/** The result of matching a path: the winning {@link RoutingRule} plus its captured params. */\nexport type AppliedRoutingRule = {\n routingRule: RoutingRule;\n pathParameters?: Record<string, string>;\n};\n\n/** Build the full outer href for an in-app target (absolute `sandboxPath` or a\n * path relative to the current route), e.g. for an `href` attribute. */\nexport const useTinkerableLink = (newSandboxLocation: string) => {\n const { outerHref, navigationState: navigation } = use(TinkerableContext);\n let newNavigationState = parseTarget(newSandboxLocation, navigation);\n if (!isAbsolutePath(newSandboxLocation)) {\n newNavigationState.sandboxPath = joinPaths(navigation.sandboxPath, newSandboxLocation);\n } else {\n newNavigationState.sandboxPath = newSandboxLocation;\n }\n return constructUrl(outerHref, newNavigationState);\n};\n\n/** Find the first rule in `routingSpec` whose pattern matches the current\n * `sandboxPath`, returning it with the captured params (or `undefined`). */\nexport const applyRoutingRule = (\n routingSpec: RoutingSpec,\n navigationState: NavigationState,\n): AppliedRoutingRule | undefined => {\n const { sandboxPath } = navigationState;\n for (const routingRule of routingSpec.routes) {\n const pathParameters = matchRoute(routingRule.pattern, sandboxPath);\n if (pathParameters) {\n return { routingRule, pathParameters };\n }\n }\n return undefined;\n};\n\n/** Render a matched rule, passing params to a `component` and falling back to `element`/`reactNode`. */\nexport const renderRoute = (routingRule: RoutingRule, params: RouteParams): ReactNode => {\n if (routingRule.component) {\n const Component = routingRule.component;\n return <Component params={params} />;\n }\n return routingRule.element ?? routingRule.reactNode ?? null;\n};\n\n/** Render the route matched for the current location (set up by `boot`'s route table). */\nexport const Router = () => {\n const context = useContext(TinkerableContext);\n const {\n navigationState: { routingRule, pathParameters },\n } = context;\n if (!routingRule) {\n // TODO: better error\n throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);\n }\n\n return renderRoute(routingRule, pathParameters ?? {});\n};\n\n/** Read the current route's matched params (`:name` segments and the `*` wildcard). */\nexport const useRouteParams = <T extends RouteParams = RouteParams>(): T =>\n (use(TinkerableContext).navigationState.pathParameters ?? {}) as T;\n\n/**\n * Read the current route: the matched rule's `name`, its `params`, the app-owned\n * `sandboxPath`, and the read-only platform prefix fields (`mode`, `provider`,\n * `namespace`, `repository`, `ref`) — e.g. to tell `/edit` from `/present`.\n */\nexport const useRoute = () => {\n const { navigationState } = use(TinkerableContext);\n const { routingRule, pathParameters, sandboxPath, mode, provider, namespace, repository, ref } = navigationState;\n return {\n name: routingRule?.name,\n params: (pathParameters ?? {}) as RouteParams,\n sandboxPath,\n mode,\n provider,\n namespace,\n repository,\n ref,\n };\n};\n\n/**\n * Navigate within the app. Messages the host to update the URL; the host then\n * pushes the new href back, which drives the actual route change.\n *\n * `opts.viewedDocument` (R3-268) optionally declares which WORKING-TREE file\n * this destination renders — a tri-state rider on the navigation event:\n * - omit the option entirely → the host derives the hint from the URL's\n * `files/` suffix convention (the zero-SDK default);\n * - `null` → this view shows no file (clears the highlight — tag pages,\n * search, home views);\n * - a repo-relative path (the CORPUS path under dispatch — only the viewer\n * can map its own key space) → the file explorer highlights it.\n * The hint is highlight-only by contract (it never scrolls, never moves focus\n * or panes, never switches the editor) and is validated host-side for\n * existence — a wrong path degrades to \"no highlight\", never an error. The\n * host remembers declarations per URL, so back/forward reproduces them\n * without re-announcement.\n */\n// R3-268: an app-registered rule mapping a navigation TARGET to its viewed\n// document, consulted by `navigate()` whenever the caller did not declare one\n// explicitly. Registered ONCE (e.g. at boot) so an app whose links all flow\n// through `<Link>`/`navigate` gets correct declarations everywhere without\n// threading an option through every call site. Return `undefined` for \"no\n// declaration\" (the host falls back to the URL convention), `null` for \"this\n// view shows no file\", or a working-tree repo-relative path.\nlet viewedDocumentResolver: ((targetHref: string) => string | null | undefined) | null = null;\n\n/** Register the app's route→viewed-document rule (R3-268); pass `null` to clear. */\nexport const setViewedDocumentResolver = (\n resolver: ((targetHref: string) => string | null | undefined) | null,\n): void => {\n viewedDocumentResolver = resolver;\n};\n\nexport const navigate = (target: string, opts?: { viewedDocument?: string | null }) => {\n console.log(`[Sandbox] Navigating to ${target}`);\n // Explicit option first; else the registered resolver; else nothing on the\n // wire (the host derives from the URL convention). A resolver throw is\n // swallowed to \"no declaration\" — a mapping bug must never break navigation.\n let declared: { viewedDocument: string | null } | Record<string, never> = {};\n if (opts && 'viewedDocument' in opts) {\n declared = { viewedDocument: opts.viewedDocument ?? null };\n } else if (viewedDocumentResolver) {\n try {\n const v = viewedDocumentResolver(target);\n if (v !== undefined) declared = { viewedDocument: v };\n } catch {\n /* no declaration */\n }\n }\n // The scratch for the entry we are LEAVING, gathered synchronously here because\n // this call is the one moment the app knows a navigation is happening (R3-627).\n // The host stamps it on the current entry before pushing the target, and hands it\n // back if the reader ever returns; it never parses it.\n const entryState = takeQueuedEntryState();\n sendMessage(URLCHANGE, {\n url: target,\n back: false,\n forward: false,\n ...(entryState ? { entryState } : {}),\n ...declared,\n });\n};\n"],"mappings":";AAmDW;AAlDX,SAAS,KAAK,kBAAkB;AAEhC,SAAS,mBAAmB;AAC5B,SAA0B,yBAAyB;AAEnD,SAAS,kBAAkB;AAC3B,SAAS,cAAc,gBAAgB,mBAAmB;AAC1D,SAAS,iBAAiB;AAC1B,SAAS,4BAA4B;AACrC,SAAS,iBAAiB;AAUnB,MAAM,oBAAoB,CAAC,uBAA+B;AAC/D,QAAM,EAAE,WAAW,iBAAiB,WAAW,IAAI,IAAI,iBAAiB;AACxE,MAAI,qBAAqB,YAAY,oBAAoB,UAAU;AACnE,MAAI,CAAC,eAAe,kBAAkB,GAAG;AACvC,uBAAmB,cAAc,UAAU,WAAW,aAAa,kBAAkB;AAAA,EACvF,OAAO;AACL,uBAAmB,cAAc;AAAA,EACnC;AACA,SAAO,aAAa,WAAW,kBAAkB;AACnD;AAIO,MAAM,mBAAmB,CAC9B,aACA,oBACmC;AACnC,QAAM,EAAE,YAAY,IAAI;AACxB,aAAW,eAAe,YAAY,QAAQ;AAC5C,UAAM,iBAAiB,WAAW,YAAY,SAAS,WAAW;AAClE,QAAI,gBAAgB;AAClB,aAAO,EAAE,aAAa,eAAe;AAAA,IACvC;AAAA,EACF;AACA,SAAO;AACT;AAGO,MAAM,cAAc,CAAC,aAA0B,WAAmC;AACvF,MAAI,YAAY,WAAW;AACzB,UAAM,YAAY,YAAY;AAC9B,WAAO,oBAAC,aAAU,QAAgB;AAAA,EACpC;AACA,SAAO,YAAY,WAAW,YAAY,aAAa;AACzD;AAGO,MAAM,SAAS,MAAM;AAC1B,QAAM,UAAU,WAAW,iBAAiB;AAC5C,QAAM;AAAA,IACJ,iBAAiB,EAAE,aAAa,eAAe;AAAA,EACjD,IAAI;AACJ,MAAI,CAAC,aAAa;AAEhB,UAAM,IAAI,MAAM,gCAAgC,QAAQ,gBAAgB,WAAW,GAAG;AAAA,EACxF;AAEA,SAAO,YAAY,aAAa,kBAAkB,CAAC,CAAC;AACtD;AAGO,MAAM,iBAAiB,MAC3B,IAAI,iBAAiB,EAAE,gBAAgB,kBAAkB,CAAC;AAOtD,MAAM,WAAW,MAAM;AAC5B,QAAM,EAAE,gBAAgB,IAAI,IAAI,iBAAiB;AACjD,QAAM,EAAE,aAAa,gBAAgB,aAAa,MAAM,UAAU,WAAW,YAAY,IAAI,IAAI;AACjG,SAAO;AAAA,IACL,MAAM,aAAa;AAAA,IACnB,QAAS,kBAAkB,CAAC;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AA2BA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CAAC,QAAgB,SAA8C;AACrF,UAAQ,IAAI,2BAA2B,MAAM,EAAE;AAI/C,MAAI,WAAsE,CAAC;AAC3E,MAAI,QAAQ,oBAAoB,MAAM;AACpC,eAAW,EAAE,gBAAgB,KAAK,kBAAkB,KAAK;AAAA,EAC3D,WAAW,wBAAwB;AACjC,QAAI;AACF,YAAM,IAAI,uBAAuB,MAAM;AACvC,UAAI,MAAM,OAAW,YAAW,EAAE,gBAAgB,EAAE;AAAA,IACtD,QAAQ;AAAA,IAER;AAAA,EACF;AAKA,QAAM,aAAa,qBAAqB;AACxC,cAAY,WAAW;AAAA,IACrB,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,IACnC,GAAG;AAAA,EACL,CAAC;AACH;","names":[]}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var scrollRestore_exports = {};
20
+ __export(scrollRestore_exports, {
21
+ RESTORE_DEADLINE_MS: () => RESTORE_DEADLINE_MS,
22
+ RESTORE_EPSILON_PX: () => RESTORE_EPSILON_PX,
23
+ nextRestoreAction: () => nextRestoreAction
24
+ });
25
+ module.exports = __toCommonJS(scrollRestore_exports);
26
+ const RESTORE_DEADLINE_MS = 900;
27
+ const RESTORE_EPSILON_PX = 2;
28
+ const nextRestoreAction = (sample) => {
29
+ const { target, scrollHeight, clientHeight, current, elapsedMs, userScrolled } = sample;
30
+ if (!Number.isFinite(target) || target <= 0) return "abandon";
31
+ if (userScrolled) return "abandon";
32
+ if (Math.abs(current - target) <= RESTORE_EPSILON_PX) return "apply";
33
+ if (elapsedMs >= RESTORE_DEADLINE_MS) return "abandon";
34
+ const reachable = Math.max(0, scrollHeight - clientHeight);
35
+ return reachable >= target ? "apply" : "wait";
36
+ };
37
+ // Annotate the CommonJS export names for ESM import in node:
38
+ 0 && (module.exports = {
39
+ RESTORE_DEADLINE_MS,
40
+ RESTORE_EPSILON_PX,
41
+ nextRestoreAction
42
+ });
43
+ //# sourceMappingURL=scrollRestore.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scrollRestore.ts"],"sourcesContent":["// Deciding WHEN a remembered scroll offset may be applied after a back/forward\n// traversal (R3-627). Pure: no DOM, no React, no timers — the caller samples the\n// geometry and this says what to do with the sample.\n//\n// The failure this exists to prevent: an app's document grows as it renders, so an\n// offset applied on arrival is clamped to whatever height exists at that instant and\n// the reader lands near the top — the very defect the feature is meant to remove. So\n// the restore waits until the content is tall enough to hold the offset, and gives up\n// rather than fighting either the clock or the reader.\n\n/** A geometry sample of the scroller, plus how the attempt is going. */\nexport interface RestoreSample {\n /** The remembered offset we are trying to reach. */\n target: number;\n /** The scroller's full scrollable height right now. */\n scrollHeight: number;\n /** The scroller's visible height right now. */\n clientHeight: number;\n /** Where the scroller is right now. */\n current: number;\n /** Milliseconds since the restore began. */\n elapsedMs: number;\n /** Whether the reader has scrolled since the restore began. */\n userScrolled: boolean;\n}\n\n/** What the caller should do with this sample. */\nexport type RestoreAction = 'apply' | 'wait' | 'abandon';\n\n/** How long to keep waiting for the content to grow before giving up. Matches the\n * give-up window `ScrollAfterNavigation` already uses for fragments, so the two\n * navigation-scroll behaviours settle on the same timescale. */\nexport const RESTORE_DEADLINE_MS = 900;\n\n/** How close counts as arrived. Sub-pixel differences and fractional device pixels\n * must not keep a restore looping. */\nexport const RESTORE_EPSILON_PX = 2;\n\n/**\n * The one decision, given a sample.\n *\n * - `abandon` — the reader has taken over, or the deadline passed. Never fight a\n * user, and never scroll a page they have already started reading.\n * - `apply` — the content can hold the offset (or the offset is already reached,\n * within {@link RESTORE_EPSILON_PX}); scroll and finish.\n * - `wait` — the content is still too short; sample again.\n *\n * A non-finite or negative target is treated as nothing to restore (`abandon`), so a\n * corrupt scratch value can never move the page.\n */\nexport const nextRestoreAction = (sample: RestoreSample): RestoreAction => {\n const { target, scrollHeight, clientHeight, current, elapsedMs, userScrolled } = sample;\n if (!Number.isFinite(target) || target <= 0) return 'abandon';\n if (userScrolled) return 'abandon';\n if (Math.abs(current - target) <= RESTORE_EPSILON_PX) return 'apply';\n if (elapsedMs >= RESTORE_DEADLINE_MS) return 'abandon';\n // The furthest this scroller can currently reach. Applying before the content is\n // this tall is what clamps the reader to the top.\n const reachable = Math.max(0, scrollHeight - clientHeight);\n return reachable >= target ? 'apply' : 'wait';\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCO,MAAM,sBAAsB;AAI5B,MAAM,qBAAqB;AAc3B,MAAM,oBAAoB,CAAC,WAAyC;AACzE,QAAM,EAAE,QAAQ,cAAc,cAAc,SAAS,WAAW,aAAa,IAAI;AACjF,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG,QAAO;AACpD,MAAI,aAAc,QAAO;AACzB,MAAI,KAAK,IAAI,UAAU,MAAM,KAAK,mBAAoB,QAAO;AAC7D,MAAI,aAAa,oBAAqB,QAAO;AAG7C,QAAM,YAAY,KAAK,IAAI,GAAG,eAAe,YAAY;AACzD,SAAO,aAAa,SAAS,UAAU;AACzC;","names":[]}
@@ -0,0 +1,39 @@
1
+ /** A geometry sample of the scroller, plus how the attempt is going. */
2
+ interface RestoreSample {
3
+ /** The remembered offset we are trying to reach. */
4
+ target: number;
5
+ /** The scroller's full scrollable height right now. */
6
+ scrollHeight: number;
7
+ /** The scroller's visible height right now. */
8
+ clientHeight: number;
9
+ /** Where the scroller is right now. */
10
+ current: number;
11
+ /** Milliseconds since the restore began. */
12
+ elapsedMs: number;
13
+ /** Whether the reader has scrolled since the restore began. */
14
+ userScrolled: boolean;
15
+ }
16
+ /** What the caller should do with this sample. */
17
+ type RestoreAction = 'apply' | 'wait' | 'abandon';
18
+ /** How long to keep waiting for the content to grow before giving up. Matches the
19
+ * give-up window `ScrollAfterNavigation` already uses for fragments, so the two
20
+ * navigation-scroll behaviours settle on the same timescale. */
21
+ declare const RESTORE_DEADLINE_MS = 900;
22
+ /** How close counts as arrived. Sub-pixel differences and fractional device pixels
23
+ * must not keep a restore looping. */
24
+ declare const RESTORE_EPSILON_PX = 2;
25
+ /**
26
+ * The one decision, given a sample.
27
+ *
28
+ * - `abandon` — the reader has taken over, or the deadline passed. Never fight a
29
+ * user, and never scroll a page they have already started reading.
30
+ * - `apply` — the content can hold the offset (or the offset is already reached,
31
+ * within {@link RESTORE_EPSILON_PX}); scroll and finish.
32
+ * - `wait` — the content is still too short; sample again.
33
+ *
34
+ * A non-finite or negative target is treated as nothing to restore (`abandon`), so a
35
+ * corrupt scratch value can never move the page.
36
+ */
37
+ declare const nextRestoreAction: (sample: RestoreSample) => RestoreAction;
38
+
39
+ export { RESTORE_DEADLINE_MS, RESTORE_EPSILON_PX, type RestoreAction, type RestoreSample, nextRestoreAction };
@@ -0,0 +1,39 @@
1
+ /** A geometry sample of the scroller, plus how the attempt is going. */
2
+ interface RestoreSample {
3
+ /** The remembered offset we are trying to reach. */
4
+ target: number;
5
+ /** The scroller's full scrollable height right now. */
6
+ scrollHeight: number;
7
+ /** The scroller's visible height right now. */
8
+ clientHeight: number;
9
+ /** Where the scroller is right now. */
10
+ current: number;
11
+ /** Milliseconds since the restore began. */
12
+ elapsedMs: number;
13
+ /** Whether the reader has scrolled since the restore began. */
14
+ userScrolled: boolean;
15
+ }
16
+ /** What the caller should do with this sample. */
17
+ type RestoreAction = 'apply' | 'wait' | 'abandon';
18
+ /** How long to keep waiting for the content to grow before giving up. Matches the
19
+ * give-up window `ScrollAfterNavigation` already uses for fragments, so the two
20
+ * navigation-scroll behaviours settle on the same timescale. */
21
+ declare const RESTORE_DEADLINE_MS = 900;
22
+ /** How close counts as arrived. Sub-pixel differences and fractional device pixels
23
+ * must not keep a restore looping. */
24
+ declare const RESTORE_EPSILON_PX = 2;
25
+ /**
26
+ * The one decision, given a sample.
27
+ *
28
+ * - `abandon` — the reader has taken over, or the deadline passed. Never fight a
29
+ * user, and never scroll a page they have already started reading.
30
+ * - `apply` — the content can hold the offset (or the offset is already reached,
31
+ * within {@link RESTORE_EPSILON_PX}); scroll and finish.
32
+ * - `wait` — the content is still too short; sample again.
33
+ *
34
+ * A non-finite or negative target is treated as nothing to restore (`abandon`), so a
35
+ * corrupt scratch value can never move the page.
36
+ */
37
+ declare const nextRestoreAction: (sample: RestoreSample) => RestoreAction;
38
+
39
+ export { RESTORE_DEADLINE_MS, RESTORE_EPSILON_PX, type RestoreAction, type RestoreSample, nextRestoreAction };
@@ -0,0 +1,18 @@
1
+ import "./chunk-VHAA22YE.js";
2
+ const RESTORE_DEADLINE_MS = 900;
3
+ const RESTORE_EPSILON_PX = 2;
4
+ const nextRestoreAction = (sample) => {
5
+ const { target, scrollHeight, clientHeight, current, elapsedMs, userScrolled } = sample;
6
+ if (!Number.isFinite(target) || target <= 0) return "abandon";
7
+ if (userScrolled) return "abandon";
8
+ if (Math.abs(current - target) <= RESTORE_EPSILON_PX) return "apply";
9
+ if (elapsedMs >= RESTORE_DEADLINE_MS) return "abandon";
10
+ const reachable = Math.max(0, scrollHeight - clientHeight);
11
+ return reachable >= target ? "apply" : "wait";
12
+ };
13
+ export {
14
+ RESTORE_DEADLINE_MS,
15
+ RESTORE_EPSILON_PX,
16
+ nextRestoreAction
17
+ };
18
+ //# sourceMappingURL=scrollRestore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scrollRestore.ts"],"sourcesContent":["// Deciding WHEN a remembered scroll offset may be applied after a back/forward\n// traversal (R3-627). Pure: no DOM, no React, no timers — the caller samples the\n// geometry and this says what to do with the sample.\n//\n// The failure this exists to prevent: an app's document grows as it renders, so an\n// offset applied on arrival is clamped to whatever height exists at that instant and\n// the reader lands near the top — the very defect the feature is meant to remove. So\n// the restore waits until the content is tall enough to hold the offset, and gives up\n// rather than fighting either the clock or the reader.\n\n/** A geometry sample of the scroller, plus how the attempt is going. */\nexport interface RestoreSample {\n /** The remembered offset we are trying to reach. */\n target: number;\n /** The scroller's full scrollable height right now. */\n scrollHeight: number;\n /** The scroller's visible height right now. */\n clientHeight: number;\n /** Where the scroller is right now. */\n current: number;\n /** Milliseconds since the restore began. */\n elapsedMs: number;\n /** Whether the reader has scrolled since the restore began. */\n userScrolled: boolean;\n}\n\n/** What the caller should do with this sample. */\nexport type RestoreAction = 'apply' | 'wait' | 'abandon';\n\n/** How long to keep waiting for the content to grow before giving up. Matches the\n * give-up window `ScrollAfterNavigation` already uses for fragments, so the two\n * navigation-scroll behaviours settle on the same timescale. */\nexport const RESTORE_DEADLINE_MS = 900;\n\n/** How close counts as arrived. Sub-pixel differences and fractional device pixels\n * must not keep a restore looping. */\nexport const RESTORE_EPSILON_PX = 2;\n\n/**\n * The one decision, given a sample.\n *\n * - `abandon` — the reader has taken over, or the deadline passed. Never fight a\n * user, and never scroll a page they have already started reading.\n * - `apply` — the content can hold the offset (or the offset is already reached,\n * within {@link RESTORE_EPSILON_PX}); scroll and finish.\n * - `wait` — the content is still too short; sample again.\n *\n * A non-finite or negative target is treated as nothing to restore (`abandon`), so a\n * corrupt scratch value can never move the page.\n */\nexport const nextRestoreAction = (sample: RestoreSample): RestoreAction => {\n const { target, scrollHeight, clientHeight, current, elapsedMs, userScrolled } = sample;\n if (!Number.isFinite(target) || target <= 0) return 'abandon';\n if (userScrolled) return 'abandon';\n if (Math.abs(current - target) <= RESTORE_EPSILON_PX) return 'apply';\n if (elapsedMs >= RESTORE_DEADLINE_MS) return 'abandon';\n // The furthest this scroller can currently reach. Applying before the content is\n // this tall is what clamps the reader to the top.\n const reachable = Math.max(0, scrollHeight - clientHeight);\n return reachable >= target ? 'apply' : 'wait';\n};\n"],"mappings":";AAgCO,MAAM,sBAAsB;AAI5B,MAAM,qBAAqB;AAc3B,MAAM,oBAAoB,CAAC,WAAyC;AACzE,QAAM,EAAE,QAAQ,cAAc,cAAc,SAAS,WAAW,aAAa,IAAI;AACjF,MAAI,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,EAAG,QAAO;AACpD,MAAI,aAAc,QAAO;AACzB,MAAI,KAAK,IAAI,UAAU,MAAM,KAAK,mBAAoB,QAAO;AAC7D,MAAI,aAAa,oBAAqB,QAAO;AAG7C,QAAM,YAAY,KAAK,IAAI,GAAG,eAAe,YAAY;AACzD,SAAO,aAAa,SAAS,UAAU;AACzC;","names":[]}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var useEntryState_exports = {};
20
+ __export(useEntryState_exports, {
21
+ useEntryState: () => useEntryState,
22
+ useNavigationDirection: () => useNavigationDirection
23
+ });
24
+ module.exports = __toCommonJS(useEntryState_exports);
25
+ var import_react = require("react");
26
+ var import_entryState = require("./entryState");
27
+ const useEntryState = (key) => {
28
+ const arrived = (0, import_react.useSyncExternalStore)(import_entryState.subscribeNavigation, import_entryState.getArrivedNavigation, import_entryState.getArrivedNavigation);
29
+ const save = (0, import_react.useCallback)((value) => (0, import_entryState.saveEntryState)(key, value), [key]);
30
+ return { value: arrived.state?.[key], save };
31
+ };
32
+ const useNavigationDirection = () => {
33
+ const arrived = (0, import_react.useSyncExternalStore)(import_entryState.subscribeNavigation, import_entryState.getArrivedNavigation, import_entryState.getArrivedNavigation);
34
+ return arrived.direction;
35
+ };
36
+ // Annotate the CommonJS export names for ESM import in node:
37
+ 0 && (module.exports = {
38
+ useEntryState,
39
+ useNavigationDirection
40
+ });
41
+ //# sourceMappingURL=useEntryState.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useEntryState.ts"],"sourcesContent":["// React surface over the per-history-entry scratch (R3-627). The store itself is in\n// `entryState.ts` and is framework-free; this file is the thin binding so a component\n// re-renders when the host tells the frame which entry it just arrived at.\n\nimport { useCallback, useSyncExternalStore } from 'react';\n\nimport { getArrivedNavigation, saveEntryState, subscribeNavigation, type NavigationDirection } from './entryState';\n\n/**\n * Read the value this app left on the current history entry, and queue the value to\n * leave on the entry the next navigation departs from.\n *\n * `value` is `undefined` on an ordinary forward navigation — there is no bookmark for\n * a page being visited for the first time — and on a traversal to an entry that was\n * stamped by nothing (a navigation the app did not initiate).\n *\n * `save` queues; it does not send. The queued value travels with the next\n * `navigate()`, which is the only moment the app knows an entry is being left.\n */\nexport const useEntryState = <T>(key: string): { value: T | undefined; save: (value: T) => void } => {\n const arrived = useSyncExternalStore(subscribeNavigation, getArrivedNavigation, getArrivedNavigation);\n const save = useCallback((value: T) => saveEntryState(key, value), [key]);\n return { value: arrived.state?.[key] as T | undefined, save };\n};\n\n/**\n * How the browser reached the page being rendered: `push` for an ordinary\n * navigation, `back`/`forward` for a history traversal.\n *\n * An app that resets its own view on arrival — scrolling a container to the top is\n * the usual one — should stand down on a traversal, so a restored position is not\n * immediately thrown away.\n */\nexport const useNavigationDirection = (): NavigationDirection => {\n const arrived = useSyncExternalStore(subscribeNavigation, getArrivedNavigation, getArrivedNavigation);\n return arrived.direction;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,mBAAkD;AAElD,wBAAoG;AAa7F,MAAM,gBAAgB,CAAI,QAAoE;AACnG,QAAM,cAAU,mCAAqB,uCAAqB,wCAAsB,sCAAoB;AACpG,QAAM,WAAO,0BAAY,CAAC,cAAa,kCAAe,KAAK,KAAK,GAAG,CAAC,GAAG,CAAC;AACxE,SAAO,EAAE,OAAO,QAAQ,QAAQ,GAAG,GAAoB,KAAK;AAC9D;AAUO,MAAM,yBAAyB,MAA2B;AAC/D,QAAM,cAAU,mCAAqB,uCAAqB,wCAAsB,sCAAoB;AACpG,SAAO,QAAQ;AACjB;","names":[]}