{"_id":"@affino/dialog-core","_rev":"2-e972de72c1cf929e47c5020b5ed334ad","name":"@affino/dialog-core","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@affino/dialog-core","version":"1.0.0","keywords":["dialog","modal","overlay","focus-management","accessibility","async-guard","headless","state-machine","ui-core"],"author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"license":"MIT","_id":"@affino/dialog-core@1.0.0","maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"homepage":"https://affino.dev","bugs":{"url":"https://github.com/affinio/affinio/issues"},"dist":{"shasum":"f3ebcd8c1ff4bc9b37f165be6906df434c37c7d5","tarball":"https://registry.npmjs.org/@affino/dialog-core/-/dialog-core-1.0.0.tgz","fileCount":15,"integrity":"sha512-tMVPkd2uvHWBonMMeiqZy9RuhcKEkEvlsrpbcc8LbQYAHOfbizgVHmj7+A8Tqs6x8ys6CznIJgrNtFBpCHSYkQ==","signatures":[{"sig":"MEUCIQCZZVTQgGtPE2odLY7vwuWAByMcxs+ikJAKd/v8UH1iLwIgH1UWrpGsEmCzZVcBA55cIaOW0WS2Yc5auH+mC1NakLo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37963},"main":"dist/index.js","type":"module","_from":"file:affino-dialog-core-1.0.0.tgz","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc -p tsconfig.json"},"_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"_resolved":"/tmp/d8815dc57a26497106e3c42783e47c79/affino-dialog-core-1.0.0.tgz","_integrity":"sha512-tMVPkd2uvHWBonMMeiqZy9RuhcKEkEvlsrpbcc8LbQYAHOfbizgVHmj7+A8Tqs6x8ys6CznIJgrNtFBpCHSYkQ==","repository":{"url":"git+https://github.com/affinio/affinio.git#main","type":"git"},"_npmVersion":"10.8.2","description":"Headless dialog/overlay engine with async guards, focus orchestration, and mobile keyboard resilience","directories":{},"sideEffects":false,"_nodeVersion":"20.19.6","dependencies":{"@affino/surface-core":"^1.0.0"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.15","@vitest/coverage-v8":"^4.0.15"},"_npmOperationalInternal":{"tmp":"tmp/dialog-core_1.0.0_1769943220225_0.42015867170704","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@affino/dialog-core","version":"1.1.0","author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"type":"module","description":"Headless dialog/overlay engine with async guards, focus orchestration, and mobile keyboard resilience","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"main":"dist/index.js","types":"dist/index.d.ts","sideEffects":false,"dependencies":{"@affino/surface-core":"^1.0.0"},"repository":{"type":"git","url":"git+https://github.com/affinio/affinio.git#main"},"homepage":"https://affino.dev","keywords":["dialog","modal","overlay","focus-management","accessibility","async-guard","headless","state-machine","ui-core"],"license":"MIT","devDependencies":{"@vitest/coverage-v8":"^4.0.15","vitest":"^4.0.15"},"scripts":{"build":"tsc -p tsconfig.json","test":"vitest run"},"_id":"@affino/dialog-core@1.1.0","bugs":{"url":"https://github.com/affinio/affinio/issues"},"_integrity":"sha512-fmXJ2qN0q6A6kLszfEv55IKop+cVpAwf0kSjF1uBaxnDzLdhjzwtr56Q6sQJtXtujOdfiX/APKHU3oafo9cgnw==","_resolved":"/tmp/ea93bb42677ff9c1f679df95330238cb/affino-dialog-core-1.1.0.tgz","_from":"file:affino-dialog-core-1.1.0.tgz","_nodeVersion":"20.19.6","_npmVersion":"10.8.2","dist":{"integrity":"sha512-fmXJ2qN0q6A6kLszfEv55IKop+cVpAwf0kSjF1uBaxnDzLdhjzwtr56Q6sQJtXtujOdfiX/APKHU3oafo9cgnw==","shasum":"62ea40f629084bc30cc83112871ca4a27e0f5f15","tarball":"https://registry.npmjs.org/@affino/dialog-core/-/dialog-core-1.1.0.tgz","fileCount":15,"unpackedSize":40131,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHN7WOCa+y7isIh8jt6yxpvGjl2WID6SnnSvcGBjPPlbAiAjaLCRh0XPnQkOqpINCN4aY6eJHts08RS6msjH04+1ZA=="}]},"_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"directories":{},"maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dialog-core_1.1.0_1770071003585_0.5603911157255792"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-01T10:53:40.125Z","modified":"2026-02-02T22:23:23.854Z","1.0.0":"2026-02-01T10:53:40.386Z","1.1.0":"2026-02-02T22:23:23.741Z"},"bugs":{"url":"https://github.com/affinio/affinio/issues"},"author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"license":"MIT","homepage":"https://affino.dev","keywords":["dialog","modal","overlay","focus-management","accessibility","async-guard","headless","state-machine","ui-core"],"repository":{"type":"git","url":"git+https://github.com/affinio/affinio.git#main"},"description":"Headless dialog/overlay engine with async guards, focus orchestration, and mobile keyboard resilience","maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"readme":"# @affino/dialog-core\n\nHeadless dialog engine that coordinates lifecycle hooks, focus scopes, async close guards, and overlay stacking across frameworks.\n\n## Highlights\n\n- Deterministic state machine (`idle → opening → open → closing → closed`).\n- Blocking **and** optimistic guard strategies with pending-attempt telemetry.\n- Overlay interaction matrix (`dialog` vs `sheet`) plus registrar hooks.\n- Focus orchestration contract for portals, scroll locks, or custom traps.\n- Pure TypeScript, zero DOM dependencies, easy to adapt to Vue/React/Livewire.\n\n## Installation\n\n```bash\npnpm add @affino/dialog-core\n# or\nnpm install @affino/dialog-core\n```\n\n## Quick start\n\n```ts\nimport { DialogController } from \"@affino/dialog-core\"\n\nconst controller = new DialogController({\n\toverlayKind: \"dialog\",\n\tcloseStrategy: \"blocking\",\n\tlifecycle: {\n\t\tafterOpen: () => console.log(\"opened\"),\n\t\tafterClose: () => console.log(\"closed\"),\n\t},\n})\n\ncontroller.subscribe((snapshot) => {\n\tconsole.log(snapshot.phase, snapshot.isGuardPending)\n})\n\ncontroller.open(\"keyboard\")\nawait controller.close(\"programmatic\")\n```\n\n### Snapshot contract\n\nEach subscriber receives a `DialogSnapshot`:\n\n```ts\ntype DialogSnapshot = {\n\tphase: \"idle\" | \"opening\" | \"open\" | \"closing\" | \"closed\"\n\tisOpen: boolean\n\tisGuardPending: boolean\n\tlastCloseReason?: DialogCloseReason\n\tguardMessage?: string\n\toptimisticCloseInFlight: boolean\n\toptimisticCloseReason?: DialogCloseReason\n\tpendingCloseAttempts: number\n\tpendingNavigationMessage?: string\n}\n```\n\nUse it to drive UI chrome (`data-phase` attributes, overlays, aria messaging) in frameworks or vanilla DOM.\n\n## Guard strategies\n\n`setCloseGuard()` wires custom logic that can allow or deny close requests. Choose the strategy per call:\n\n```ts\ncontroller.setCloseGuard(async ({ reason, metadata }) => {\n\tconst hasUnsavedChanges = await checkDraft(metadata?.draftId)\n\treturn hasUnsavedChanges ? { outcome: \"deny\", message: \"Save draft first\" } : { outcome: \"allow\" }\n})\n\n// Blocking (default): keep dialog open until guard resolves.\nawait controller.close(\"programmatic\", { strategy: \"blocking\" })\n\n// Optimistic: play close animation immediately, reopen if guard denies.\nawait controller.close(\"programmatic\", { strategy: \"optimistic\" })\n```\n\n```\nopen → [close requested]\n\t\t\t\t│\n\t\t\t\t├─ blocking ── wait ── allow ── closing ── closed\n\t\t\t\t│                       └─ deny ── open (message)\n\t\t\t\t│\n\t\t\t\t└─ optimistic ─ closing ─ allow ─ closed\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t└─ deny ─ open (message)\n```\n\n- `pendingNavigationMessage` surfaces copy such as “Saving changes…” whenever a guard is resolving.\n- Repeated `close()` calls while a guard is pending increment `pendingCloseAttempts`. Hook `onPendingCloseAttempt` or `onPendingCloseLimitReached` to respond to ESC storms.\n\n## Overlay stacking rules\n\n`OverlayInteractionMatrix` encodes how dialogs and sheets interact by default:\n\n| Source → Target | Can stack? | Close strategy |\n| --- | --- | --- |\n| dialog → dialog | ✅ | `single` |\n| dialog → sheet | ✅ | `single` |\n| sheet → dialog | ❌ | `cascade` |\n| sheet → sheet | ✅ | `cascade` |\n\nUse the helpers to make open/close decisions before instantiating additional overlays:\n\n```ts\nif (!controller.canStackOver(\"dialog\")) {\n\t// dismiss the sheet first or show a toast\n}\n\nconst strategy = controller.closeStrategyFor(\"dialog\") // \"cascade\" for sheet → dialog\n```\n\nProvide custom rules or telemetry emitters via `interactionMatrix` when embedding into your own overlay manager.\n\n## Focus orchestration\n\nInject a `focusOrchestrator` to centralize focus scopes, return targets, or iOS soft-keyboard fixes:\n\n```ts\nconst controller = new DialogController({\n\tfocusOrchestrator: {\n\t\tactivate: ({ reason }) => trap.activate(reason),\n\t\tdeactivate: ({ reason }) => trap.deactivate(reason),\n\t},\n})\n```\n\nThe controller only calls `activate` once per open cycle and automatically invokes `deactivate` when closing or when `destroy()` is called.\n\n## API surface\n\n### Constructor options\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `defaultOpen` | `boolean` | Start in the `open` phase (SSR previews/testing). |\n| `overlayKind` | `\"dialog\" \\| \"sheet\"` | Drive stacking decisions for this controller. |\n| `interactionMatrix` | `OverlayInteractionMatrixConfig` | Override stacking rules or attach telemetry. |\n| `closeStrategy` | `\"blocking\" \\| \"optimistic\"` | Default guard strategy (per-close overrides allowed). |\n| `pendingNavigationMessage` | `string` | Copy shown while a guard is resolving. |\n| `maxPendingAttempts` | `number` | ESC spam ceiling before firing `onPendingCloseLimitReached`. |\n| `lifecycle` | `DialogLifecycleHooks` | `before/after` open + close callbacks. |\n| `focusOrchestrator` | `DialogFocusOrchestrator` | Hook to your focus trap/return target logic. |\n| `overlayRegistrar` | `OverlayRegistrar` | Bridge into an external overlay manager (must expose `register` + `isTopMost`). |\n| `onSnapshot` | `(snapshot) => void` | Shortcut subscription invoked immediately + on change. |\n| `onPendingCloseAttempt` | `(info) => void` | Called every time a guard is already pending and a new request arrives. |\n| `onPendingCloseLimitReached` | `(info) => void` | Fired once per pending cycle when attempts reach the configured limit. |\n\n### Methods\n\n| Method | Description |\n| --- | --- |\n| `open(reason?)` | Transition to `opening → open`, run lifecycle hooks, and activate focus orchestration. |\n| `close(reason?, options?)` | Request a close with optional guard metadata/strategy. Returns `Promise<boolean>`. |\n| `setCloseGuard(fn)` | Provide async/sync guard logic (resolve `{ outcome: \"allow\" }` or `{ outcome: \"deny\", message }`). |\n| `subscribe(listener)` | Receive snapshots; returns an unsubscribe function. |\n| `on(event, listener)` | Listen to `phase-change`, `open`, `close`, `overlay-registered`, `overlay-unregistered`. |\n| `registerOverlay(registration)` | Relay to an external registrar and emit overlay events; returns disposer. |\n| `canHandleClose(reason)` | Returns `true` if the controller is allowed to process the close request (top-most checks happen automatically). |\n| `canStackOver(kind)` / `closeStrategyFor(kind)` | Consult interaction matrix before stacking new overlays. |\n| `getPendingCloseAttempts()` | Inspect how many retries happened during the active guard. |\n| `destroy(reason?)` | Clear subscribers, event listeners, guard state, and deactivate focus orchestration. |\n\n## Scripts\n\n- `pnpm build` — compile TypeScript output.\n- `pnpm test` — run Vitest suite with coverage.\n\n## Supporting docs\n\n- Implementation notes live in [`docs/dialog-implementation-plan.md`](../../docs/dialog-implementation-plan.md).\n- Livewire adapter ideas live in [`docs/dialog-livewire.md`](../../docs/dialog-livewire.md).\n","readmeFilename":"README.md"}