@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/react.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.mjs","names":[],"sources":["../src/react/resolver.ts","../src/react/modal.tsx","../src/react/use-modal.ts"],"sourcesContent":["// Module-level resolver store — persists across Inertia navigations.\n// Set once in app.tsx before createInertiaApp.\nlet resolveCallback: ((name: string) => unknown) | undefined\n\nexport const resolver = {\n set(cb: (name: string) => unknown): void {\n resolveCallback = cb\n },\n resolve(name: string): unknown {\n if (!resolveCallback) {\n throw new Error(\n '[@stratal/inertia-modal] Resolver not registered. '\n + 'Call resolver.set() before createInertiaApp().',\n )\n }\n return resolveCallback(name)\n },\n}\n","import type { PageProps } from '@inertiajs/core'\nimport { router, usePage } from '@inertiajs/react'\nimport { type ComponentType, useEffect, useState } from 'react'\nimport type { ModalData } from '../services/modal.service'\nimport { resolver } from './resolver'\n\ninterface ModalPageProps extends PageProps {\n modal?: ModalData\n}\n\n/**\n * Headless modal component. Place this anywhere in your layout.\n *\n * When the current Inertia page has `props.modal` (set by `ctx.inertiaModal()`\n * on the server), this component dynamically loads the modal page component and\n * renders it as an overlay. The background page is what Inertia renders normally.\n *\n * @example\n * ```tsx\n * // dashboard-layout.tsx\n * import { Modal } from '@stratal/inertia-modal/react'\n *\n * export function DashboardLayout({ children }) {\n * return (\n * <>\n * <Sidebar />\n * <main>{children}</main>\n * <Modal />\n * </>\n * )\n * }\n * ```\n */\nexport function Modal() {\n const page = usePage<ModalPageProps>()\n const modal = page.props.modal\n\n const [Component, setComponent] = useState<ComponentType<Record<string, unknown>> | null>(null)\n\n useEffect(() => {\n if (!modal?.component) {\n setComponent(null)\n return\n }\n\n Promise.resolve(resolver.resolve(modal.component))\n .then((mod) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const component = (mod as any)?.default ?? mod\n setComponent(() => component as ComponentType<Record<string, unknown>>)\n })\n .catch(() => setComponent(null))\n }, [modal?.component])\n\n // Inject x-inertia-modal-key into every Inertia request while the modal is\n // open. This ensures partial reloads (e.g. country → states cascade) reuse the\n // same key, preventing React from unmounting and remounting the modal component\n // (which would wipe all uncontrolled form state).\n useEffect(() => {\n if (!modal?.key) return\n\n return router.on('before', (event) => {\n event.detail.visit.headers['x-inertia-modal-key'] = modal.key\n })\n }, [modal?.key])\n\n if (!Component || !modal) {\n return null\n }\n\n return <Component key={modal.component} {...modal.props} />\n}\n","import { router, usePage } from '@inertiajs/react';\nimport { useCallback } from 'react';\nimport { type ModalData } from '../services/modal.service';\n\ndeclare const window: { history: { back(): void } }\n\ninterface UseModalReturn {\n /** Whether a modal is currently active on this page. */\n show: boolean\n /** Navigate back to the page that opened the modal (or the base URL on direct visits). */\n redirect(): void\n /** The modal component's props, if a modal is active. */\n props: Record<string, unknown> | undefined\n}\n\nexport function useModal(): UseModalReturn {\n const page = usePage()\n const modal = page.props.modal as ModalData\n\n const redirect = useCallback(() => {\n if (!modal) return\n if (modal.nativeBack) {\n window.history.back()\n } else {\n router.visit(modal.redirectURL ?? modal.baseURL, {\n preserveScroll: true,\n preserveState: true,\n })\n }\n }, [modal])\n\n return {\n show: !!modal,\n redirect,\n props: modal?.props,\n }\n}\n"],"mappings":";;;;AAEA,IAAI;AAEJ,MAAa,WAAW;CACtB,IAAI,IAAqC;EACvC,kBAAkB;CACpB;CACA,QAAQ,MAAuB;EAC7B,IAAI,CAAC,iBACH,MAAM,IAAI,MACR,kGAEF;EAEF,OAAO,gBAAgB,IAAI;CAC7B;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;ACgBA,SAAgB,QAAQ;CAEtB,MAAM,QADO,QACI,EAAE,MAAM;CAEzB,MAAM,CAAC,WAAW,gBAAgB,SAAwD,IAAI;CAE9F,gBAAgB;EACd,IAAI,CAAC,OAAO,WAAW;GACrB,aAAa,IAAI;GACjB;EACF;EAEA,QAAQ,QAAQ,SAAS,QAAQ,MAAM,SAAS,CAAC,EAC9C,MAAM,QAAQ;GAEb,MAAM,YAAa,KAAa,WAAW;GAC3C,mBAAmB,SAAmD;EACxE,CAAC,EACA,YAAY,aAAa,IAAI,CAAC;CACnC,GAAG,CAAC,OAAO,SAAS,CAAC;CAMrB,gBAAgB;EACd,IAAI,CAAC,OAAO,KAAK;EAEjB,OAAO,OAAO,GAAG,WAAW,UAAU;GACpC,MAAM,OAAO,MAAM,QAAQ,yBAAyB,MAAM;EAC5D,CAAC;CACH,GAAG,CAAC,OAAO,GAAG,CAAC;CAEf,IAAI,CAAC,aAAa,CAAC,OACjB,OAAO;CAGT,OAAO,oBAAC,WAAD,EAAiC,GAAI,MAAM,MAAQ,GAAnC,MAAM,SAA6B;AAC5D;;;ACxDA,SAAgB,WAA2B;CAEzC,MAAM,QADO,QACI,EAAE,MAAM;CAEzB,MAAM,WAAW,kBAAkB;EACjC,IAAI,CAAC,OAAO;EACZ,IAAI,MAAM,YACR,OAAO,QAAQ,KAAK;OAEpB,OAAO,MAAM,MAAM,eAAe,MAAM,SAAS;GAC/C,gBAAgB;GAChB,eAAe;EACjB,CAAC;CAEL,GAAG,CAAC,KAAK,CAAC;CAEV,OAAO;EACL,MAAM,CAAC,CAAC;EACR;EACA,OAAO,OAAO;CAChB;AACF"}
|
|
1
|
+
{"version":3,"file":"react.mjs","names":["installed","InertiaDeferred","InertiaInfiniteScroll"],"sources":["../src/react/component-registry.ts","../src/react/scroll-teardown.ts","../src/react/stack.ts","../src/react/stack-store.ts","../src/react/graft.ts","../src/react/modal-context.ts","../src/react/held-header.ts","../src/react/resolver.ts","../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/reset.ts"],"sourcesContent":["import type { ComponentType } from 'react'\n\nexport type ModalComponent = ComponentType<Record<string, unknown>>\n\n/**\n * The modal components resolved so far, by page-component name.\n *\n * Module-level, and safe on a server: it maps build-time names to build-time components, so nothing\n * request-scoped enters it and a render only ever reads the names its own payload carries.\n */\nlet components: Record<string, ModalComponent> = {}\n\nexport function rememberModalComponents(resolved: Record<string, ModalComponent>): void {\n Object.assign(components, resolved)\n}\n\nexport function readModalComponents(): Record<string, ModalComponent> {\n return components\n}\n\nexport function clearModalComponents(): void {\n components = {}\n}\n","// Ending a level's scroll subscription at the moment it is dismissed.\n//\n// `<InfiniteScroll>` subscribes to Inertia's `success` event and, when it fires, reads its own\n// scroll prop off the page. Inside a level that prop is `modal.props.<name>`, and it exists only\n// while the level does — so the response that dismisses the level answers with a page that does not\n// carry it, and the subscription throws looking for it.\n//\n// Inertia's own guard cannot prevent that here. It compares the page component it mounted against\n// the one that just arrived, and a level's grafted page reports the component of the page BENEATH\n// it — which is exactly the page a dismissal lands on, so the two match and the read goes ahead.\n//\n// Unmounting cannot prevent it either: the page is replaced, `success` fires, and only then does\n// React render. The subscription has to be gone before the visit resolves, which is why this is\n// driven from the dismissal rather than from a component lifecycle.\n\ntype Teardown = () => void\n\nconst byLevel = new Map<string, Set<Teardown>>()\n\n/**\n * Register `teardown` to run when the level at `url` is dismissed.\n *\n * Returns the unsubscribe, for the ordinary unmount path.\n */\nexport function onLevelDismissed(url: string, teardown: Teardown): Teardown {\n const existing = byLevel.get(url) ?? new Set<Teardown>()\n existing.add(teardown)\n byLevel.set(url, existing)\n\n return () => {\n const current = byLevel.get(url)\n if (current === undefined) return\n current.delete(teardown)\n if (current.size === 0) byLevel.delete(url)\n }\n}\n\n/**\n * Run every teardown registered for these levels, and forget them.\n *\n * Scoped to the levels actually being dismissed: closing a level above another must not end the\n * subscriptions of the one it was covering, which stays open and keeps the rows it had loaded.\n */\nexport function dismissLevels(urls: readonly string[]): void {\n for (const url of urls) {\n const teardowns = byLevel.get(url)\n if (teardowns === undefined) continue\n\n byLevel.delete(url)\n for (const teardown of teardowns) teardown()\n }\n}\n","// The client holds the stack, because it is the only party that knows it.\n//\n// The server renders one level and names what sits beneath it. Where that level belongs is decided\n// here, by matching its `base` against what is already open — which is why no stack, keys or depth\n// header has to travel.\nimport { samePath } from '../core/level-path'\nimport type { ModalData } from '../core/wire'\n\n/**\n * The stack after a response.\n *\n * `beneath` describes the chain a document response rebuilt, where there is nothing open to match\n * against and the whole chain arrives at once. It is only read while that holds: it travels in\n * props, which a partial response merges into rather than replaces, so it is still there long after\n * the response that meant it.\n */\nexport function applyStack(\n current: ModalData[],\n incoming: ModalData | null,\n beneath: ModalData[] | null,\n): ModalData[] {\n if (incoming === null) return []\n\n // Only when nothing is open, which is the case it describes: a document response arrives with no\n // stack to match against and carries the whole chain. It is read from props, and Inertia merges a\n // partial response onto the props the page already holds — so it outlives the response that sent\n // it, and rebuilding the stack from it on a later one would seat that response's level on a chain\n // it never described, discarding what the open level holds.\n if (current.length === 0 && beneath !== null) return [...beneath, incoming]\n\n const held = current.find((level) => samePath(level.url, incoming.url))\n if (held !== undefined) {\n // The same level answering again — a refined query, a scroll fetch, a targeted reload.\n // Replacing rather than pushing is what keeps a filter from opening a second copy of the sheet,\n // and merging rather than overwriting is what keeps a narrowed answer from taking the rest of\n // the sheet's props away.\n //\n // `close` is fixed when the level opens, and this is where that holds. Closing the level above\n // lands here by visiting this level's url, and the server derives that response's `close` from\n // the Referer — which is the level that just closed. Taking it would aim this level back at the\n // one it came from, and the two would close onto each other without ever reaching the page.\n const merged: ModalData = {\n ...incoming,\n close: held.close,\n props: { ...held.props, ...incoming.props },\n }\n return [...current.slice(0, current.indexOf(held)), merged]\n }\n\n const parent = current.findIndex((level) => samePath(level.url, incoming.base))\n if (parent >= 0) return [...current.slice(0, parent + 1), incoming]\n\n // Its base is not open, so this is a stack of its own.\n return [incoming]\n}\n","import type { ModalData } from '../core/wire'\n\n/**\n * The open stack, held outside the React tree.\n *\n * CLIENT ONLY. A Workers isolate serves many requests, so a module-level value\n * read during SSR would leak one user's open sheets into another user's HTML.\n * `<Modal />` seeds from the payload on the server and consults this only in a\n * browser.\n */\nlet held: ModalData[] = []\n\nexport function readHeldStack(): ModalData[] {\n return held\n}\n\nexport function holdStack(stack: ModalData[]): void {\n held = stack\n}\n\nexport function clearHeldStack(): void {\n held = []\n}\n","// Applying a modal response onto the page the client already has mounted.\n//\n// The server sends one level and nothing else. Grafting is what makes that enough: the response\n// takes the mounted page's component and props, so Inertia treats it as the same page and merges\n// rather than replaces — which is why a modal reached by redirect still lands on the page behind it.\nimport { interceptors } from '@inertiajs/core'\nimport type { HttpResponse, InternalActiveVisit, Page } from '@inertiajs/core'\n\nimport { flushSync } from 'react-dom'\n\nimport { samePath } from '../core/level-path'\nimport { isModalData, MODAL_BENEATH_PROP, MODAL_MARKER_HEADER, MODAL_PROP } from '../core/wire'\nimport { dismissLevels } from './scroll-teardown'\nimport { applyStack } from './stack'\nimport { readHeldStack } from './stack-store'\n\n// This package targets Workers and deliberately omits the DOM lib, so `document` is not a known\n// global here. The guard only needs to know whether it exists, which is exactly what this declares.\ndeclare const document: object | undefined\n\n/** The mounted page, as `<Modal />` last saw it. Client-only; the graft never runs on a server. */\nlet mounted: Page | null = null\n\nexport function holdMountedPage(page: Page): void {\n mounted = page\n}\n\nexport function clearMountedPage(): void {\n mounted = null\n}\n\n/**\n * Rewrites `incoming` in place so it reads as the mounted page carrying a modal.\n *\n * In place because Inertia captures the parsed page object before it runs its response handlers and\n * merges props against that same reference — a replacement object would be dropped.\n *\n * Only the top level of the mounted props is copied. A deep clone would walk the whole background\n * payload on every sheet open, which on a page with many resolved props is real main-thread time.\n */\nexport function graftOnto(incoming: Page, page: Page): void {\n incoming.component = page.component\n\n // The chain beneath only ever arrives on a document response. Carried forward from the mounted\n // page it would re-seed the stack from a snapshot taken before anything was opened or closed.\n const { [MODAL_BENEATH_PROP]: _seededChain, ...mounted } = page.props\n incoming.props = { ...mounted, ...incoming.props }\n\n if (page.scrollProps !== undefined || incoming.scrollProps !== undefined) {\n incoming.scrollProps = { ...page.scrollProps, ...incoming.scrollProps }\n }\n if (page.onceProps !== undefined || incoming.onceProps !== undefined) {\n incoming.onceProps = { ...page.onceProps, ...incoming.onceProps }\n }\n}\n\nlet installed = false\n\n/**\n * Starts grafting modal responses onto the mounted page.\n *\n * Idempotent, and installed from the resolver rather than at module scope so importing this package\n * has no side effect.\n */\nexport function installGraft(): void {\n if (installed || typeof document === 'undefined') return\n installed = true\n\n interceptors.onVisitResponse((_visit: InternalActiveVisit, response: HttpResponse) => {\n const data: unknown = response.data\n const page = typeof data === 'object' && data !== null ? (data as Page) : null\n const isModal = response.headers[MODAL_MARKER_HEADER] === 'true'\n\n if (isModal && mounted !== null && page !== null) {\n graftOnto(page, mounted)\n }\n\n if (page !== null) dropDismissedLevels(page, isModal)\n\n return response\n })\n}\n\n/**\n * Take the levels this response drops off the page, before the page changes.\n *\n * A level's props sit at `modal.props.*`, and anything addressing them by path — a scroll\n * subscription, for one — reads that path off the page whenever Inertia announces a response. The\n * path exists only while the level does, so a response that drops the level leaves those reads\n * looking for something the new page does not carry.\n *\n * Here rather than at a dismissal, because a level is dropped by any navigation at all: closing it,\n * the browser's back button, a link to somewhere else entirely. Interceptors run before the page is\n * set and before that announcement, which is the only point early enough to matter — flushed, so\n * React has removed them by then rather than at the next render, which comes after.\n */\nexport function dropDismissedLevels(page: Page, isModal: boolean): void {\n const held = readHeldStack()\n if (held.length === 0) return\n\n const carried = page.props[MODAL_PROP]\n const incoming = isModal && isModalData(carried) ? carried : null\n const beneath = page.props[MODAL_BENEATH_PROP]\n const next = applyStack(held, incoming, Array.isArray(beneath) ? beneath.filter(isModalData) : null)\n\n // A response that names props is merged over the ones the page holds rather than replacing them,\n // so a landing that carries no level leaves the level it dismissed exactly where it was: the\n // sheet stays on screen, over a page that has already moved on, with nothing left that can close\n // it. Naming them empty is what a merge can overwrite — omitting them is what it keeps.\n if (next.length === 0) {\n page.props[MODAL_PROP] = null\n page.props[MODAL_BENEATH_PROP] = null\n }\n\n const dropped = held\n .filter((level) => !next.some((kept) => samePath(kept.url, level.url)))\n .map((level) => level.url)\n\n if (dropped.length > 0) flushSync(() => dismissLevels(dropped))\n}\n","import { createContext } from 'react'\nimport type { ModalData } from '../core/wire'\n\nexport interface ModalLevel {\n modal: ModalData\n depth: number\n isTop: boolean\n}\n\n/** Every open level, outermost first. */\nexport const ModalStackContext = createContext<ModalData[]>([])\n\n/** The level the component reading it is rendered in. */\nexport const ModalLevelContext = createContext<ModalLevel | null>(null)\n","// Telling the server which levels the client has open.\n//\n// On the router rather than at this package's own visit sites, because the visits that most need it\n// are the application's: a plain `<Link>`, a `router.visit` after a submit. No scheme inside the\n// package reaches those, and a header a consumer has to add by hand is one a consumer forgets — the\n// level it is forgotten on closes onto a sheet again, silently, with every test still green.\nimport { interceptors } from '@inertiajs/core'\nimport type { HttpRequestConfig } from '@inertiajs/core'\n\nimport { encodeHeldLevels, MODAL_HELD_HEADER } from '../core/wire'\nimport { readHeldStack } from './stack-store'\n\n// This package targets Workers and deliberately omits the DOM lib, so `document` is not a known\n// global. Its presence is the only thing asked of it here.\ndeclare const document: object | undefined\n\n/**\n * `config` carrying the levels the client has open.\n *\n * The visit is not consulted, and is typed as `unknown` to say so: what the client holds is a\n * property of the client, not of whichever visit happens to be leaving.\n *\n * Read here, as the request is built, rather than closed over: the stack changes with every\n * response, and a value captured earlier would describe it as it was one level ago.\n */\nexport function nameHeldLevels(_visit: unknown, config: HttpRequestConfig): HttpRequestConfig {\n return {\n ...config,\n headers: {\n ...config.headers,\n [MODAL_HELD_HEADER]: encodeHeldLevels(readHeldStack().map((level) => level.url)),\n },\n }\n}\n\nlet installed = false\n\n/**\n * Starts naming the open levels on every visit the router makes.\n *\n * Idempotent, and installed from the resolver rather than at module scope so importing this package\n * has no side effect. A server never installs it: the held stack is client-only state, and a\n * request interceptor registered in an isolate would read one visitor's sheets while answering\n * another's.\n */\nexport function installHeldHeader(): void {\n if (installed || typeof document === 'undefined') return\n installed = true\n\n interceptors.onVisitRequest(nameHeldLevels)\n}\n","import type { Page } from '@inertiajs/core'\nimport { getSsrExcludeMatchers, isSsrExcluded } from '@stratal/inertia/services/ssr-exclusion'\n\nimport { isModalData, MODAL_BENEATH_PROP, MODAL_PROP } from '../core/wire'\nimport { rememberModalComponents, type ModalComponent } from './component-registry'\nimport { installGraft } from './graft'\nimport { installHeldHeader } from './held-header'\n\n// This package targets Workers and deliberately omits the DOM lib, so `document` is not a known\n// global. Its presence is the only thing asked of it here.\ndeclare const document: object | undefined\n\n/**\n * Wraps the `resolve` callback `createInertiaApp` is given, so a page's modal levels are resolved\n * before the tree renders.\n *\n * Resolution is a real `import()`, so it cannot happen during render. Doing it here — the one point\n * Inertia already awaits before it swaps the page — is what puts a level in server HTML and keeps a\n * step between levels from flashing an empty sheet.\n *\n * @example\n * ```tsx\n * createInertiaApp({\n * resolve: withModals((name) => pages[`./pages/${name}.tsx`]()),\n * setup: ({ el, App, props }) => hydrateRoot(el, <App {...props} />),\n * })\n * ```\n */\nexport function withModals<TComponent>(\n resolve: (name: string) => TComponent | Promise<TComponent>,\n): (name: string, page?: Page) => Promise<TComponent> {\n resolveCallback = resolve\n\n return async (name, page) => {\n installGraft()\n installHeldHeader()\n\n const [component] = await Promise.all([resolve(name), prepare(resolve, page)])\n return component\n }\n}\n\nlet resolveCallback: ((name: string) => unknown) | undefined\n\n/**\n * Resolves one level's component after the tree has mounted.\n *\n * `<Modal />` needs this for a level the pre-pass could not reach — one excluded from SSR, on a\n * server, which by definition has no entry to resolve it from.\n */\nexport function resolveModalComponent(name: string): Promise<ModalComponent> {\n if (resolveCallback === undefined) {\n throw new Error(\n '[@stratal/inertia-modal] no resolver registered. Pass createInertiaApp\\'s `resolve` through '\n + 'withModals() before rendering <Modal />.',\n )\n }\n\n return unwrap(resolveCallback(name))\n}\n\n/** Resolves every level the page carries that this runtime has an entry for. */\nasync function prepare<TComponent>(\n resolve: (name: string) => TComponent | Promise<TComponent>,\n page: Page | undefined,\n): Promise<void> {\n if (page === undefined) return\n\n const top = page.props[MODAL_PROP]\n const beneath = page.props[MODAL_BENEATH_PROP]\n const levels = [...(Array.isArray(beneath) ? beneath : []), top].filter(isModalData)\n if (levels.length === 0) return\n\n const names = [...new Set(levels.map((level) => level.component))].filter(resolvableHere)\n\n const resolved = await Promise.all(names.map(async (component) => {\n try {\n return [component, await unwrap(resolve(component))] as const\n }\n catch (cause) {\n throw new Error(`[@stratal/inertia-modal] could not resolve modal component \"${component}\".`, { cause })\n }\n }))\n\n rememberModalComponents(Object.fromEntries(resolved))\n}\n\n/**\n * Whether this runtime has an entry to resolve `component` from.\n *\n * `ssrExclude` drops a page from the worker bundle, so on a server there is nothing to import —\n * which is the whole of what the option means. Every page stays in the client glob, so a browser\n * can always resolve one, and must: `<Modal />` draws nothing for a level whose component has not\n * landed, and its own resolve effect runs only once the page has already swapped. A level left to\n * that takes the sheet a visitor is looking at off the screen until the `import()` returns.\n *\n * Rendering it too early is not this decision. `<Modal />` withholds an excluded level for the\n * render that hydrates, which is the only one that has to match HTML the server produced.\n */\nfunction resolvableHere(component: string): boolean {\n return typeof document !== 'undefined' || !isSsrExcluded(component, getSsrExcludeMatchers())\n}\n\n/** A page module's default export, or the module itself when it is the component. */\nasync function unwrap<TComponent>(loaded: TComponent | Promise<TComponent>): Promise<ModalComponent> {\n const mod = await loaded\n return (mod as { default?: ModalComponent }).default ?? (mod as ModalComponent)\n}\n","import { usePage } from '@inertiajs/react'\nimport { getSsrExcludeMatchers, isSsrExcluded } from '@stratal/inertia/services/ssr-exclusion'\nimport { useEffect, useRef, useState, useSyncExternalStore } from 'react'\n\nimport { levelPath } from '../core/level-path'\nimport { isModalData, MODAL_BENEATH_PROP, MODAL_PROP, type ModalData } from '../core/wire'\nimport { readModalComponents, rememberModalComponents } from './component-registry'\nimport { holdMountedPage } from './graft'\nimport { ModalLevelContext, ModalStackContext } from './modal-context'\nimport { resolveModalComponent } from './resolver'\nimport { applyStack } from './stack'\nimport { holdStack, readHeldStack } from './stack-store'\n\n// This package targets Workers and deliberately omits the DOM lib, so `document`\n// is not a known global here. The guard only needs to know whether it exists,\n// which is exactly what this declares — narrower than pulling DOM types in.\ndeclare const document: object | undefined\n\n/** The store below never changes, so there is nothing to subscribe to. */\nconst unchanging = () => () => {}\n\n/**\n * Whether React is past the render that hydrates.\n *\n * `useSyncExternalStore`'s third argument is the value React reads while server-rendering and\n * while hydrating, and its second is what it switches to once hydration is over — so this asks\n * React which phase it is in rather than asking something to remember. Nothing to set, nothing to\n * reset between documents, and no answer that can outlive the render it describes.\n */\nfunction usePastHydration(): boolean {\n return useSyncExternalStore(unchanging, () => true, () => false)\n}\n\n/**\n * Headless modal host. Place it once in your layout.\n *\n * Renders every open level, so a modal opened from inside another appears above it with the one\n * below still mounted.\n *\n * @example\n * ```tsx\n * export function DashboardLayout({ children }) {\n * return (\n * <>\n * <main>{children}</main>\n * <Modal />\n * </>\n * )\n * }\n * ```\n */\nexport function Modal() {\n // `modal` and `modalBeneath` come from the `PageProps` augmentation in `../page-props`,\n // bare-imported by this entry (`src/react.tsx`) — no type argument needed here.\n const page = usePage()\n const carried = page.props[MODAL_PROP]\n const incoming = isModalData(carried) ? carried : null\n const beneath = page.props[MODAL_BENEATH_PROP] ?? null\n const pastHydration = usePastHydration()\n\n // Seeded with an initializer rather than `[]`: the payload is already in props on the very first\n // render, and taking it in an effect is what kept every level out of server HTML and out of the\n // first client paint. The held stack is consulted only in a browser — a Workers isolate serves\n // many requests, so a module-level value read during SSR would put one user's open sheets into\n // another user's HTML.\n const [stack, setStack] = useState<ModalData[]>(() => {\n const held = typeof document !== 'undefined' ? readHeldStack() : []\n return applyStack(held, incoming, beneath)\n })\n\n // Read while deciding the next stack; reading it from state inside the effect would pin a stale\n // value between renders.\n const stackRef = useRef<ModalData[]>(stack)\n // What this render may draw, which is NOT the same as what has been resolved so\n // far. `withModals()` resolves a response's levels and remembers them BEFORE\n // Inertia swaps the page, so by the time a step's render runs the registry\n // already holds the arriving level — while state seeded at mount, back when\n // that level did not exist, does not. Reading the registry here is what lets\n // the arriving sheet be drawn in the same commit that drops the one it\n // replaces; state alone withholds it until an effect has copied the registry\n // across, leaving a commit with no sheet on screen at every step of a stack.\n const components = readModalComponents()\n\n // A level whose own chunk is still in flight when the stack changes is resolved by the effect\n // below, which writes it to that same registry — module state React is not subscribed to. This\n // is the render that reads it back.\n const [, redrawWithResolved] = useState(0)\n\n // The graft merges the next modal response onto the page as it stands, so it needs this one.\n useEffect(() => {\n holdMountedPage(page)\n }, [page])\n\n useEffect(() => {\n const next = applyStack(stackRef.current, incoming, beneath)\n stackRef.current = next\n if (typeof document !== 'undefined') holdStack(next)\n setStack(next)\n }, [incoming, beneath])\n\n // The levels this render cannot draw yet. Named as one string so the effect below depends on\n // WHICH components are missing rather than on the lookup object, whose identity never changes.\n // NUL joins them because it is the one character a component name cannot contain, so no name\n // can split back into two.\n const missing = [...new Set(stack.map((level) => level.component))]\n .filter((name) => !components[name])\n .join('\\0')\n\n useEffect(() => {\n const wanted = missing === '' ? [] : missing.split('\\0')\n if (wanted.length === 0) return\n\n let cancelled = false\n\n // Read once, outside the updater: a state updater must be pure, and React may run\n // one more than once or discard the render it belonged to, while the store write\n // below has to happen exactly once.\n const held = readModalComponents()\n\n void Promise.allSettled(wanted.map(async (name) => [name, await resolveModalComponent(name)] as const))\n .then((results) => {\n if (cancelled) return\n\n const next = { ...held }\n for (const result of results) {\n if (result.status === 'fulfilled') {\n const [name, component] = result.value\n next[name] = component\n }\n }\n rememberModalComponents(next)\n redrawWithResolved((n) => n + 1)\n\n // One mis-registered level must not blank out the others resolved in the same pass, but it\n // still needs a loud signal instead of a silent gap.\n for (const [index, result] of results.entries()) {\n if (result.status === 'rejected') {\n console.error(`inertia-modal: failed to resolve component \"${wanted[index]}\"`, result.reason)\n }\n }\n })\n\n return () => { cancelled = true }\n }, [missing])\n\n if (stack.length === 0) return null\n\n return (\n <ModalStackContext.Provider value={stack}>\n {stack.map((level, index) => {\n const Component = components[level.component]\n if (!Component) return null\n\n // A level the server had no entry to render must not be in the render that hydrates\n // either: hydration matches this tree against HTML that never contained it. It appears on\n // the very next render, which React schedules as soon as hydration is over.\n if (!pastHydration && isSsrExcluded(level.component, getSsrExcludeMatchers())) return null\n\n // Keyed by the level's identity rather than its address: keyed by the whole url, a filter\n // applied through `refresh()` gives the level a new key, and React answers a new key by\n // unmounting the old subtree — discarding the very state `refresh()` asks to preserve.\n return (\n <ModalLevelContext.Provider\n key={levelPath(level.url)}\n value={{ modal: level, depth: index, isTop: index === stack.length - 1 }}\n >\n <Component {...level.props} />\n </ModalLevelContext.Provider>\n )\n })}\n </ModalStackContext.Provider>\n )\n}\n","// The way a sheet is opened.\n//\n// The visit options live here rather than in a constant a consumer spreads: an exported options\n// object can be forgotten at one call site, and the failure is silent — the sheet opens, but the\n// page beneath is re-sent with it.\nimport { Link } from '@inertiajs/react'\nimport type { ComponentProps } from 'react'\n\nimport { MODAL_PROP } from '../core/wire'\n\nexport type ModalLinkProps = ComponentProps<typeof Link>\n\n/**\n * Opens a modal route as a sheet over the current page.\n *\n * @example\n * ```tsx\n * <ModalLink href=\"/parent/1/edit\">Edit</ModalLink>\n * <ModalLink href=\"/parent/1/edit\" prefetch>Edit</ModalLink>\n * ```\n */\nexport function ModalLink({ children, ...rest }: ModalLinkProps) {\n return (\n <Link\n // Narrows what the SERVER sends to the level alone. `preserveState` keeps the page behind from\n // remounting; `preserveScroll` keeps its position, which is where the reader returns.\n only={[MODAL_PROP]}\n preserveState\n preserveScroll\n {...rest}\n >\n {children}\n </Link>\n )\n}\n","import type {\n ReloadOptions,\n RequestPayload,\n UrlMethodPair,\n VisitHelperOptions,\n VisitOptions,\n} from '@inertiajs/core'\nimport { router } from '@inertiajs/react'\nimport { useCallback, useContext } from 'react'\n\nimport { MODAL_PROP, modalPropPath, type ModalData } from '../core/wire'\nimport { ModalLevelContext, ModalStackContext } from './modal-context'\n\n/** Function members are declared as properties, not methods: they are closures, and\n * destructuring them is how this hook is used. */\ninterface UseModalReturn {\n /** This level, or `undefined` outside a modal. */\n modal: ModalData | undefined\n /** How deep this level sits. The outermost is 0. */\n depth: number\n /** Whether this is the level the reader is looking at. */\n isTop: boolean\n /** Close this level and land where it was opened from. */\n close: <TPayload extends RequestPayload = RequestPayload>(\n options?: VisitOptions<TPayload>,\n ) => void\n /** Close every open level and land where the outermost one was opened from. */\n closeAll: <TPayload extends RequestPayload = RequestPayload>(\n options?: VisitOptions<TPayload>,\n ) => void\n /**\n * Re-read this level under a refined query — a filter, a sort, a code the server prices.\n *\n * Takes the same options as `router.get`, so a caller can observe the visit it started rather\n * than the next one to finish: without `onFinish` here, timing this means subscribing to the\n * router's own event, which fires for every visit in flight — a poll included.\n */\n refresh: <TPayload extends RequestPayload = RequestPayload>(\n query?: TPayload,\n options?: VisitHelperOptions<TPayload>,\n ) => void\n /**\n * Fetch some of this level's props again, leaving the rest of the page alone.\n *\n * Takes `router.reload`'s own options. `only`, `except` and `reset` name props, so they are given\n * in the level's terms — `'items'`, not `'modal.props.items'` — and anchored here; everything\n * else is passed through untouched.\n */\n reload: <TPayload extends RequestPayload = RequestPayload>(\n options?: ReloadOptions<TPayload>,\n ) => void\n /**\n * Open a modal route from code, the way `<ModalLink>` opens one from a click.\n *\n * Carries the same visit options, and takes the same arguments as `router.visit`, so anything\n * they accept — `replace`, a method, callbacks — is passed straight through.\n */\n visit: <TPayload extends RequestPayload = RequestPayload>(\n href: string | URL | UrlMethodPair,\n options?: VisitOptions<TPayload>,\n ) => void\n}\n\n/**\n * Closing is always an explicit visit, never `history.back()`.\n *\n * Going back is cheaper — the entry below is already in history, and Inertia answers it from cache\n * without touching the server. But a cached entry is a SNAPSHOT of the whole page as it was when\n * that entry was stored, and restoring it replaces every prop the page holds, not merely the modal\n * ones. A page that has changed since — an autosaving draft, a filtered list — is rewound to the\n * moment it was cached, with no sign that it happened. Landing on a URL asks the server instead:\n * what comes back cannot disagree with what is stored.\n */\nconst CLOSE_OPTIONS = { replace: true, preserveScroll: true, preserveState: true } as const\n\nexport function useModal(): UseModalReturn {\n const level = useContext(ModalLevelContext)\n const stack = useContext(ModalStackContext)\n const modal = level?.modal\n\n const close = useCallback(\n <TPayload extends RequestPayload = RequestPayload>(options: VisitOptions<TPayload> = {}) => {\n if (modal === undefined) return\n router.visit(modal.close, { ...CLOSE_OPTIONS, ...options })\n },\n [modal],\n )\n\n /**\n * Dismissing the whole stack is the outermost level closing: every level above it goes with it,\n * and it lands where it would have landed alone.\n */\n const closeAll = useCallback(\n <TPayload extends RequestPayload = RequestPayload>(options: VisitOptions<TPayload> = {}) => {\n const outermost = stack[0]\n if (outermost === undefined) return\n router.visit(outermost.close, { ...CLOSE_OPTIONS, ...options })\n },\n [stack],\n )\n\n /**\n * A refined re-read is an ordinary visit to this level's own url. The client recognises the\n * answer as this level by that url, so nothing has to declare the intent.\n */\n const refresh = useCallback(\n <TPayload extends RequestPayload = RequestPayload>(\n query?: TPayload,\n options: VisitHelperOptions<TPayload> = {},\n ) => {\n if (modal === undefined) return\n // Spread last, so a caller can override any of them — the same rule `visit()` follows.\n router.get(modal.url, query, {\n preserveState: true,\n preserveScroll: true,\n replace: true,\n ...options,\n })\n },\n [modal],\n )\n\n const reload = useCallback(\n <TPayload extends RequestPayload = RequestPayload>(options: ReloadOptions<TPayload> = {}) => {\n // `router.reload` preserves scroll and state itself, so neither is named here.\n //\n // Three of its fields name props, and a level's props are not where the client sees them.\n // Each is anchored only when given: writing `only: undefined` into the options would read as\n // a request for no props rather than as the absence of a request.\n router.reload({\n ...options,\n ...(options.only && { only: options.only.map(modalPropPath) }),\n ...(options.except && { except: options.except.map(modalPropPath) }),\n ...(options.reset && { reset: options.reset.map(modalPropPath) }),\n })\n },\n [],\n )\n\n /**\n * The options `<ModalLink>` sets, applied to a visit made from code. Spread first so a caller\n * can override any of them, exactly as passing the prop to the component would.\n */\n const visit = useCallback(\n <TPayload extends RequestPayload = RequestPayload>(\n href: string | URL | UrlMethodPair,\n options: VisitOptions<TPayload> = {},\n ) => {\n router.visit(href, {\n only: [MODAL_PROP],\n preserveState: true,\n preserveScroll: true,\n ...options,\n })\n },\n [],\n )\n\n return {\n modal,\n depth: level?.depth ?? 0,\n isTop: level?.isTop ?? false,\n close,\n closeAll,\n refresh,\n reload,\n visit,\n }\n}\n","import { Deferred as InertiaDeferred } from '@inertiajs/react'\nimport { useContext, type ComponentProps } from 'react'\n\nimport { modalPropPath } from '../core/wire'\nimport { ModalLevelContext } from './modal-context'\n\n/**\n * Props of `@inertiajs/react`'s `<Deferred>`.\n *\n * Re-exported because Inertia does not export the interface itself, and a\n * declaration file cannot name a type it cannot import.\n */\nexport type DeferredProps = ComponentProps<typeof InertiaDeferred>\n\nfunction resolve(data: string | string[], nested: boolean): string | string[] {\n if (!nested) return data\n return Array.isArray(data) ? data.map(modalPropPath) : modalPropPath(data)\n}\n\n/**\n * `@inertiajs/react`'s `<Deferred>`, with `data` resolved against the modal it is rendered in.\n *\n * The component addresses its prop by name at the page root, and a level's props are nested under\n * one page prop. Import this one instead of the Inertia component and the same JSX works in a sheet\n * and on a page — outside a modal the name is already the path.\n *\n * @example\n * ```tsx\n * import { Deferred } from '@stratal/inertia-modal/react'\n *\n * <Deferred data=\"entries\" fallback={<Skeleton />}>\n * <Entries entries={entries} />\n * </Deferred>\n * ```\n */\nexport const Deferred: typeof InertiaDeferred = Object.assign(\n function Deferred({ data, ...props }: DeferredProps) {\n const level = useContext(ModalLevelContext)\n return <InertiaDeferred {...props} data={resolve(data, level !== null)} />\n },\n { displayName: 'Deferred' },\n)\n","import { InfiniteScroll as InertiaInfiniteScroll } from '@inertiajs/react'\nimport { forwardRef, useContext, useEffect, useState, type ComponentProps } from 'react'\n\nimport { modalPropPath } from '../core/wire'\nimport { ModalLevelContext } from './modal-context'\nimport { onLevelDismissed } from './scroll-teardown'\n\n/**\n * Props of `@inertiajs/react`'s `<InfiniteScroll>`.\n *\n * Re-exported because Inertia does not export the interface itself, and a\n * declaration file cannot name a type it cannot import.\n */\nexport type InfiniteScrollProps = ComponentProps<typeof InertiaInfiniteScroll>\n\n/**\n * `@inertiajs/react`'s `<InfiniteScroll>`, with `data` resolved against the modal it is rendered in.\n *\n * The component addresses its prop by name at the page root, and a level's props are nested under\n * one page prop. Import this one instead of the Inertia component and the same JSX works in a sheet\n * and on a page — outside a modal the name is already the path.\n *\n * @example\n * ```tsx\n * import { InfiniteScroll } from '@stratal/inertia-modal/react'\n *\n * <InfiniteScroll data=\"items\">\n * {items.data.map((item) => <Row key={item.id} item={item} />)}\n * </InfiniteScroll>\n * ```\n */\nexport const InfiniteScroll: typeof InertiaInfiniteScroll = forwardRef(\n function InfiniteScroll({ data, ...props }: InfiniteScrollProps, ref) {\n const level = useContext(ModalLevelContext)\n const url = level?.modal.url\n const [dismissed, setDismissed] = useState(false)\n\n useEffect(() => {\n if (url === undefined) return\n setDismissed(false)\n return onLevelDismissed(url, () => setDismissed(true))\n }, [url])\n\n // The subscription this renders lives inside Inertia's component, so not rendering it is what\n // ends the subscription. See `scroll-teardown` for why that cannot wait for the unmount.\n if (dismissed) return null\n\n return <InertiaInfiniteScroll {...props} data={level ? modalPropPath(data) : data} ref={ref} />\n },\n)\n","import { clearModalComponents } from './component-registry'\nimport { clearMountedPage } from './graft'\nimport { clearHeldStack } from './stack-store'\n\n/**\n * Discard everything this package holds outside the React tree.\n *\n * Three stores survive unmounting, because each exists precisely to outlive a render: the open\n * stack, the page a level grafts onto, and the components resolved so far. In a browser that is\n * what they are for — one document, one visitor, state that must not reset when a sheet closes.\n * Under a test runner the same module is reused across files, so one test's open sheet is the next\n * test's starting state: a sheet nothing opened, or a component a test meant to leave unresolved\n * answering instantly from an earlier test's resolution.\n *\n * Call it between tests. It is the only supported way to empty them; the individual stores are\n * internal so that resetting cannot drift out of step with what the package holds.\n *\n * @example\n * ```ts\n * import { resetModalState } from '@stratal/inertia-modal/react'\n *\n * beforeEach(() => resetModalState())\n * ```\n */\nexport function resetModalState(): void {\n clearHeldStack()\n clearMountedPage()\n clearModalComponents()\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { a as ModalData, o as modalPropPath } from "./wire-CbwmWkPr.mjs";
|
|
2
|
+
//#region src/augment/test-response.d.ts
|
|
3
|
+
declare module '@stratal/testing' {
|
|
4
|
+
interface TestResponse {
|
|
5
|
+
/** Assert a modal is open. The callback receives the level the response rendered. */
|
|
6
|
+
assertModal(callback?: (level: ModalData) => void): Promise<this>;
|
|
7
|
+
/** Assert no modal is open. */
|
|
8
|
+
assertNoModal(): Promise<this>;
|
|
9
|
+
/** Assert the component of the level this response rendered, or of the one at `depth`. */
|
|
10
|
+
assertModalComponent(component: string, depth?: number): Promise<this>;
|
|
11
|
+
/** Assert what a level sits over — the page or modal route its `base` names. */
|
|
12
|
+
assertModalBase(base: string, depth?: number): Promise<this>;
|
|
13
|
+
/** Assert where closing a level lands. */
|
|
14
|
+
assertModalClose(close: string, depth?: number): Promise<this>;
|
|
15
|
+
/** Assert every open level's component, outermost first. Document responses only. */
|
|
16
|
+
assertModalComponents(components: string[]): Promise<this>;
|
|
17
|
+
/** Assert how many levels are open. Document responses only. */
|
|
18
|
+
assertModalCount(count: number): Promise<this>;
|
|
19
|
+
/** Assert the depth the level this response rendered sits at — 0 is outermost. */
|
|
20
|
+
assertModalDepth(depth: number): Promise<this>;
|
|
21
|
+
/** Assert a level's prop at the given dot-path equals the expected value. */
|
|
22
|
+
assertModalProp(path: string, expected: unknown, depth?: number): Promise<this>;
|
|
23
|
+
/** Assert the response carries the level alone, without the chain beneath it. */
|
|
24
|
+
assertModalOnly(): Promise<this>;
|
|
25
|
+
/** Read back the level this response rendered, or the one at `depth`. */
|
|
26
|
+
modalLevel<TProps = Record<string, unknown>>(depth?: number): Promise<ModalData<TProps>>;
|
|
27
|
+
/** Read back every open level, outermost first. Document responses only. */
|
|
28
|
+
modalLevels<TProps = Record<string, unknown>>(): Promise<ModalData<TProps>[]>;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { type ModalData, modalPropPath };
|
|
33
|
+
//# sourceMappingURL=testing.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.mts","names":[],"sources":["../src/augment/test-response.ts"],"mappings":";;;YAMY;;IAER,YAAY,YAAY,OAAO,qBAAqB;;IAEpD,iBAAiB;;IAEjB,qBAAqB,mBAAmB,iBAAiB;;IAEzD,gBAAgB,cAAc,iBAAiB;;IAE/C,iBAAiB,eAAe,iBAAiB;;IAEjD,sBAAsB,uBAAuB;;IAE7C,iBAAiB,gBAAgB;;IAEjC,iBAAiB,gBAAgB;;IAEjC,gBAAgB,cAAc,mBAAmB,iBAAiB;;IAElE,mBAAmB;;IAEnB,WAAW,SAAS,yBAAyB,iBAAiB,QAAQ,UAAU;;IAEhF,YAAY,SAAS,4BAA4B,QAAQ,UAAU"}
|
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { a as MODAL_PROP, c as isModalData, t as MODAL_BENEATH_PROP, u as modalPropPath } from "./wire-BNVvmku4.mjs";
|
|
2
|
+
import { TestResponse, getValueAtPath } from "@stratal/testing";
|
|
3
|
+
import { expect } from "vitest";
|
|
4
|
+
//#region src/augment/test-response.ts
|
|
5
|
+
const LEVEL_ONLY = "The response carries only the level it rendered, so it does not say how many levels are open. Assert that level with assertModalComponent(), or request the URL as a document to get the whole chain.";
|
|
6
|
+
function readModal(page) {
|
|
7
|
+
const carried = page.props[MODAL_PROP];
|
|
8
|
+
if (carried === void 0 || carried === null) return null;
|
|
9
|
+
if (!isModalData(carried)) throw new Error(`The response carries a "${MODAL_PROP}" prop that is not a modal level. Something other than a modal route wrote to that prop name.`);
|
|
10
|
+
const beneath = page.props[MODAL_BENEATH_PROP];
|
|
11
|
+
return {
|
|
12
|
+
level: carried,
|
|
13
|
+
beneath: beneath ?? [],
|
|
14
|
+
carriesChain: beneath !== void 0 && beneath !== null
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async function requireModal(response) {
|
|
18
|
+
const carried = readModal(await response.json());
|
|
19
|
+
if (carried === null) throw new Error("Expected a modal to be open, but the response carries none.");
|
|
20
|
+
return carried;
|
|
21
|
+
}
|
|
22
|
+
/** Every level the response reports, outermost first. */
|
|
23
|
+
function chainOf(carried) {
|
|
24
|
+
if (!carried.carriesChain) throw new Error(LEVEL_ONLY);
|
|
25
|
+
return [...carried.beneath, carried.level];
|
|
26
|
+
}
|
|
27
|
+
function levelAt(carried, depth) {
|
|
28
|
+
if (depth === void 0) return carried.level;
|
|
29
|
+
const chain = chainOf(carried);
|
|
30
|
+
const level = chain[depth];
|
|
31
|
+
if (level === void 0) throw new Error(`Expected a modal level at depth ${depth}, but the response carries ${chain.length}.`);
|
|
32
|
+
return level;
|
|
33
|
+
}
|
|
34
|
+
function augmentTestResponse() {
|
|
35
|
+
TestResponse.macro("assertModal", async function(callback) {
|
|
36
|
+
const carried = await requireModal(this);
|
|
37
|
+
callback?.(carried.level);
|
|
38
|
+
return this;
|
|
39
|
+
});
|
|
40
|
+
TestResponse.macro("assertNoModal", async function() {
|
|
41
|
+
const carried = readModal(await this.json());
|
|
42
|
+
expect(carried, `Expected no modal to be open, but "${carried?.level.component}" is`).toBeNull();
|
|
43
|
+
return this;
|
|
44
|
+
});
|
|
45
|
+
TestResponse.macro("assertModalComponent", async function(component, depth) {
|
|
46
|
+
const level = levelAt(await requireModal(this), depth);
|
|
47
|
+
expect(level.component, `Expected the modal to be "${component}", got "${level.component}"`).toBe(component);
|
|
48
|
+
return this;
|
|
49
|
+
});
|
|
50
|
+
TestResponse.macro("assertModalBase", async function(base, depth) {
|
|
51
|
+
const level = levelAt(await requireModal(this), depth);
|
|
52
|
+
expect(level.base, `Expected the modal to sit over "${base}", got "${level.base}"`).toBe(base);
|
|
53
|
+
return this;
|
|
54
|
+
});
|
|
55
|
+
TestResponse.macro("assertModalClose", async function(close, depth) {
|
|
56
|
+
const level = levelAt(await requireModal(this), depth);
|
|
57
|
+
expect(level.close, `Expected closing the modal to land on "${close}", got "${level.close}"`).toBe(close);
|
|
58
|
+
return this;
|
|
59
|
+
});
|
|
60
|
+
TestResponse.macro("assertModalComponents", async function(components) {
|
|
61
|
+
const actual = chainOf(await requireModal(this)).map((level) => level.component);
|
|
62
|
+
expect(actual, `Expected the open modals to be ${JSON.stringify(components)} outermost first, got ${JSON.stringify(actual)}`).toStrictEqual(components);
|
|
63
|
+
return this;
|
|
64
|
+
});
|
|
65
|
+
TestResponse.macro("assertModalCount", async function(count) {
|
|
66
|
+
const chain = chainOf(await requireModal(this));
|
|
67
|
+
expect(chain.length, `Expected ${count} modal level(s) open, got ${chain.length}: ${JSON.stringify(chain.map((level) => level.component))}`).toBe(count);
|
|
68
|
+
return this;
|
|
69
|
+
});
|
|
70
|
+
TestResponse.macro("assertModalDepth", async function(depth) {
|
|
71
|
+
const actual = chainOf(await requireModal(this)).length - 1;
|
|
72
|
+
expect(actual, `Expected the modal to be at depth ${depth}, got ${actual}`).toBe(depth);
|
|
73
|
+
return this;
|
|
74
|
+
});
|
|
75
|
+
TestResponse.macro("assertModalProp", async function(path, expected, depth) {
|
|
76
|
+
const level = levelAt(await requireModal(this), depth);
|
|
77
|
+
const actual = getValueAtPath(level.props, path);
|
|
78
|
+
expect(actual, `Expected prop "${path}" of the modal to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toStrictEqual(expected);
|
|
79
|
+
return this;
|
|
80
|
+
});
|
|
81
|
+
TestResponse.macro("assertModalOnly", async function() {
|
|
82
|
+
const carried = await requireModal(this);
|
|
83
|
+
expect(carried.carriesChain, "Expected the response to carry the level alone, but it assembled the chain beneath it. Only a document request renders what sits below.").toBe(false);
|
|
84
|
+
return this;
|
|
85
|
+
});
|
|
86
|
+
TestResponse.macro("modalLevel", async function(depth) {
|
|
87
|
+
return levelAt(await requireModal(this), depth);
|
|
88
|
+
});
|
|
89
|
+
TestResponse.macro("modalLevels", async function() {
|
|
90
|
+
return chainOf(await requireModal(this));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/testing.ts
|
|
95
|
+
augmentTestResponse();
|
|
96
|
+
//#endregion
|
|
97
|
+
export { modalPropPath };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/augment/test-response.ts","../src/testing.ts"],"sourcesContent":["import type { Page } from '@inertiajs/core'\nimport { getValueAtPath, TestResponse } from '@stratal/testing'\nimport { expect } from 'vitest'\nimport { isModalData, MODAL_BENEATH_PROP, MODAL_PROP, type ModalData } from '../core/wire'\n\ndeclare module '@stratal/testing' {\n interface TestResponse {\n /** Assert a modal is open. The callback receives the level the response rendered. */\n assertModal(callback?: (level: ModalData) => void): Promise<this>\n /** Assert no modal is open. */\n assertNoModal(): Promise<this>\n /** Assert the component of the level this response rendered, or of the one at `depth`. */\n assertModalComponent(component: string, depth?: number): Promise<this>\n /** Assert what a level sits over — the page or modal route its `base` names. */\n assertModalBase(base: string, depth?: number): Promise<this>\n /** Assert where closing a level lands. */\n assertModalClose(close: string, depth?: number): Promise<this>\n /** Assert every open level's component, outermost first. Document responses only. */\n assertModalComponents(components: string[]): Promise<this>\n /** Assert how many levels are open. Document responses only. */\n assertModalCount(count: number): Promise<this>\n /** Assert the depth the level this response rendered sits at — 0 is outermost. */\n assertModalDepth(depth: number): Promise<this>\n /** Assert a level's prop at the given dot-path equals the expected value. */\n assertModalProp(path: string, expected: unknown, depth?: number): Promise<this>\n /** Assert the response carries the level alone, without the chain beneath it. */\n assertModalOnly(): Promise<this>\n /** Read back the level this response rendered, or the one at `depth`. */\n modalLevel<TProps = Record<string, unknown>>(depth?: number): Promise<ModalData<TProps>>\n /** Read back every open level, outermost first. Document responses only. */\n modalLevels<TProps = Record<string, unknown>>(): Promise<ModalData<TProps>[]>\n }\n}\n\n/**\n * What one response says about the open levels.\n *\n * `carriesChain` is false on an Inertia visit, which genuinely does not report what sits below —\n * the client holds that. The assertions that read the whole chain say so rather than letting a\n * one-level answer pass as a one-level stack.\n */\ninterface CarriedLevels {\n level: ModalData\n beneath: ModalData[]\n carriesChain: boolean\n}\n\nconst LEVEL_ONLY = 'The response carries only the level it rendered, so it does not say how many levels are open. '\n + 'Assert that level with assertModalComponent(), or request the URL as a document to get the whole chain.'\n\nfunction readModal(page: Page): CarriedLevels | null {\n const carried = page.props[MODAL_PROP]\n // Both readings of \"no level here\": a response that never carried one, and one that says the\n // level it dismissed is gone.\n if (carried === undefined || carried === null) return null\n\n if (!isModalData(carried)) {\n throw new Error(\n `The response carries a \"${MODAL_PROP}\" prop that is not a modal level. `\n + 'Something other than a modal route wrote to that prop name.',\n )\n }\n\n const beneath = page.props[MODAL_BENEATH_PROP]\n\n return { level: carried, beneath: beneath ?? [], carriesChain: beneath !== undefined && beneath !== null }\n}\n\nasync function requireModal(response: TestResponse): Promise<CarriedLevels> {\n const carried = readModal(await response.json<Page>())\n\n if (carried === null) {\n throw new Error('Expected a modal to be open, but the response carries none.')\n }\n\n return carried\n}\n\n/** Every level the response reports, outermost first. */\nfunction chainOf(carried: CarriedLevels): ModalData[] {\n if (!carried.carriesChain) throw new Error(LEVEL_ONLY)\n return [...carried.beneath, carried.level]\n}\n\nfunction levelAt(carried: CarriedLevels, depth: number | undefined): ModalData {\n if (depth === undefined) return carried.level\n\n const chain = chainOf(carried)\n const level = chain[depth]\n if (level === undefined) {\n throw new Error(`Expected a modal level at depth ${depth}, but the response carries ${chain.length}.`)\n }\n\n return level\n}\n\nexport function augmentTestResponse(): void {\n TestResponse.macro('assertModal', async function (this: TestResponse, callback?: (level: ModalData) => void) {\n const carried = await requireModal(this)\n\n callback?.(carried.level)\n\n return this\n })\n\n TestResponse.macro('assertNoModal', async function (this: TestResponse) {\n const carried = readModal(await this.json<Page>())\n\n expect(\n carried,\n `Expected no modal to be open, but \"${carried?.level.component}\" is`,\n ).toBeNull()\n\n return this\n })\n\n TestResponse.macro('assertModalComponent', async function (this: TestResponse, component: string, depth?: number) {\n const carried = await requireModal(this)\n const level = levelAt(carried, depth)\n\n expect(\n level.component,\n `Expected the modal to be \"${component}\", got \"${level.component}\"`,\n ).toBe(component)\n\n return this\n })\n\n TestResponse.macro('assertModalBase', async function (this: TestResponse, base: string, depth?: number) {\n const level = levelAt(await requireModal(this), depth)\n\n expect(\n level.base,\n `Expected the modal to sit over \"${base}\", got \"${level.base}\"`,\n ).toBe(base)\n\n return this\n })\n\n TestResponse.macro('assertModalClose', async function (this: TestResponse, close: string, depth?: number) {\n const level = levelAt(await requireModal(this), depth)\n\n expect(\n level.close,\n `Expected closing the modal to land on \"${close}\", got \"${level.close}\"`,\n ).toBe(close)\n\n return this\n })\n\n TestResponse.macro('assertModalComponents', async function (this: TestResponse, components: string[]) {\n const actual = chainOf(await requireModal(this)).map((level) => level.component)\n\n expect(\n actual,\n `Expected the open modals to be ${JSON.stringify(components)} outermost first, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(components)\n\n return this\n })\n\n TestResponse.macro('assertModalCount', async function (this: TestResponse, count: number) {\n const chain = chainOf(await requireModal(this))\n\n expect(\n chain.length,\n `Expected ${count} modal level(s) open, got ${chain.length}: ${JSON.stringify(chain.map((level) => level.component))}`,\n ).toBe(count)\n\n return this\n })\n\n TestResponse.macro('assertModalDepth', async function (this: TestResponse, depth: number) {\n const actual = chainOf(await requireModal(this)).length - 1\n\n expect(actual, `Expected the modal to be at depth ${depth}, got ${actual}`).toBe(depth)\n\n return this\n })\n\n TestResponse.macro('assertModalProp', async function (this: TestResponse, path: string, expected: unknown, depth?: number) {\n const carried = await requireModal(this)\n const level = levelAt(carried, depth)\n const actual = getValueAtPath(level.props, path)\n\n expect(\n actual,\n `Expected prop \"${path}\" of the modal to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected)\n\n return this\n })\n\n TestResponse.macro('assertModalOnly', async function (this: TestResponse) {\n const carried = await requireModal(this)\n\n expect(\n carried.carriesChain,\n 'Expected the response to carry the level alone, but it assembled the chain beneath it. '\n + 'Only a document request renders what sits below.',\n ).toBe(false)\n\n return this\n })\n\n TestResponse.macro('modalLevel', async function (this: TestResponse, depth?: number) {\n return levelAt(await requireModal(this), depth)\n })\n\n TestResponse.macro('modalLevels', async function (this: TestResponse) {\n return chainOf(await requireModal(this))\n })\n}\n","import { augmentTestResponse } from './augment/test-response'\n\n// Load-bearing, despite the value import above resolving the same module: the\n// DTS bundler emits the `declare module '@stratal/testing'` block only for a\n// module reached by a bare import. Reached through the value import alone, it\n// keeps `augmentTestResponse`'s signature and drops the ambient block — which is\n// this entry's entire public API. In-repo specs import source and would not\n// notice; every consumer would. `tsdown.config.ts` asserts the emitted block.\nimport './augment/test-response'\n\n// Patch TestResponse.prototype with modal assertion methods\naugmentTestResponse()\n\n// Re-export the level shape, so a test can name the props it expects back\nexport type { ModalData } from './core/wire'\nexport { modalPropPath } from './core/wire'\n"],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//#region src/core/wire.ts
|
|
2
|
+
/**
|
|
3
|
+
* The contract between the server and the browser.
|
|
4
|
+
*
|
|
5
|
+
* Framework-free, so both bundles can import it without pulling the other's machinery in behind it.
|
|
6
|
+
*/
|
|
7
|
+
/** The page prop one modal travels on. */
|
|
8
|
+
const MODAL_PROP = "modal";
|
|
9
|
+
/**
|
|
10
|
+
* The levels beneath the requested one.
|
|
11
|
+
*
|
|
12
|
+
* Present only on a document response, which is the one case with no mounted page to graft onto.
|
|
13
|
+
* An Inertia visit never carries it.
|
|
14
|
+
*/
|
|
15
|
+
const MODAL_BENEATH_PROP = "modalBeneath";
|
|
16
|
+
/**
|
|
17
|
+
* Marks a response as a modal, so the client grafts it onto the page it already holds.
|
|
18
|
+
*
|
|
19
|
+
* On the RESPONSE rather than derived from request headers: a redirect drops whatever the original
|
|
20
|
+
* visit was doing, and a modal reached by redirect must still graft.
|
|
21
|
+
*
|
|
22
|
+
* Outside the `x-inertia-*` namespace, which Inertia is still adding to — a silent collision there
|
|
23
|
+
* would present as a partial reload returning the wrong props.
|
|
24
|
+
*/
|
|
25
|
+
const MODAL_MARKER_HEADER = "stratal-modal";
|
|
26
|
+
/** Set on the server's own background sub-request, so a modal route can tell it is a background. */
|
|
27
|
+
const MODAL_DOCUMENT_HEADER = "stratal-modal-document";
|
|
28
|
+
/**
|
|
29
|
+
* Where one of this level's props lives on the page object.
|
|
30
|
+
*
|
|
31
|
+
* No key: a modal response carries exactly one modal, so there is nothing to disambiguate. A
|
|
32
|
+
* targeted reload names this path and visits the level's own url, and the client knows which level
|
|
33
|
+
* it belongs to because it issued the request from there.
|
|
34
|
+
*/
|
|
35
|
+
function modalPropPath(prop) {
|
|
36
|
+
return `${MODAL_PROP}.props.${prop}`;
|
|
37
|
+
}
|
|
38
|
+
/** Whether a partial-request prop name addresses the modal rather than the page beneath it. */
|
|
39
|
+
function isModalPropPath(name) {
|
|
40
|
+
return name === "modal" || name.startsWith(`modal.`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a value is a level this build understands.
|
|
44
|
+
*
|
|
45
|
+
* A tab open across a deploy can hold a payload from an older shape. Refusing it is what keeps the
|
|
46
|
+
* client from rendering a level it cannot address.
|
|
47
|
+
*/
|
|
48
|
+
function isModalData(value) {
|
|
49
|
+
if (value === null || typeof value !== "object") return false;
|
|
50
|
+
const candidate = value;
|
|
51
|
+
return typeof candidate.component === "string" && typeof candidate.url === "string" && typeof candidate.base === "string" && typeof candidate.close === "string" && typeof candidate.props === "object" && candidate.props !== null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The levels the client has open, sent on every modal visit.
|
|
55
|
+
*
|
|
56
|
+
* A level is not a place to land on. Without this the server can only see the `Referer`, which on a
|
|
57
|
+
* visit made from a sheet is that sheet, and a level that takes it aims itself back at the sheet
|
|
58
|
+
* the student is leaving.
|
|
59
|
+
*
|
|
60
|
+
* Outside the `x-inertia-*` namespace, which Inertia is still adding to, for the reason
|
|
61
|
+
* `MODAL_MARKER_HEADER` is.
|
|
62
|
+
*/
|
|
63
|
+
const MODAL_HELD_HEADER = "stratal-modal-held";
|
|
64
|
+
/** The separator, which no percent-encoded url can contain. */
|
|
65
|
+
const HELD_SEPARATOR = " ";
|
|
66
|
+
/**
|
|
67
|
+
* Held urls as one header value, outermost first.
|
|
68
|
+
*
|
|
69
|
+
* Encoded because a url carries a query and a header value has no way to say where one ends and the
|
|
70
|
+
* next begins.
|
|
71
|
+
*/
|
|
72
|
+
function encodeHeldLevels(urls) {
|
|
73
|
+
return urls.map((url) => encodeURIComponent(url)).join(HELD_SEPARATOR);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The urls back.
|
|
77
|
+
*
|
|
78
|
+
* An absent or unreadable header is read as nothing held rather than as an error: a tab open across
|
|
79
|
+
* a deploy can send an older shape, and the honest answer to "what does this client hold" is then
|
|
80
|
+
* "nothing I can trust", which lands on exactly the behaviour that existed before the header.
|
|
81
|
+
*/
|
|
82
|
+
function decodeHeldLevels(header) {
|
|
83
|
+
if (header === null || header === "") return [];
|
|
84
|
+
try {
|
|
85
|
+
return header.split(HELD_SEPARATOR).filter((part) => part !== "").map((part) => decodeURIComponent(part));
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
export { MODAL_PROP as a, isModalData as c, MODAL_MARKER_HEADER as i, isModalPropPath as l, MODAL_DOCUMENT_HEADER as n, decodeHeldLevels as o, MODAL_HELD_HEADER as r, encodeHeldLevels as s, MODAL_BENEATH_PROP as t, modalPropPath as u };
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=wire-BNVvmku4.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-BNVvmku4.mjs","names":[],"sources":["../src/core/wire.ts"],"sourcesContent":["/**\n * The contract between the server and the browser.\n *\n * Framework-free, so both bundles can import it without pulling the other's machinery in behind it.\n */\n\n/** The page prop one modal travels on. */\nexport const MODAL_PROP = 'modal'\n\n/**\n * The levels beneath the requested one.\n *\n * Present only on a document response, which is the one case with no mounted page to graft onto.\n * An Inertia visit never carries it.\n */\nexport const MODAL_BENEATH_PROP = 'modalBeneath'\n\n/**\n * Marks a response as a modal, so the client grafts it onto the page it already holds.\n *\n * On the RESPONSE rather than derived from request headers: a redirect drops whatever the original\n * visit was doing, and a modal reached by redirect must still graft.\n *\n * Outside the `x-inertia-*` namespace, which Inertia is still adding to — a silent collision there\n * would present as a partial reload returning the wrong props.\n */\nexport const MODAL_MARKER_HEADER = 'stratal-modal'\n\n/** Set on the server's own background sub-request, so a modal route can tell it is a background. */\nexport const MODAL_DOCUMENT_HEADER = 'stratal-modal-document'\n\n/**\n * One modal. A response never carries more than one.\n *\n * `TProps` is open so a caller reading a level back — a test asserting what a route put in a sheet —\n * names the props it expects instead of narrowing an untyped record at the call site.\n */\nexport interface ModalData<TProps = Record<string, unknown>> {\n component: string\n props: TProps\n /** This level's own address — pathname plus search. */\n url: string\n /** What sits beneath it: a page route, or another modal route. */\n base: string\n /** Where closing lands. Fixed when the level opens, never re-derived. */\n close: string\n}\n\n/**\n * Where one of this level's props lives on the page object.\n *\n * No key: a modal response carries exactly one modal, so there is nothing to disambiguate. A\n * targeted reload names this path and visits the level's own url, and the client knows which level\n * it belongs to because it issued the request from there.\n */\nexport function modalPropPath(prop: string): string {\n return `${MODAL_PROP}.props.${prop}`\n}\n\n/** Whether a partial-request prop name addresses the modal rather than the page beneath it. */\nexport function isModalPropPath(name: string): boolean {\n return name === MODAL_PROP || name.startsWith(`${MODAL_PROP}.`)\n}\n\n/**\n * Whether a value is a level this build understands.\n *\n * A tab open across a deploy can hold a payload from an older shape. Refusing it is what keeps the\n * client from rendering a level it cannot address.\n */\nexport function isModalData(value: unknown): value is ModalData {\n if (value === null || typeof value !== 'object') return false\n\n const candidate = value as Partial<ModalData>\n return (\n typeof candidate.component === 'string'\n && typeof candidate.url === 'string'\n && typeof candidate.base === 'string'\n && typeof candidate.close === 'string'\n && typeof candidate.props === 'object'\n && candidate.props !== null\n )\n}\n\n/**\n * The levels the client has open, sent on every modal visit.\n *\n * A level is not a place to land on. Without this the server can only see the `Referer`, which on a\n * visit made from a sheet is that sheet, and a level that takes it aims itself back at the sheet\n * the student is leaving.\n *\n * Outside the `x-inertia-*` namespace, which Inertia is still adding to, for the reason\n * `MODAL_MARKER_HEADER` is.\n */\nexport const MODAL_HELD_HEADER = 'stratal-modal-held'\n\n/** The separator, which no percent-encoded url can contain. */\nconst HELD_SEPARATOR = ' '\n\n/**\n * Held urls as one header value, outermost first.\n *\n * Encoded because a url carries a query and a header value has no way to say where one ends and the\n * next begins.\n */\nexport function encodeHeldLevels(urls: readonly string[]): string {\n return urls.map((url) => encodeURIComponent(url)).join(HELD_SEPARATOR)\n}\n\n/**\n * The urls back.\n *\n * An absent or unreadable header is read as nothing held rather than as an error: a tab open across\n * a deploy can send an older shape, and the honest answer to \"what does this client hold\" is then\n * \"nothing I can trust\", which lands on exactly the behaviour that existed before the header.\n */\nexport function decodeHeldLevels(header: string | null): string[] {\n if (header === null || header === '') return []\n\n try {\n return header\n .split(HELD_SEPARATOR)\n .filter((part) => part !== '')\n .map((part) => decodeURIComponent(part))\n }\n catch {\n return []\n }\n}\n"],"mappings":";;;;;;;AAOA,MAAa,aAAa;;;;;;;AAQ1B,MAAa,qBAAqB;;;;;;;;;;AAWlC,MAAa,sBAAsB;;AAGnC,MAAa,wBAAwB;;;;;;;;AA0BrC,SAAgB,cAAc,MAAsB;CAClD,OAAO,GAAG,WAAW,SAAS;AAChC;;AAGA,SAAgB,gBAAgB,MAAuB;CACrD,OAAO,SAAA,WAAuB,KAAK,WAAW,QAAgB;AAChE;;;;;;;AAQA,SAAgB,YAAY,OAAoC;CAC9D,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CAExD,MAAM,YAAY;CAClB,OACE,OAAO,UAAU,cAAc,YAC5B,OAAO,UAAU,QAAQ,YACzB,OAAO,UAAU,SAAS,YAC1B,OAAO,UAAU,UAAU,YAC3B,OAAO,UAAU,UAAU,YAC3B,UAAU,UAAU;AAE3B;;;;;;;;;;;AAYA,MAAa,oBAAoB;;AAGjC,MAAM,iBAAiB;;;;;;;AAQvB,SAAgB,iBAAiB,MAAiC;CAChE,OAAO,KAAK,KAAK,QAAQ,mBAAmB,GAAG,CAAC,CAAC,CAAC,KAAK,cAAc;AACvE;;;;;;;;AASA,SAAgB,iBAAiB,QAAiC;CAChE,IAAI,WAAW,QAAQ,WAAW,IAAI,OAAO,CAAC;CAE9C,IAAI;EACF,OAAO,OACJ,MAAM,cAAc,CAAC,CACrB,QAAQ,SAAS,SAAS,EAAE,CAAC,CAC7B,KAAK,SAAS,mBAAmB,IAAI,CAAC;CAC3C,QACM;EACJ,OAAO,CAAC;CACV;AACF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/core/wire.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The contract between the server and the browser.
|
|
4
|
+
*
|
|
5
|
+
* Framework-free, so both bundles can import it without pulling the other's machinery in behind it.
|
|
6
|
+
*/
|
|
7
|
+
/** The page prop one modal travels on. */
|
|
8
|
+
declare const MODAL_PROP = "modal";
|
|
9
|
+
/**
|
|
10
|
+
* The levels beneath the requested one.
|
|
11
|
+
*
|
|
12
|
+
* Present only on a document response, which is the one case with no mounted page to graft onto.
|
|
13
|
+
* An Inertia visit never carries it.
|
|
14
|
+
*/
|
|
15
|
+
declare const MODAL_BENEATH_PROP = "modalBeneath";
|
|
16
|
+
/**
|
|
17
|
+
* Marks a response as a modal, so the client grafts it onto the page it already holds.
|
|
18
|
+
*
|
|
19
|
+
* On the RESPONSE rather than derived from request headers: a redirect drops whatever the original
|
|
20
|
+
* visit was doing, and a modal reached by redirect must still graft.
|
|
21
|
+
*
|
|
22
|
+
* Outside the `x-inertia-*` namespace, which Inertia is still adding to — a silent collision there
|
|
23
|
+
* would present as a partial reload returning the wrong props.
|
|
24
|
+
*/
|
|
25
|
+
declare const MODAL_MARKER_HEADER = "stratal-modal";
|
|
26
|
+
/** Set on the server's own background sub-request, so a modal route can tell it is a background. */
|
|
27
|
+
declare const MODAL_DOCUMENT_HEADER = "stratal-modal-document";
|
|
28
|
+
/**
|
|
29
|
+
* One modal. A response never carries more than one.
|
|
30
|
+
*
|
|
31
|
+
* `TProps` is open so a caller reading a level back — a test asserting what a route put in a sheet —
|
|
32
|
+
* names the props it expects instead of narrowing an untyped record at the call site.
|
|
33
|
+
*/
|
|
34
|
+
interface ModalData<TProps = Record<string, unknown>> {
|
|
35
|
+
component: string;
|
|
36
|
+
props: TProps;
|
|
37
|
+
/** This level's own address — pathname plus search. */
|
|
38
|
+
url: string;
|
|
39
|
+
/** What sits beneath it: a page route, or another modal route. */
|
|
40
|
+
base: string;
|
|
41
|
+
/** Where closing lands. Fixed when the level opens, never re-derived. */
|
|
42
|
+
close: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Where one of this level's props lives on the page object.
|
|
46
|
+
*
|
|
47
|
+
* No key: a modal response carries exactly one modal, so there is nothing to disambiguate. A
|
|
48
|
+
* targeted reload names this path and visits the level's own url, and the client knows which level
|
|
49
|
+
* it belongs to because it issued the request from there.
|
|
50
|
+
*/
|
|
51
|
+
declare function modalPropPath(prop: string): string;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { ModalData as a, MODAL_PROP as i, MODAL_DOCUMENT_HEADER as n, modalPropPath as o, MODAL_MARKER_HEADER as r, MODAL_BENEATH_PROP as t };
|
|
54
|
+
//# sourceMappingURL=wire-CbwmWkPr.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-CbwmWkPr.d.mts","names":[],"sources":["../src/core/wire.ts"],"mappings":";;;;;;;cAOa;;;;;;;cAQA;;;;;;;;;;cAWA;;cAGA;;;;;;;UAQI,UAAU,SAAS;EAClC;EACA,OAAO;;EAEP;;EAEA;;EAEA;;;;;;;;;iBAUc,cAAc"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratal/inertia-modal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Modal page primitive for Stratal Inertia — backend-driven modal dialogs",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": [
|
|
7
|
+
"./dist/index.mjs",
|
|
8
|
+
"./dist/testing.mjs"
|
|
9
|
+
],
|
|
6
10
|
"license": "MIT",
|
|
7
|
-
"author":
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Temitayo Fadojutimi",
|
|
13
|
+
"url": "https://x.com/adesege_"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://stratal.dev",
|
|
8
16
|
"engines": {
|
|
9
17
|
"node": ">=22.0.0"
|
|
10
18
|
},
|
|
@@ -33,19 +41,26 @@
|
|
|
33
41
|
"types": "./dist/react.d.mts",
|
|
34
42
|
"import": "./dist/react.mjs"
|
|
35
43
|
},
|
|
44
|
+
"./testing": {
|
|
45
|
+
"types": "./dist/testing.d.mts",
|
|
46
|
+
"import": "./dist/testing.mjs"
|
|
47
|
+
},
|
|
36
48
|
"./package.json": "./package.json"
|
|
37
49
|
},
|
|
38
50
|
"scripts": {
|
|
39
51
|
"build": "tsdown",
|
|
40
|
-
"typecheck": "tsc --noEmit"
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run"
|
|
41
54
|
},
|
|
42
55
|
"peerDependencies": {
|
|
43
56
|
"@inertiajs/core": ">=3",
|
|
44
57
|
"@inertiajs/react": ">=3",
|
|
45
|
-
"@stratal/inertia": ">=0.
|
|
58
|
+
"@stratal/inertia": ">=0.1.1",
|
|
59
|
+
"@stratal/testing": ">=0.1.1",
|
|
46
60
|
"hono": ">=4",
|
|
47
61
|
"react": ">=19",
|
|
48
|
-
"stratal": ">=0.
|
|
62
|
+
"stratal": ">=0.1.1",
|
|
63
|
+
"vitest": ">=4"
|
|
49
64
|
},
|
|
50
65
|
"peerDependenciesMeta": {
|
|
51
66
|
"@inertiajs/core": {
|
|
@@ -54,21 +69,30 @@
|
|
|
54
69
|
"@inertiajs/react": {
|
|
55
70
|
"optional": true
|
|
56
71
|
},
|
|
72
|
+
"@stratal/testing": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
57
75
|
"react": {
|
|
58
76
|
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"vitest": {
|
|
79
|
+
"optional": true
|
|
59
80
|
}
|
|
60
81
|
},
|
|
61
82
|
"devDependencies": {
|
|
62
|
-
"@cloudflare/workers-types": "
|
|
63
|
-
"@inertiajs/core": "^3.
|
|
64
|
-
"@inertiajs/react": "^3.
|
|
65
|
-
"@stratal/inertia": "
|
|
66
|
-
"@
|
|
67
|
-
"@types/
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
83
|
+
"@cloudflare/workers-types": "5.20260919.1",
|
|
84
|
+
"@inertiajs/core": "^3.7.1",
|
|
85
|
+
"@inertiajs/react": "^3.7.1",
|
|
86
|
+
"@stratal/inertia": "0.1.1",
|
|
87
|
+
"@stratal/testing": "0.1.1",
|
|
88
|
+
"@types/node": "^26.6.2",
|
|
89
|
+
"@types/react": "^19.3.0",
|
|
90
|
+
"hono": "^4.13.8",
|
|
91
|
+
"react": "^19.3.0",
|
|
92
|
+
"react-dom": "^19.3.0",
|
|
93
|
+
"stratal": "0.1.1",
|
|
94
|
+
"tsdown": "^0.23.0",
|
|
95
|
+
"typescript": "^7.0.2",
|
|
96
|
+
"vitest": "~4.1.11"
|
|
73
97
|
}
|
|
74
|
-
}
|
|
98
|
+
}
|