@stratal/inertia-modal 0.0.27 → 0.1.1
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.
- package/CHANGELOG.md +298 -0
- package/README.md +223 -0
- package/dist/index.d.mts +85 -22
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +437 -123
- package/dist/index.mjs.map +1 -1
- package/dist/level-path-DCJD-aS3.mjs +33 -0
- package/dist/level-path-DCJD-aS3.mjs.map +1 -0
- package/dist/page-props-BCEWEw3V.d.mts +23 -0
- package/dist/page-props-BCEWEw3V.d.mts.map +1 -0
- package/dist/react.d.mts +144 -21
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +557 -46
- package/dist/react.mjs.map +1 -1
- package/dist/testing.d.mts +33 -0
- package/dist/testing.d.mts.map +1 -0
- package/dist/testing.mjs +99 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/wire-BNVvmku4.mjs +93 -0
- package/dist/wire-BNVvmku4.mjs.map +1 -0
- package/dist/wire-CbwmWkPr.d.mts +54 -0
- package/dist/wire-CbwmWkPr.d.mts.map +1 -0
- package/package.json +41 -17
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["RequestScoped"],"sources":["../src/tokens.ts","../src/augment/router-context.ts","../src/errors/modal-background-fetch.error.ts","../src/services/modal.service.ts","../src/modal.module.ts"],"sourcesContent":["export const MODAL_TOKENS = {\n ModalService: Symbol.for('stratal:inertia-modal:service'),\n} as const\n","import { RouterContext } from 'stratal/router'\nimport type { ModalRenderOptions, ModalService } from '../services/modal.service'\nimport { MODAL_TOKENS } from '../tokens'\n\ndeclare module 'stratal/router' {\n interface RouterContext {\n /**\n * Renders a modal page component over a background page.\n *\n * The background page at `options.baseURL` is always rendered as the main\n * Inertia page. The given `component` and `props` are embedded in the\n * background page's `modal` prop and rendered as an overlay by the\n * client-side `<Modal>` component.\n *\n * Handles direct URL visits by fetching the background page in-process.\n * Handles partial reloads (e.g., cascading selects) when `only: ['modal']`\n * is requested.\n */\n inertiaModal(\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ): Promise<Response>\n }\n}\n\nexport function augmentRouterContextWithModal(\n resolveService: (ctx: RouterContext) => ModalService,\n): void {\n RouterContext.macro('inertiaModal', function (\n this: RouterContext,\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ) {\n const service = resolveService(this)\n return service.render(this, component, props, options)\n })\n}\n\nexport { MODAL_TOKENS }\n","import { HttpException } from 'stratal/errors'\n\n/**\n * Thrown when the internal sub-request to fetch the background page fails\n * (e.g., non-2xx response, redirect, or empty body).\n *\n * HTTP Status: 502 Bad Gateway — the modal service acted as a proxy and the\n * upstream (background page) returned an unexpected response.\n */\nexport class ModalBackgroundFetchError extends HttpException {\n constructor() {\n super(502, 'Failed to load background page for modal')\n }\n}\n","import type { Page } from '@inertiajs/core'\nimport { INERTIA_TOKENS, type SsrRendererService, type TemplateService } from '@stratal/inertia'\nimport { Request as RequestScoped, inject } from 'stratal/di'\nimport type { RouterContext } from 'stratal/router'\nimport { ROUTER_TOKENS } from 'stratal/router'\nimport { ModalBackgroundFetchError } from '../errors/modal-background-fetch.error'\n\nexport interface ModalData {\n component: string\n props: Record<string, unknown>\n baseURL: string\n redirectURL: string\n key: string\n nativeBack: boolean\n}\n\nexport interface ModalRenderOptions {\n baseURL: string\n}\n\n// Page from @inertiajs/core doesn't have a 'modal' prop — we extend it here\ntype PageWithModal = Page\n\n// HonoApp extends OpenAPIHono which extends Hono — it has a standard fetch() method\ninterface FetchableApp {\n fetch(request: Request, env: unknown, ctx: unknown): Promise<Response>\n}\n\n@RequestScoped()\nexport class ModalService {\n constructor(\n @inject(ROUTER_TOKENS.HonoApp) private readonly app: FetchableApp,\n @inject(INERTIA_TOKENS.SsrRenderer) private readonly ssr: SsrRendererService,\n @inject(INERTIA_TOKENS.TemplateService) private readonly template: TemplateService,\n ) { }\n\n async render(\n ctx: RouterContext,\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ): Promise<Response> {\n const isInertia = ctx.c.req.header('x-inertia') === 'true'\n const partialComponent = ctx.c.req.header('x-inertia-partial-component')\n const partialData = ctx.c.req.header('x-inertia-partial-data')\n\n const redirectURL = this.resolveRedirectURL(ctx, options.baseURL)\n const key = ctx.c.req.header('x-inertia-modal-key') ?? crypto.randomUUID()\n const modalURL = new URL(ctx.c.req.url).pathname\n\n // Partial reload requesting 'modal' — skip background sub-request,\n // return just the modal prop with fresh data. The client already has\n // the background page loaded, so nativeBack: true tells useModal()\n // to use history.back() on close instead of a server round-trip.\n if (isInertia && partialComponent && partialData) {\n const requestedProps = partialData.split(',').map((s) => s.trim())\n if (requestedProps.includes('modal')) {\n const partialModalData: ModalData = {\n component,\n props,\n baseURL: options.baseURL,\n redirectURL,\n key,\n nativeBack: true,\n }\n const page: PageWithModal = {\n component: partialComponent,\n props: { modal: partialModalData, errors: {} },\n url: modalURL,\n version: null,\n flash: {},\n rememberedState: {},\n rescuedProps: [],\n }\n return new Response(JSON.stringify(page), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Inertia': 'true',\n 'Vary': 'X-Inertia',\n },\n })\n }\n }\n\n const modalData: ModalData = {\n component,\n props,\n baseURL: options.baseURL,\n redirectURL,\n key,\n nativeBack: false,\n }\n\n // Fetch background page as an Inertia JSON request to get its component and\n // props without triggering SSR. We will run SSR ourselves below with the\n // combined page object so that page.url equals the modal URL in both the\n // SSR output and on the client — preventing React hydration mismatches.\n const bgResponse = await this.fetchBackground(ctx, redirectURL)\n const bgText = await bgResponse.text()\n if (!bgText || bgResponse.status >= 300) {\n throw new ModalBackgroundFetchError()\n }\n const bgPage = JSON.parse(bgText) as PageWithModal\n\n // Build the combined page: background props + modal data, URL = modal URL.\n // Setting url to the modal URL ensures Inertia's InitialVisit.handleDefault\n // calls history.replaceState with the modal URL (matching window.location),\n // so the address bar stays at the modal URL on direct visits.\n const combinedPage: PageWithModal = {\n ...bgPage,\n props: { ...bgPage.props, modal: modalData },\n url: modalURL,\n }\n\n if (isInertia) {\n // Inertia AJAX navigation: return JSON\n return new Response(JSON.stringify(combinedPage), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Inertia': 'true',\n 'Vary': 'X-Inertia',\n },\n })\n }\n\n // Full-page (direct visit): run SSR with the combined page so that\n // page.url = modalURL in both the server-rendered HTML and the client\n // hydration pass. The Modal component renders null during SSR (effects\n // don't run server-side), so there is no hydration mismatch.\n const { head, stream } = await this.ssr.render(combinedPage)\n const body = this.template.renderStream(combinedPage, head, stream)\n return new Response(body, {\n status: 200,\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n })\n }\n\n private resolveRedirectURL(ctx: RouterContext, baseURL: string): string {\n const referer = ctx.c.req.header('referer')\n const isInertia = ctx.c.req.header('x-inertia') === 'true'\n\n if (isInertia && referer) {\n try {\n const refererURL = new URL(referer)\n const currentURL = new URL(ctx.c.req.url)\n if (refererURL.pathname !== currentURL.pathname) {\n // Preserve the query string so the background page (and the\n // post-close redirect) keeps the filter/pagination state the\n // user had on the list view — without this, opening a modal\n // resets the parent page to defaults.\n return refererURL.pathname + refererURL.search\n }\n }\n catch {\n // malformed referer — fall through to baseURL\n }\n }\n\n return baseURL\n }\n\n private async fetchBackground(ctx: RouterContext, url: string): Promise<Response> {\n const currentURL = new URL(ctx.c.req.url)\n const bgURL = new URL(url, currentURL.origin)\n\n const headers: Record<string, string> = {\n // Always request JSON — we run SSR ourselves with the combined page object\n 'x-inertia': 'true',\n // Eagerly resolve deferred props so the background page renders with data\n 'x-inertia-resolve-deferred': 'true',\n // Deliberately omit x-inertia-version: the InertiaMiddleware version check\n // returns a 409 with no body when versions don't match, which would make\n // JSON.parse fail. Internal sub-requests don't need cache-bust checks.\n 'accept': 'application/json',\n // Forward auth/session cookies so the background request is authenticated\n 'cookie': ctx.c.req.header('cookie') ?? '',\n // Forward the host header so domain-pattern middleware can match the\n // request against the configured domain pattern. Without this, the host\n // resolves to the URL's origin (e.g., localhost:1234) which won't match\n // patterns like '{tenant}.admsn.test', causing a DomainMismatchError.\n 'host': ctx.c.req.header('host') ?? '',\n }\n\n // Forward proxy/forwarded-for headers when present so middleware that\n // reconstructs the canonical request URL (e.g. setting `appUrl` to\n // `https://...`) sees the same protocol/host the original request had.\n // Without this, downstream auth (better-auth's secure-cookie prefix is\n // derived from `baseURL`'s protocol) would look up the wrong cookie name\n // and the bg fetch would be unauthenticated — even though the cookie is\n // forwarded above.\n const passthrough = [\n 'x-forwarded-proto',\n 'x-forwarded-host',\n 'x-forwarded-for',\n 'x-forwarded-port',\n 'x-real-ip',\n 'accept-language',\n 'user-agent',\n ] as const\n for (const name of passthrough) {\n const value = ctx.c.req.header(name)\n if (value) headers[name] = value\n }\n\n const bgRequest = new Request(bgURL.toString(), { method: 'GET', headers })\n\n return this.app.fetch(bgRequest, ctx.c.env, ctx.c.executionCtx)\n }\n}\n","import type { OnInitialize } from 'stratal/module'\nimport { Module } from 'stratal/module'\nimport { augmentRouterContextWithModal } from './augment/router-context'\nimport { ModalService } from './services/modal.service'\nimport { MODAL_TOKENS } from './tokens'\n\n@Module({\n providers: [\n { provide: MODAL_TOKENS.ModalService, useClass: ModalService },\n ],\n})\nexport class ModalModule implements OnInitialize {\n onInitialize(): void {\n augmentRouterContextWithModal((ctx) => {\n return ctx.getContainer().resolve<ModalService>(MODAL_TOKENS.ModalService)\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["RequestScoped"],"sources":["../src/tokens.ts","../src/augment/router-context.ts","../src/i18n/en.ts","../src/errors/modal-background-fetch.error.ts","../src/errors/modal-base-cycle.error.ts","../src/server/background.ts","../src/core/close-target.ts","../src/core/level-props.ts","../src/server/modal.service.ts","../src/modal.module.ts"],"sourcesContent":["export const MODAL_TOKENS = {\n ModalService: Symbol.for('stratal:inertia-modal:service'),\n /**\n * How the page beneath a modal is fetched on a document request. Override it to dispatch through\n * something other than the app in process.\n */\n BackgroundDispatcher: Symbol.for('stratal:inertia-modal:background-dispatcher'),\n} as const\n","import { RouterContext } from 'stratal/router'\nimport type { ModalRenderOptions, ModalService } from '../server/modal.service'\nimport { MODAL_TOKENS } from '../tokens'\n\ndeclare module 'stratal/router' {\n interface RouterContext {\n /**\n * Renders `component` as a modal over whatever the client already has mounted.\n *\n * `options.base` declares what sits beneath this level — a page route, or another modal route.\n * On a document request that chain is followed and rendered, so a modal URL stays a permalink;\n * on an Inertia visit only this level is sent, and the client grafts it onto the page it holds.\n */\n modal(\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ): Promise<Response>\n }\n}\n\nexport function augmentRouterContextWithModal(\n resolveService: (ctx: RouterContext) => ModalService,\n): void {\n RouterContext.macro('modal', function (\n this: RouterContext,\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ) {\n return resolveService(this).render(this, component, props, options)\n })\n}\n\nexport { MODAL_TOKENS }\n","export const modalMessages = {\n en: {\n errors: {\n backgroundFetchFailed: 'Failed to load background page for modal',\n baseCycle: 'The modal base chain leads back to {url}',\n },\n },\n} as const\n\ndeclare module 'stratal/i18n' {\n interface AppMessageNamespaces {\n modal: typeof modalMessages['en']\n }\n}\n","import { HttpException } from 'stratal/errors'\nimport { withI18n } from 'stratal/i18n'\n\n/**\n * Thrown when the sub-request for the page beneath a modal answers with something the chain cannot\n * be built from — a non-2xx, a redirect, an empty body, a body that is not a page, or a level of a\n * shape this build cannot read.\n *\n * `cause` carries the parse failure where there was one. The status this reports is a property of\n * the exchange, not of the reason, so every one of those answers the caller identically; a `base`\n * pointing at a route that does not render a page is still a mistake someone has to find, and the\n * reason is the only thing that says which mistake it was.\n *\n * HTTP Status: 502 Bad Gateway — this service acted as a proxy and the upstream answered\n * unexpectedly.\n */\nexport class ModalBackgroundFetchError extends HttpException {\n constructor(cause?: unknown) {\n super(502, withI18n('modal.errors.backgroundFetchFailed'), cause)\n }\n}\n","import { HttpException } from 'stratal/errors'\nimport { withI18n } from 'stratal/i18n'\n\n/**\n * Raised when a route's `base` chain leads back to a route already in it.\n *\n * Assembling the chain costs one sub-request per level, so a cycle would otherwise run until the\n * runtime's sub-request budget is exhausted and surface as an opaque failure.\n */\nexport class ModalBaseCycleError extends HttpException {\n constructor(url: string) {\n super(500, withI18n('modal.errors.baseCycle', { url }))\n }\n}\n","// How the page beneath a modal is rendered, and by whom.\n//\n// Behind a token so an application can substitute its own dispatcher — one that goes through a\n// response-cache gateway entrypoint, say — without this package knowing anything about caching.\nimport type { Page } from '@inertiajs/core'\nimport { inject, Transient } from 'stratal/di'\nimport { markNestedDispatch, ROUTER_TOKENS, type RouterContext } from 'stratal/router'\n\nimport { ModalBackgroundFetchError } from '../errors/modal-background-fetch.error'\nimport { ModalBaseCycleError } from '../errors/modal-base-cycle.error'\nimport { isModalData, MODAL_DOCUMENT_HEADER, MODAL_MARKER_HEADER, MODAL_PROP, type ModalData } from '../core/wire'\nimport { MODAL_TOKENS } from '../tokens'\n\n/** What this package needs of the app it dispatches into. */\ninterface FetchableApp {\n fetch(request: Request, env: unknown, ctx: unknown): Promise<Response>\n}\n\nexport interface ModalBackgroundDispatcher {\n fetch(request: Request, ctx: RouterContext): Promise<Response>\n}\n\n/**\n * What the marker header carries, minted once and never sent to a client.\n *\n * The answer has to survive every hop between the dispatch and the route — a caching entrypoint\n * re-dispatches the request, and each hop rebuilds it — so it travels in a header, the only thing\n * that does. A header is otherwise a value anyone can send, and this one decides whether a route\n * that refuses clients answers instead; carrying a value from here closes that, because the value\n * rides only on requests this package makes and is never part of a response.\n *\n * Minted on first use rather than at module scope, where a runtime may refuse to generate it.\n */\nlet token: string | undefined\n\nfunction backgroundToken(): string {\n return (token ??= crypto.randomUUID())\n}\n\n/**\n * Whether this request is the background render issued for the page beneath a modal.\n *\n * A route that answers a client with a redirect still has to render when it is the `base` of a\n * modal that client may open — otherwise the redirect is followed back to the modal and the chain\n * reports a cycle.\n *\n * A dispatcher that leaves the isolate answers `false` on the far side, which is the safe\n * direction: the route gates as it would for a client rather than opening for one.\n *\n * @example\n * ```typescript\n * if (isModalBackground(ctx)) return next()\n * ```\n */\nexport function isModalBackground(ctx: RouterContext): boolean {\n return ctx.c.req.header(MODAL_DOCUMENT_HEADER) === backgroundToken()\n}\n\n/**\n * The default: the app itself, in process.\n *\n * The marker header goes on here rather than at the call site, so every dispatcher gets it — a\n * route that renders differently as a background must not depend on which one is installed.\n */\n@Transient()\nexport class HonoBackgroundDispatcher implements ModalBackgroundDispatcher {\n constructor(@inject(ROUTER_TOKENS.HonoApp) private readonly app: FetchableApp) {}\n\n fetch(request: Request, ctx: RouterContext): Promise<Response> {\n // Marked as a nested dispatch as well as with the header, and the two are\n // not the same claim. The header says *what* this render is, to whichever\n // route answers it; the mark says the app dispatched it into itself, which\n // is what keeps the router from handing it to anything that would answer\n // it somewhere else — the header is minted in this isolate and means\n // nothing outside it.\n const marked = markNestedDispatch(new Request(request))\n marked.headers.set(MODAL_DOCUMENT_HEADER, backgroundToken())\n\n return this.app.fetch(marked, ctx.c.env, ctx.c.executionCtx)\n }\n}\n\n/** The page beneath a modal, and the modal levels between the two. */\nexport interface ModalChain {\n page: Page\n /** Outermost first. Empty when the requested level sits directly on a page. */\n levels: ModalData[]\n}\n\n/**\n * Headers a background render needs to answer as the caller would have been answered.\n *\n * The host and the forwarded set matter because middleware reconstructs the canonical request URL\n * from them, and auth derives its cookie name from that URL's protocol — without them a background\n * render is unauthenticated even though the cookie was forwarded.\n */\nconst FORWARDED_HEADERS = [\n 'cookie',\n 'host',\n 'x-forwarded-proto',\n 'x-forwarded-host',\n 'x-forwarded-for',\n 'x-forwarded-port',\n 'x-real-ip',\n 'accept-language',\n 'user-agent',\n] as const\n\n/**\n * Walks `base` to `base` until it reaches a route that is not a modal.\n *\n * Only a document request gets here: an Inertia visit has a page mounted to graft onto, so there is\n * nothing to rebuild.\n */\n@Transient()\nexport class ModalBackground {\n constructor(\n @inject(MODAL_TOKENS.BackgroundDispatcher) private readonly dispatcher: ModalBackgroundDispatcher,\n ) {}\n\n async chainFor(ctx: RouterContext, base: string): Promise<ModalChain> {\n const origin = new URL(ctx.c.req.url).origin\n const levels: ModalData[] = []\n // A chain is bounded by the set of distinct routes in it, so the routes already followed are\n // the whole budget — no counter needed.\n const followed = new Set<string>()\n\n let next = base\n for (;;) {\n const url = new URL(next, origin)\n if (followed.has(url.pathname)) {\n throw new ModalBaseCycleError(url.pathname)\n }\n followed.add(url.pathname)\n\n const response = await this.dispatcher.fetch(this.requestFor(ctx, url), ctx)\n const body = await response.text()\n if (body === '' || response.status >= 300) {\n throw new ModalBackgroundFetchError()\n }\n\n // A 2xx whose body is not a page: a `base` pointing at a route that answers something other\n // than Inertia JSON. That is the same class of answer as the checks above — the chain cannot\n // be built from it — so it reports as one rather than escaping as a bare `SyntaxError`.\n let page: Page\n try {\n page = JSON.parse(body) as Page\n } catch (error) {\n throw new ModalBackgroundFetchError(error)\n }\n\n if (response.headers.get(MODAL_MARKER_HEADER) !== 'true') {\n return { page, levels }\n }\n\n const level = page.props[MODAL_PROP]\n if (!isModalData(level)) {\n // Marked as a modal but carrying something else: a build on the other side of a deploy, or\n // a route writing to that prop name itself. Either way there is no level to place.\n throw new ModalBackgroundFetchError()\n }\n\n levels.unshift(level)\n next = level.base\n }\n }\n\n private requestFor(ctx: RouterContext, url: URL): Request {\n const headers = new Headers({\n // Answer as JSON: the document is rendered here, from the combined page object.\n 'x-inertia': 'true',\n 'accept': 'application/json',\n // `x-inertia-version` is deliberately absent. A version mismatch answers 409 with no body,\n // which this would have nothing to parse, and a sub-request needs no cache-bust check.\n })\n\n for (const name of FORWARDED_HEADERS) {\n const value = ctx.c.req.header(name)\n if (value !== undefined && value !== '') headers.set(name, value)\n }\n\n return new Request(url.toString(), { method: 'GET', headers })\n }\n}\n","/**\n * Where a level lands when it closes.\n *\n * Resolved ONCE, when the level opens, and echoed by the client on every later request for it. A\n * target re-derived per answer drifts: on a refresh the browser's `Referer` is the sheet itself, so\n * the referer branch stops matching and the answer silently falls back to `base` — losing whatever\n * query the list had.\n */\nimport { levelPath, samePath } from './level-path'\n\nexport interface CloseTargetInput {\n /** The request's `Referer`, if any. */\n referer: string | null\n /** The route's declared background. */\n base: string\n /** The URL being requested. */\n requestURL: string\n /** The app's own origin; a referer from anywhere else is not a page we can land on. */\n origin: string\n /**\n * The levels the client has open, from `MODAL_HELD_HEADER`.\n *\n * A sheet the student is leaving is not a place to land on. A visit made from one sends it as the\n * `Referer`, and a level that took it would aim itself back at that sheet; the two then close onto\n * each other without ever reaching the page. The one open level that IS a place to land on is this\n * level's own `base` — the thing it was opened over.\n */\n held: readonly string[]\n}\n\n/**\n * The page this level closes onto.\n *\n * The referer is preferred because it is the page the user is actually looking at, query and all,\n * where `base` is usually written query-free.\n */\nexport function resolveCloseTarget({ referer, base, requestURL, origin, held }: CloseTargetInput): string {\n if (referer === null) return base\n\n let refererURL: URL\n try {\n refererURL = new URL(referer)\n }\n catch {\n // A referer we cannot parse tells us nothing; the declared base is what the route promised.\n return base\n }\n\n if (refererURL.origin !== origin) return base\n\n // Compared as levels, because a level's query is what it is currently showing rather than what\n // it is, and one route has two spellings wherever an app appends a trailing slash.\n const refererPath = levelPath(refererURL.pathname)\n\n // An open referer is the thing this level sits over only when it is the declared `base`; any\n // other open level is a sibling, and landing on one closes the two onto each other. That base is\n // taken from the referer rather than from `base` because the referer carries the query it is\n // showing — the filter a list was narrowed by — where the declaration is written without one.\n const isDeclaredBase = samePath(refererPath, base)\n if (!isDeclaredBase && held.some((url) => samePath(url, refererPath))) return base\n\n // A refresh of the sheet sends the sheet's own url. Landing there would make closing a no-op.\n if (samePath(refererURL.pathname, new URL(requestURL).pathname)) return base\n\n return `${refererURL.pathname}${refererURL.search}`\n}\n","// A level's props are nested under one page prop, so every piece of Inertia's prop metadata has to\n// name them at that depth or it addresses nothing.\nimport { isModalPropPath, MODAL_PROP } from './wire'\n\nconst PREFIX = `${MODAL_PROP}.props.`\n\nconst ARRAY_KEYS = ['mergeProps', 'prependProps', 'deepMergeProps', 'matchPropsOn'] as const\nconst RECORD_OF_ARRAY_KEYS = ['deferredProps'] as const\n\n/** Rewrites every metadata entry on `page` to address `modal.props.*`. */\nexport function anchorPropMetadata(page: Record<string, unknown>): Record<string, unknown> {\n const anchored: Record<string, unknown> = { ...page }\n\n for (const key of ARRAY_KEYS) {\n const value = anchored[key]\n if (Array.isArray(value)) {\n anchored[key] = value.map((entry) => `${PREFIX}${String(entry)}`)\n }\n }\n\n for (const key of RECORD_OF_ARRAY_KEYS) {\n const value = anchored[key]\n if (isRecord(value)) {\n anchored[key] = Object.fromEntries(\n Object.entries(value as Record<string, string[]>).map(([group, names]) => [\n group,\n names.map((name) => `${PREFIX}${name}`),\n ]),\n )\n }\n }\n\n const scrollProps = anchored.scrollProps\n if (isRecord(scrollProps)) {\n anchored.scrollProps = Object.fromEntries(\n Object.entries(scrollProps).map(([name, entry]) => [`${PREFIX}${name}`, entry]),\n )\n }\n\n const onceProps = anchored.onceProps\n if (isRecord(onceProps)) {\n // A once entry names a prop inside itself as well as being keyed by one, and the client reads\n // both as paths — anchoring only the key leaves the entry pointing at the page root.\n anchored.onceProps = Object.fromEntries(\n Object.entries(onceProps as Record<string, { prop: string }>).map(([name, entry]) => [\n `${PREFIX}${name}`,\n { ...entry, prop: `${PREFIX}${entry.prop}` },\n ]),\n )\n }\n\n return anchored\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * What a partial reload asks of the level.\n *\n * Three answers, because a reload can name the level's props, the level itself, or nothing about\n * it at all — and the third is not the same as asking for all of it. A reload for a prop of the\n * page around the sheet leaves the level exactly as the client already holds it, so re-resolving\n * it answers a question nobody asked: its `defer()` props go unresolved and are advertised again,\n * and the client, reading that as work still outstanding, fetches them again.\n */\nexport type LevelAsk =\n /** Every prop. A first render of the level, or a reload naming the level itself. */\n | { readonly kind: 'whole' }\n /** Only these, named as the level keys them. */\n | { readonly kind: 'props'; readonly names: string[] }\n /** Nothing: what the client holds of this level is still current. */\n | { readonly kind: 'unchanged' }\n\nexport function levelAskFor(partialData: string | null): LevelAsk {\n if (partialData === null || partialData.trim() === '') return { kind: 'whole' }\n\n const names = partialData.split(',').map((name) => name.trim())\n const modalNames = names.filter((name) => isModalPropPath(name))\n\n // A partial reload for a PAGE prop is not addressed to the level. Answering it with a fresh\n // level would also replace the page the client is on.\n if (modalNames.length === 0) return { kind: 'unchanged' }\n\n // The level itself rather than anything inside it — what `<ModalLink>` and `useModal().visit()`\n // ask for. Naming it alongside props inside it still asks for the whole level: the wider request\n // is the one that has to be honoured.\n if (modalNames.includes(MODAL_PROP)) return { kind: 'whole' }\n\n return { kind: 'props', names: levelPropNames(modalNames) }\n}\n\n/**\n * The level-relative names among a page-anchored list, dropping any addressed elsewhere.\n *\n * Inertia's prop metadata travels page-anchored, because `modal.props.x` is where the client sees\n * the prop. The level's own props are keyed by the bare name, so anything resolving them has to ask\n * in those terms or it names nothing that exists.\n */\nexport function levelPropNames(names: readonly string[]): string[] {\n return names.filter((name) => name.startsWith(PREFIX)).map((name) => name.slice(PREFIX.length))\n}\n","import type { Page } from '@inertiajs/core'\nimport {\n INERTIA_TOKENS,\n type DocumentRendererService,\n type InertiaPartialRequest,\n type InertiaPropResolution,\n type InertiaService,\n type SeoData,\n type SeoService,\n} from '@stratal/inertia'\nimport { Request as RequestScoped, inject } from 'stratal/di'\nimport type { RouterContext } from 'stratal/router'\n\nimport { resolveCloseTarget } from '../core/close-target'\nimport { anchorPropMetadata, levelAskFor, levelPropNames, type LevelAsk } from '../core/level-props'\nimport {\n decodeHeldLevels,\n MODAL_BENEATH_PROP,\n MODAL_HELD_HEADER,\n MODAL_MARKER_HEADER,\n MODAL_PROP,\n type ModalData,\n} from '../core/wire'\nimport { ModalBackground } from './background'\n\nexport interface ModalRenderOptions {\n /** What sits beneath this level: a page route, or another modal route. */\n base: string\n}\n\n/** The page-object keys `anchorPropMetadata` rewrites, in the shape a page carries them. */\ntype PageMetadata = Pick<\n Page,\n 'mergeProps' | 'prependProps' | 'deepMergeProps' | 'matchPropsOn' | 'scrollProps' | 'deferredProps' | 'onceProps'\n>\n\n@RequestScoped()\nexport class ModalService {\n constructor(\n @inject(INERTIA_TOKENS.DocumentRenderer) private readonly documentRenderer: DocumentRendererService,\n @inject(INERTIA_TOKENS.InertiaService) private readonly inertia: InertiaService,\n @inject(INERTIA_TOKENS.SeoService) private readonly seo: SeoService,\n // Named explicitly so the class stays a VALUE import: a type-only one leaves the DI container\n // with no token for this parameter.\n @inject(ModalBackground) private readonly background: ModalBackground,\n ) {}\n\n async render(\n ctx: RouterContext,\n component: string,\n props: Record<string, unknown>,\n options: ModalRenderOptions,\n ): Promise<Response> {\n const isInertia = ctx.header('x-inertia') === 'true'\n const requestURL = new URL(ctx.c.req.url)\n const referer = ctx.header('referer') ?? null\n\n const close = resolveCloseTarget({\n referer,\n base: options.base,\n requestURL: ctx.c.req.url,\n origin: requestURL.origin,\n held: decodeHeldLevels(ctx.header(MODAL_HELD_HEADER) ?? null),\n })\n\n const ask = this.askedOf(ctx, referer, requestURL)\n // Nothing asked, nothing resolved — and so nothing advertised either. A level whose props the\n // client already holds contributes no metadata, which is what stops its outstanding `defer()`\n // props being announced again on every reload the page around it makes.\n const resolution = ask.kind === 'unchanged'\n ? EMPTY_RESOLUTION\n : await this.inertia.resolveProps(\n props,\n this.levelRequest(this.inertia.partialRequestFor(ctx, component, isInertia), ask),\n )\n\n const modal: ModalData = {\n component,\n props: resolution.resolvedProps,\n url: `${requestURL.pathname}${requestURL.search}`,\n base: options.base,\n close,\n }\n\n const anchored = anchorPropMetadata(metadataOf(resolution)) as PageMetadata\n const seoProp = this.seo.contributed() ? { seo: await this.seo.resolve(ctx) } : {}\n\n // This request's own flash, on both paths. A submission that redirects into a modal route\n // flashes a result for the sheet to show, and it is read once — so dropping it here loses it\n // outright rather than deferring it. `errors` rides in the same bag and is lifted into props,\n // where `preserveState: 'errors'` resolves it.\n const { flash, errors } = this.flashFrom(ctx)\n\n // The whole rule. An Inertia visit has a page mounted to graft onto; a document request does\n // not, and is the only case that has to rebuild what sits beneath.\n if (isInertia) {\n return this.json({\n component,\n // An unchanged level is not sent. Inertia merges a partial response onto the props the\n // client holds, so omitting the level leaves the one it is looking at exactly as it was —\n // which is what `unchanged` claims. Sending it with no props instead states the opposite:\n // that the level now has none. A client that took that literally would render the sheet\n // with nothing in it, and no later response would put the props back.\n props: { ...(ask.kind === 'unchanged' ? {} : { [MODAL_PROP]: modal }), errors, ...seoProp },\n url: modal.url,\n version: null,\n flash,\n rememberedState: {},\n rescuedProps: [],\n ...anchored,\n })\n }\n\n const chain = await this.background.chainFor(ctx, options.base)\n\n // page.url is the modal's, so Inertia's initial visit keeps the address bar on the modal and the\n // SSR and hydration passes agree on it.\n const page: Page = {\n ...chain.page,\n props: {\n ...chain.page.props,\n errors,\n [MODAL_PROP]: modal,\n [MODAL_BENEATH_PROP]: chain.levels,\n },\n url: modal.url,\n flash,\n // The page beneath keeps its own deferred and merge props; the level's are added to them.\n ...mergeMetadata(chain.page, anchored),\n }\n\n // The level is what the URL names, so its own `ctx.seo()` is this page's metadata. Only when it\n // named some: the page beneath carries its own, and resolving unconditionally would answer with\n // the module defaults and overwrite it.\n const seoTags = this.seo.contributed() ? this.applyLevelSeo(page, await this.seo.resolve(ctx)) : []\n\n return this.documentRenderer.render(page, 200, seoTags)\n }\n\n /**\n * The partial request as the level's own props see it.\n *\n * Inertia decides what to resolve by matching the request's names against the keys of the record\n * it is given, and the two are in different namespaces here: the names arrive page-anchored\n * (`modal.props.items`), while the record is the level's own props keyed bare (`items`). Passed\n * through untranslated, a partial reload names nothing that exists — every prop is skipped, a\n * deferred one is never resolved, and the level answers empty. `<Deferred>` reads that as the\n * prop still being missing and asks again, which is a loop that does not end.\n *\n * `isPartial` is taken from what the request names rather than from `request.isPartial`, which\n * is false for every level. Inertia decides that by comparing `X-Inertia-Partial-Component`\n * against the component being rendered, and the client sends the component of the page it holds\n * — the page beneath, since a level is grafted onto it as a prop, not swapped in for it. So the\n * comparison is between a page and a level and can never match. `narrowedTo` has already\n * established that this request is addressed to this level, by the referer, which is the signal\n * that actually means what `isPartial` is being asked here.\n *\n * `null` resolves the level whole, covering both of its readings: a request addressed elsewhere,\n * and one asking for the level itself.\n */\n private levelRequest(request: InertiaPartialRequest, ask: LevelAsk): InertiaPartialRequest {\n return {\n ...request,\n isPartial: ask.kind === 'props',\n requested: ask.kind === 'props' ? ask.names : [],\n except: levelPropNames(request.except),\n reset: levelPropNames(request.reset),\n }\n }\n\n /**\n * What this request asks of the level.\n *\n * Answering with anything but the whole level is only safe when the client is already looking at\n * it, because a narrowed answer is merged over the props it holds and an `unchanged` one carries\n * none at all. `Referer` is what says so: an XHR from inside the sheet reports the sheet's own\n * url, while a partial re-issued into a modal route by a redirect reports the page the request\n * started from. Its partial headers survive that redirect unchanged, so without this check such\n * a response would leave the level with holes nothing will ever fill.\n */\n private askedOf(ctx: RouterContext, referer: string | null, requestURL: URL): LevelAsk {\n if (referer === null) return { kind: 'whole' }\n\n let refererURL: URL\n try {\n refererURL = new URL(referer)\n }\n catch {\n return { kind: 'whole' }\n }\n\n if (refererURL.origin !== requestURL.origin || refererURL.pathname !== requestURL.pathname) {\n return { kind: 'whole' }\n }\n\n return levelAskFor(ctx.header('x-inertia-partial-data') ?? null)\n }\n\n /** Writes a level's resolved SEO onto the page and returns its head tags. */\n private applyLevelSeo(page: Page, resolved: SeoData): string[] {\n page.props.seo = resolved\n return this.seo.tagsFor(resolved)\n }\n\n /**\n * The validation errors this response carries.\n *\n * Load-bearing on this path rather than incidental: a failed submission redirects back into the\n * modal route, and `preserveState: 'errors'` resolves against the response — an empty record here\n * closes the sheet the user was filling in.\n */\n private flashFrom(\n ctx: RouterContext,\n ): { flash: Record<string, unknown>; errors: Page['props']['errors'] } {\n const raw = (ctx.c.get('inertiaFlash') as Record<string, unknown> | undefined) ?? {}\n const { errors: rawErrors, ...flash } = raw\n const errors = (rawErrors !== undefined && typeof rawErrors === 'object'\n && !Array.isArray(rawErrors) && rawErrors !== null)\n ? rawErrors as Page['props']['errors']\n : {}\n\n return { flash, errors }\n }\n\n private json(page: Page): Response {\n return new Response(JSON.stringify(page), {\n status: 200,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Inertia': 'true',\n [MODAL_MARKER_HEADER]: 'true',\n 'Vary': 'X-Inertia',\n },\n })\n }\n}\n\n/** What a level contributes when the request asked nothing of it. */\nconst EMPTY_RESOLUTION: InertiaPropResolution = {\n resolvedProps: {},\n mergeProps: [],\n prependProps: [],\n deepMergeProps: [],\n matchPropsOn: [],\n scrollProps: {},\n deferredProps: {},\n onceProps: {},\n}\n\n/** A resolution's metadata in the shape a page object carries it. */\nfunction metadataOf(resolution: InertiaPropResolution): Record<string, unknown> {\n return {\n ...(resolution.mergeProps.length > 0 ? { mergeProps: resolution.mergeProps } : {}),\n ...(resolution.prependProps.length > 0 ? { prependProps: resolution.prependProps } : {}),\n ...(resolution.deepMergeProps.length > 0 ? { deepMergeProps: resolution.deepMergeProps } : {}),\n ...(resolution.matchPropsOn.length > 0 ? { matchPropsOn: resolution.matchPropsOn } : {}),\n ...(Object.keys(resolution.scrollProps).length > 0 ? { scrollProps: resolution.scrollProps } : {}),\n ...(Object.keys(resolution.deferredProps).length > 0 ? { deferredProps: resolution.deferredProps } : {}),\n ...(Object.keys(resolution.onceProps).length > 0 ? { onceProps: resolution.onceProps } : {}),\n }\n}\n\n/** The page's own metadata with the level's anchored metadata added to it. */\nfunction mergeMetadata(base: Page, anchored: PageMetadata): PageMetadata {\n const deferredProps: NonNullable<Page['deferredProps']> = { ...base.deferredProps }\n for (const [group, names] of Object.entries(anchored.deferredProps ?? {})) {\n deferredProps[group] = [...(deferredProps[group] ?? []), ...names]\n }\n\n const mergeProps = [...(base.mergeProps ?? []), ...(anchored.mergeProps ?? [])]\n const prependProps = [...(base.prependProps ?? []), ...(anchored.prependProps ?? [])]\n const deepMergeProps = [...(base.deepMergeProps ?? []), ...(anchored.deepMergeProps ?? [])]\n const matchPropsOn = [...(base.matchPropsOn ?? []), ...(anchored.matchPropsOn ?? [])]\n const scrollProps = { ...base.scrollProps, ...anchored.scrollProps }\n const onceProps = { ...base.onceProps, ...anchored.onceProps }\n\n return {\n ...(mergeProps.length > 0 ? { mergeProps } : {}),\n ...(prependProps.length > 0 ? { prependProps } : {}),\n ...(deepMergeProps.length > 0 ? { deepMergeProps } : {}),\n ...(matchPropsOn.length > 0 ? { matchPropsOn } : {}),\n ...(Object.keys(scrollProps).length > 0 ? { scrollProps } : {}),\n ...(Object.keys(deferredProps).length > 0 ? { deferredProps } : {}),\n ...(Object.keys(onceProps).length > 0 ? { onceProps } : {}),\n }\n}\n","import { I18nModule } from 'stratal/i18n'\nimport type { OnInitialize } from 'stratal/module'\nimport { Module } from 'stratal/module'\nimport { augmentRouterContextWithModal } from './augment/router-context'\nimport { modalMessages } from './i18n'\nimport { HonoBackgroundDispatcher } from './server/background'\nimport { ModalService } from './server/modal.service'\nimport { MODAL_TOKENS } from './tokens'\n\n@Module({\n imports: [\n // The messages this package's errors are raised from. Unregistered, the\n // file is unreachable from every entry point, the bundler drops it, and each\n // error reaches the browser as the raw key it failed to translate. The\n // import also carries the translator into apps that never install i18n\n // themselves, which is what makes the English text the default rather than\n // something a consumer has to opt into.\n I18nModule.registerMessages({ en: { modal: modalMessages.en } }),\n ],\n providers: [\n { provide: MODAL_TOKENS.ModalService, useClass: ModalService },\n { provide: MODAL_TOKENS.BackgroundDispatcher, useClass: HonoBackgroundDispatcher },\n ],\n})\nexport class ModalModule implements OnInitialize {\n onInitialize(): void {\n augmentRouterContextWithModal((ctx) => {\n return ctx.getContainer().resolve<ModalService>(MODAL_TOKENS.ModalService)\n })\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/core/level-path.ts
|
|
2
|
+
/**
|
|
3
|
+
* What makes a level itself, as opposed to where it currently points.
|
|
4
|
+
*
|
|
5
|
+
* The contract both bundles answer "is this the same level?" by, framework-free like the rest of
|
|
6
|
+
* `core`, so the server deciding what a referer names and the client deciding what a response
|
|
7
|
+
* addresses cannot drift apart. Two spellings of one route reaching different answers is a
|
|
8
|
+
* navigation loop that reproduces on one route and not another.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* A url reduced to the level it names.
|
|
12
|
+
*
|
|
13
|
+
* A level's `url` carries its query and may carry a fragment, because that is its address — what a
|
|
14
|
+
* refresh re-reads and what closing the level above lands on. Its identity is the path alone: a
|
|
15
|
+
* refined query is the same sheet showing something else, and a fragment is a position within it.
|
|
16
|
+
* `/parent/42/edit` and `/parent/42/edit/` are one route, so an app that appends a trailing slash
|
|
17
|
+
* does not spell a second level by writing the same one twice. The root keeps its slash, being the
|
|
18
|
+
* whole path rather than a trailing one.
|
|
19
|
+
*
|
|
20
|
+
* A held url may be relative, so it cannot go through `new URL`.
|
|
21
|
+
*/
|
|
22
|
+
function levelPath(url) {
|
|
23
|
+
const path = url.split(/[?#]/)[0] ?? "";
|
|
24
|
+
return path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
25
|
+
}
|
|
26
|
+
/** Whether two urls name the same level, whatever each is currently showing. */
|
|
27
|
+
function samePath(a, b) {
|
|
28
|
+
return levelPath(a) === levelPath(b);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { samePath as n, levelPath as t };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=level-path-DCJD-aS3.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"level-path-DCJD-aS3.mjs","names":[],"sources":["../src/core/level-path.ts"],"sourcesContent":["/**\n * What makes a level itself, as opposed to where it currently points.\n *\n * The contract both bundles answer \"is this the same level?\" by, framework-free like the rest of\n * `core`, so the server deciding what a referer names and the client deciding what a response\n * addresses cannot drift apart. Two spellings of one route reaching different answers is a\n * navigation loop that reproduces on one route and not another.\n */\n\n/**\n * A url reduced to the level it names.\n *\n * A level's `url` carries its query and may carry a fragment, because that is its address — what a\n * refresh re-reads and what closing the level above lands on. Its identity is the path alone: a\n * refined query is the same sheet showing something else, and a fragment is a position within it.\n * `/parent/42/edit` and `/parent/42/edit/` are one route, so an app that appends a trailing slash\n * does not spell a second level by writing the same one twice. The root keeps its slash, being the\n * whole path rather than a trailing one.\n *\n * A held url may be relative, so it cannot go through `new URL`.\n */\nexport function levelPath(url: string): string {\n const path = url.split(/[?#]/)[0] ?? ''\n return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path\n}\n\n/** Whether two urls name the same level, whatever each is currently showing. */\nexport function samePath(a: string, b: string): boolean {\n return levelPath(a) === levelPath(b)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,UAAU,KAAqB;CAC7C,MAAM,OAAO,IAAI,MAAM,MAAM,CAAC,CAAC,MAAM;CACrC,OAAO,KAAK,SAAS,KAAK,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;AACrE;;AAGA,SAAgB,SAAS,GAAW,GAAoB;CACtD,OAAO,UAAU,CAAC,MAAM,UAAU,CAAC;AACrC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { a as ModalData } from "./wire-CbwmWkPr.mjs";
|
|
2
|
+
//#region src/page-props.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Types the modal page props for every consumer of `@inertiajs/core`'s
|
|
5
|
+
* `PageProps` — `usePage()`, `Page['props']`, and anything built on top of
|
|
6
|
+
* them — instead of each call site re-declaring the same shape locally.
|
|
7
|
+
*
|
|
8
|
+
* `PageProps` carries `[key: string]: unknown`, so this merges cleanly: both keys are optional and
|
|
9
|
+
* assignable to `unknown`, and they narrow only what callers actually read.
|
|
10
|
+
*/
|
|
11
|
+
declare module '@inertiajs/core' {
|
|
12
|
+
interface PageProps {
|
|
13
|
+
/**
|
|
14
|
+
* `null` says the level is gone, where absence says nothing: a response naming props is merged
|
|
15
|
+
* over the ones the page holds, and an omitted key is one the page keeps.
|
|
16
|
+
*/
|
|
17
|
+
modal?: ModalData | null;
|
|
18
|
+
/** The chain a document response rebuilt, outermost first. `null` as for `modal`. */
|
|
19
|
+
modalBeneath?: ModalData[] | null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//# sourceMappingURL=page-props-BCEWEw3V.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-props-BCEWEw3V.d.mts","names":[],"sources":["../src/page-props.ts"],"mappings":";;;;;;;;;;;YAWY;;;;;IAKR,QAAQ;;IAER,eAAe"}
|
package/dist/react.d.mts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
+
import { a as ModalData, i as MODAL_PROP } from "./wire-CbwmWkPr.mjs";
|
|
2
|
+
import "./page-props-BCEWEw3V.mjs";
|
|
3
|
+
import { Deferred as Deferred$1, InfiniteScroll as InfiniteScroll$1, Link } from "@inertiajs/react";
|
|
4
|
+
import { ComponentProps } from "react";
|
|
5
|
+
import { Page, ReloadOptions, RequestPayload, UrlMethodPair, VisitHelperOptions, VisitOptions } from "@inertiajs/core";
|
|
1
6
|
//#region src/react/modal.d.ts
|
|
2
7
|
/**
|
|
3
|
-
* Headless modal
|
|
8
|
+
* Headless modal host. Place it once in your layout.
|
|
4
9
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* renders it as an overlay. The background page is what Inertia renders normally.
|
|
10
|
+
* Renders every open level, so a modal opened from inside another appears above it with the one
|
|
11
|
+
* below still mounted.
|
|
8
12
|
*
|
|
9
13
|
* @example
|
|
10
14
|
* ```tsx
|
|
11
|
-
* // dashboard-layout.tsx
|
|
12
|
-
* import { Modal } from '@stratal/inertia-modal/react'
|
|
13
|
-
*
|
|
14
15
|
* export function DashboardLayout({ children }) {
|
|
15
16
|
* return (
|
|
16
17
|
* <>
|
|
17
|
-
* <Sidebar />
|
|
18
18
|
* <main>{children}</main>
|
|
19
19
|
* <Modal />
|
|
20
20
|
* </>
|
|
@@ -22,24 +22,147 @@
|
|
|
22
22
|
* }
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
declare function Modal(): import("react").JSX.Element | null;
|
|
25
|
+
export declare function Modal(): import("react").JSX.Element | null;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/react/modal-link.d.ts
|
|
28
|
+
type ModalLinkProps = ComponentProps<typeof Link>;
|
|
29
|
+
/**
|
|
30
|
+
* Opens a modal route as a sheet over the current page.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <ModalLink href="/parent/1/edit">Edit</ModalLink>
|
|
35
|
+
* <ModalLink href="/parent/1/edit" prefetch>Edit</ModalLink>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function ModalLink({ children, ...rest }: ModalLinkProps): import("react").JSX.Element;
|
|
26
39
|
//#endregion
|
|
27
40
|
//#region src/react/use-modal.d.ts
|
|
41
|
+
/** Function members are declared as properties, not methods: they are closures, and
|
|
42
|
+
* destructuring them is how this hook is used. */
|
|
28
43
|
interface UseModalReturn {
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
|
|
44
|
+
/** This level, or `undefined` outside a modal. */
|
|
45
|
+
modal: ModalData | undefined;
|
|
46
|
+
/** How deep this level sits. The outermost is 0. */
|
|
47
|
+
depth: number;
|
|
48
|
+
/** Whether this is the level the reader is looking at. */
|
|
49
|
+
isTop: boolean;
|
|
50
|
+
/** Close this level and land where it was opened from. */
|
|
51
|
+
close: <TPayload extends RequestPayload = RequestPayload>(options?: VisitOptions<TPayload>) => void;
|
|
52
|
+
/** Close every open level and land where the outermost one was opened from. */
|
|
53
|
+
closeAll: <TPayload extends RequestPayload = RequestPayload>(options?: VisitOptions<TPayload>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Re-read this level under a refined query — a filter, a sort, a code the server prices.
|
|
56
|
+
*
|
|
57
|
+
* Takes the same options as `router.get`, so a caller can observe the visit it started rather
|
|
58
|
+
* than the next one to finish: without `onFinish` here, timing this means subscribing to the
|
|
59
|
+
* router's own event, which fires for every visit in flight — a poll included.
|
|
60
|
+
*/
|
|
61
|
+
refresh: <TPayload extends RequestPayload = RequestPayload>(query?: TPayload, options?: VisitHelperOptions<TPayload>) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Fetch some of this level's props again, leaving the rest of the page alone.
|
|
64
|
+
*
|
|
65
|
+
* Takes `router.reload`'s own options. `only`, `except` and `reset` name props, so they are given
|
|
66
|
+
* in the level's terms — `'items'`, not `'modal.props.items'` — and anchored here; everything
|
|
67
|
+
* else is passed through untouched.
|
|
68
|
+
*/
|
|
69
|
+
reload: <TPayload extends RequestPayload = RequestPayload>(options?: ReloadOptions<TPayload>) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Open a modal route from code, the way `<ModalLink>` opens one from a click.
|
|
72
|
+
*
|
|
73
|
+
* Carries the same visit options, and takes the same arguments as `router.visit`, so anything
|
|
74
|
+
* they accept — `replace`, a method, callbacks — is passed straight through.
|
|
75
|
+
*/
|
|
76
|
+
visit: <TPayload extends RequestPayload = RequestPayload>(href: string | URL | UrlMethodPair, options?: VisitOptions<TPayload>) => void;
|
|
35
77
|
}
|
|
36
|
-
declare function useModal(): UseModalReturn;
|
|
78
|
+
export declare function useModal(): UseModalReturn;
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/react/deferred.d.ts
|
|
81
|
+
/**
|
|
82
|
+
* `@inertiajs/react`'s `<Deferred>`, with `data` resolved against the modal it is rendered in.
|
|
83
|
+
*
|
|
84
|
+
* The component addresses its prop by name at the page root, and a level's props are nested under
|
|
85
|
+
* one page prop. Import this one instead of the Inertia component and the same JSX works in a sheet
|
|
86
|
+
* and on a page — outside a modal the name is already the path.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```tsx
|
|
90
|
+
* import { Deferred } from '@stratal/inertia-modal/react'
|
|
91
|
+
*
|
|
92
|
+
* <Deferred data="entries" fallback={<Skeleton />}>
|
|
93
|
+
* <Entries entries={entries} />
|
|
94
|
+
* </Deferred>
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare const Deferred: typeof Deferred$1;
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/react/infinite-scroll.d.ts
|
|
100
|
+
/**
|
|
101
|
+
* `@inertiajs/react`'s `<InfiniteScroll>`, with `data` resolved against the modal it is rendered in.
|
|
102
|
+
*
|
|
103
|
+
* The component addresses its prop by name at the page root, and a level's props are nested under
|
|
104
|
+
* one page prop. Import this one instead of the Inertia component and the same JSX works in a sheet
|
|
105
|
+
* and on a page — outside a modal the name is already the path.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```tsx
|
|
109
|
+
* import { InfiniteScroll } from '@stratal/inertia-modal/react'
|
|
110
|
+
*
|
|
111
|
+
* <InfiniteScroll data="items">
|
|
112
|
+
* {items.data.map((item) => <Row key={item.id} item={item} />)}
|
|
113
|
+
* </InfiniteScroll>
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare const InfiniteScroll: typeof InfiniteScroll$1;
|
|
37
117
|
//#endregion
|
|
38
118
|
//#region src/react/resolver.d.ts
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Wraps the `resolve` callback `createInertiaApp` is given, so a page's modal levels are resolved
|
|
121
|
+
* before the tree renders.
|
|
122
|
+
*
|
|
123
|
+
* Resolution is a real `import()`, so it cannot happen during render. Doing it here — the one point
|
|
124
|
+
* Inertia already awaits before it swaps the page — is what puts a level in server HTML and keeps a
|
|
125
|
+
* step between levels from flashing an empty sheet.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```tsx
|
|
129
|
+
* createInertiaApp({
|
|
130
|
+
* resolve: withModals((name) => pages[`./pages/${name}.tsx`]()),
|
|
131
|
+
* setup: ({ el, App, props }) => hydrateRoot(el, <App {...props} />),
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export declare function withModals<TComponent>(resolve: (name: string) => TComponent | Promise<TComponent>): (name: string, page?: Page) => Promise<TComponent>;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/react/reset.d.ts
|
|
138
|
+
/**
|
|
139
|
+
* Discard everything this package holds outside the React tree.
|
|
140
|
+
*
|
|
141
|
+
* Three stores survive unmounting, because each exists precisely to outlive a render: the open
|
|
142
|
+
* stack, the page a level grafts onto, and the components resolved so far. In a browser that is
|
|
143
|
+
* what they are for — one document, one visitor, state that must not reset when a sheet closes.
|
|
144
|
+
* Under a test runner the same module is reused across files, so one test's open sheet is the next
|
|
145
|
+
* test's starting state: a sheet nothing opened, or a component a test meant to leave unresolved
|
|
146
|
+
* answering instantly from an earlier test's resolution.
|
|
147
|
+
*
|
|
148
|
+
* Call it between tests. It is the only supported way to empty them; the individual stores are
|
|
149
|
+
* internal so that resetting cannot drift out of step with what the package holds.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* import { resetModalState } from '@stratal/inertia-modal/react'
|
|
154
|
+
*
|
|
155
|
+
* beforeEach(() => resetModalState())
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export declare function resetModalState(): void;
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/react/modal-context.d.ts
|
|
161
|
+
interface ModalLevel {
|
|
162
|
+
modal: ModalData;
|
|
163
|
+
depth: number;
|
|
164
|
+
isTop: boolean;
|
|
165
|
+
}
|
|
43
166
|
//#endregion
|
|
44
|
-
export {
|
|
167
|
+
export { MODAL_PROP, type ModalData, type ModalLevel };
|
|
45
168
|
//# sourceMappingURL=react.d.mts.map
|
package/dist/react.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/modal.tsx","../src/react/use-modal.ts","../src/react/resolver.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/modal.tsx","../src/react/modal-link.tsx","../src/react/use-modal.ts","../src/react/deferred.tsx","../src/react/infinite-scroll.tsx","../src/react/resolver.ts","../src/react/reset.ts","../src/react/modal-context.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;wBAmDgB,yBAAK,IAAA;;;KCzCT,iBAAiB,sBAAsB;;;;;;;;;;wBAWnC,YAAY,aAAa,QAAQ,iCAAc,IAAA;;;;;UCNrD;;EAER,OAAO;;EAEP;;EAEA;;EAEA,QAAQ,iBAAiB,iBAAiB,gBACxC,UAAU,aAAa;;EAGzB,WAAW,iBAAiB,iBAAiB,gBAC3C,UAAU,aAAa;;;;;;;;EASzB,UAAU,iBAAiB,iBAAiB,gBAC1C,QAAQ,UACR,UAAU,mBAAmB;;;;;;;;EAS/B,SAAS,iBAAiB,iBAAiB,gBACzC,UAAU,cAAc;;;;;;;EAQ1B,QAAQ,iBAAiB,iBAAiB,gBACxC,eAAe,MAAM,eACrB,UAAU,aAAa;;wBAgBX,YAAY;;;;;;;;;;;;;;;;;;;qBCxCf,iBAAiB;;;;;;;;;;;;;;;;;;;qBCJjB,uBAAuB;;;;;;;;;;;;;;;;;;;wBCHpB,WAAW,YACzB,UAAU,iBAAiB,aAAa,QAAQ,eAC9C,cAAc,OAAO,SAAS,QAAQ;;;;;;;;;;;;;;;;;;;;;;;wBCN1B;;;UCrBC;EACf,OAAO;EACP;EACA"}
|