{"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, type UrlchangePayload } 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 * `opts.replace` (R3-874, APP_CUSTOMIZATION_SPEC §5): `true` asks the host to\n * REPLACE the current history entry instead of pushing — for an app whose URL\n * encodes view state, so re-renders don't flood the reader's Back chain.\n * In-prefix targets only: the host ignores it for a navigation that leaves the\n * app (an app must not erase the entry the user would press Back to reach,\n * G-CUST-6). Absent or `false` keeps the field off the wire entirely.\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; replace?: boolean }) => {\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  //\n  // R3-874: `replace: true` asks the host to REPLACE the current entry instead of\n  // pushing (an app whose URL encodes view state would otherwise flood history).\n  // The host ignores it for out-of-prefix targets (an app must not erase the entry\n  // the user would press Back to reach — G-CUST-6). The message is built as ONE\n  // typed value — the wire-shape gate fingerprints the declared UrlchangePayload,\n  // and a spread-built union would extract as 'any' and never match (protocol#38).\n  const entryState = takeQueuedEntryState();\n  const message: UrlchangePayload = {\n    url: target,\n    back: false,\n    forward: false,\n    ...(entryState ? { entryState } : {}),\n    ...declared,\n    ...(opts?.replace === true ? { replace: true as const } : {}),\n  };\n  sendMessage(URLCHANGE, message);\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,sBAAiD;AAU1C,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;AAkCA,IAAI,yBAAqF;AAGlF,MAAM,4BAA4B,CACvC,aACS;AACT,2BAAyB;AAC3B;AAEO,MAAM,WAAW,CAAC,QAAgB,SAAiE;AACxG,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;AAYA,QAAM,iBAAa,wCAAqB;AACxC,QAAM,UAA4B;AAAA,IAChC,KAAK;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,IACnC,GAAG;AAAA,IACH,GAAI,MAAM,YAAY,OAAO,EAAE,SAAS,KAAc,IAAI,CAAC;AAAA,EAC7D;AACA,uCAAY,2BAAW,OAAO;AAChC;","names":[]}