{"_id":"@affino/dialog-vue","_rev":"3-12ed8151df3dbfdce72bb9382d1a5990","name":"@affino/dialog-vue","dist-tags":{"latest":"1.3.0"},"versions":{"1.0.0":{"name":"@affino/dialog-vue","version":"1.0.0","keywords":["vue","dialog","overlay","focus-management","headless","controller"],"author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"license":"MIT","_id":"@affino/dialog-vue@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":"8fbf5837b66cf4f92b07c5081709cf15d0f8c218","tarball":"https://registry.npmjs.org/@affino/dialog-vue/-/dialog-vue-1.0.0.tgz","fileCount":18,"integrity":"sha512-o78Rm6ohFYy/NV5Db+QsEe8Hkf3vprQyjhilrqMYb/HDF4b+36fDP4LYf6imsfftbsFEqxtxYSmiRbqLehruGg==","signatures":[{"sig":"MEUCIC5XnDEjBxREDVWNN7Kbd2mWqqw/Sq9R2TNE3hjSHlk5AiEAnEk9VLlsCHa0OTJUgedTUD4cco+x5NUMs3C3pFxgcKM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":19790},"main":"dist/index.js","type":"module","_from":"file:affino-dialog-vue-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 --build tsconfig.json"},"_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"_resolved":"/tmp/6825231e98c48af01bb61d72ed11eaab/affino-dialog-vue-1.0.0.tgz","_integrity":"sha512-o78Rm6ohFYy/NV5Db+QsEe8Hkf3vprQyjhilrqMYb/HDF4b+36fDP4LYf6imsfftbsFEqxtxYSmiRbqLehruGg==","repository":{"url":"git+https://github.com/affinio/affinio.git#main","type":"git"},"_npmVersion":"10.8.2","description":"Vue 3 composables for the @affino/dialog-core controller","directories":{},"sideEffects":false,"_nodeVersion":"20.19.6","dependencies":{"@affino/dialog-core":"^1.0.0"},"_hasShrinkwrap":false,"devDependencies":{"vue":"^3.4.0","jsdom":"^27.3.0","vitest":"^4.0.15","@vue/test-utils":"^2.4.6","@vitejs/plugin-vue":"^6.0.3","@vitest/coverage-v8":"^4.0.15","@testing-library/vue":"^8.1.0"},"peerDependencies":{"vue":"^3.3.0"},"_npmOperationalInternal":{"tmp":"tmp/dialog-vue_1.0.0_1769943236035_0.5166664051148955","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@affino/dialog-vue","version":"1.1.0","keywords":["vue","dialog","overlay","focus-management","headless","controller"],"author":{"name":"Anton Pavlov","email":"a.pavlov@affino.dev"},"license":"MIT","_id":"@affino/dialog-vue@1.1.0","maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"homepage":"https://affino.dev","bugs":{"url":"https://github.com/affinio/affinio/issues"},"dist":{"shasum":"00aecd181404227b2d80061fcd3402b9f24cd5fa","tarball":"https://registry.npmjs.org/@affino/dialog-vue/-/dialog-vue-1.1.0.tgz","fileCount":18,"integrity":"sha512-80MKrX9H+YQ/5v3otx6SDGV/ew6CxsAVKwHOAFLESlTR7+ZqsFmJ1iTm2eNWXeplYr6Okn58Q+xKEd9/9lloow==","signatures":[{"sig":"MEQCIES5oqNuAB4DuZtN8AMsVimd4A6FmbOeFQ3LxJTMQV3fAiBO7yc8DU8zPUpBACbUhVRhUMxUxUuYR6uG3K47GQAD/g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":19790},"main":"dist/index.js","type":"module","_from":"file:affino-dialog-vue-1.1.0.tgz","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc --build tsconfig.json"},"_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"_resolved":"/tmp/b6a200b18ff9832f6af70896543fab58/affino-dialog-vue-1.1.0.tgz","_integrity":"sha512-80MKrX9H+YQ/5v3otx6SDGV/ew6CxsAVKwHOAFLESlTR7+ZqsFmJ1iTm2eNWXeplYr6Okn58Q+xKEd9/9lloow==","repository":{"url":"git+https://github.com/affinio/affinio.git#main","type":"git"},"_npmVersion":"10.8.2","description":"Vue 3 composables for the @affino/dialog-core controller","directories":{},"sideEffects":false,"_nodeVersion":"20.19.6","dependencies":{"@affino/dialog-core":"^1.1.0"},"_hasShrinkwrap":false,"devDependencies":{"vue":"^3.4.0","jsdom":"^27.3.0","vitest":"^4.0.15","@vue/test-utils":"^2.4.6","@vitejs/plugin-vue":"^6.0.3","@vitest/coverage-v8":"^4.0.15","@testing-library/vue":"^8.1.0"},"peerDependencies":{"vue":"^3.3.0"},"_npmOperationalInternal":{"tmp":"tmp/dialog-vue_1.1.0_1770071005226_0.8786597844231177","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"_id":"@affino/dialog-vue@1.3.0","bugs":{"url":"https://github.com/affinio/affinio/issues"},"dist":{"shasum":"30ed6fce3c215b22afd8aa42ee7d409431cf1fb5","tarball":"https://registry.npmjs.org/@affino/dialog-vue/-/dialog-vue-1.3.0.tgz","fileCount":14,"integrity":"sha512-I35R57u9+oM9FhMtYY1odz7+aOHD5JCA0yySugHj7SLT87rcq26sH8WeH+WGnCFye82iLEzBD8dg7AoItV/KUw==","signatures":[{"sig":"MEUCIQCNHsrBHXtnHABTXzc5Wzx5xqBbNPuM+Igiwmb5HyoTsgIgUiURfFston09oAwYzbw0urTMDbiIdqx3kEUXFnTlibk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDnr5yOvf74blS41QYR0CuMvF6YQKdif7BvUYQ1GCz/sQIgDiovZCuOHCKsz1NchDrYFkIFpm4T0rf41mqcYLcadXY="}],"unpackedSize":22026},"main":"dist/index.js","name":"@affino/dialog-vue","type":"module","types":"dist/index.d.ts","author":"Anton Pavlov <a.pavlov@affino.dev>","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"license":"MIT","scripts":{"test":"vitest run","build":"tsc --build tsconfig.json"},"version":"1.3.0","_npmUser":{"name":"affino","email":"anton.pavlov.personal@gmail.com"},"homepage":"https://github.com/affinio/affinio/tree/main/packages/dialog-vue#readme","keywords":["vue","dialog","overlay","focus-management","headless","controller"],"repository":{"url":"git+https://github.com/affinio/affinio.git","type":"git","directory":"packages/dialog-vue"},"description":"Vue adapter for @affino/dialog-core","directories":{},"maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"sideEffects":false,"dependencies":{"@affino/dialog-core":"^1.3.0","@affino/overlay-kernel":"^0.2.0"},"_hasShrinkwrap":false,"devDependencies":{"vue":"^3.4.0","jsdom":"^27.3.0","vitest":"^4.0.15","@vue/test-utils":"^2.4.6","@vitejs/plugin-vue":"^6.0.3","@vitest/coverage-v8":"^4.0.15","@testing-library/vue":"^8.1.0"},"peerDependencies":{"vue":"^3.3.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/dialog-vue_1.3.0_1789834344207_0.27108914044112664"}}},"time":{"created":"2026-02-01T10:53:55.924Z","modified":"2026-09-19T16:12:24.489Z","1.0.0":"2026-02-01T10:53:56.177Z","1.1.0":"2026-02-02T22:23:25.379Z","1.3.0":"2026-09-19T16:12:24.304Z"},"bugs":{"url":"https://github.com/affinio/affinio/issues"},"author":"Anton Pavlov <a.pavlov@affino.dev>","license":"MIT","homepage":"https://github.com/affinio/affinio/tree/main/packages/dialog-vue#readme","keywords":["vue","dialog","overlay","focus-management","headless","controller"],"repository":{"url":"git+https://github.com/affinio/affinio.git","type":"git","directory":"packages/dialog-vue"},"description":"Vue adapter for @affino/dialog-core","maintainers":[{"name":"affino","email":"anton.pavlov.personal@gmail.com"}],"readme":"# @affino/dialog-vue\n\nVue 3 bindings for [`@affino/dialog-core`](../dialog-core) with focus orchestration, async guards, and overlay-kernel integration.\n\n## Why use it\n\n- **State machine quality** – identical controller used across Vue, React, and Livewire adapters.\n- **Focus orchestration** – initial focus, mount retries, and focus return are handled by the helper; modal Tab trapping remains a host responsibility.\n- **Async-friendly** – optimistic closes, guard hooks, and retry budgets are one option away.\n- **Stack aware** – controllers cooperate so ESC/backdrop close only the top-most surface.\n- **Headless by design** – rendering, inert siblings, scroll locking, gestures, and modal Tab trapping remain host responsibilities.\n\n## Installation\n\n```bash\npnpm add @affino/dialog-vue @affino/dialog-core\n# or npm / yarn if you prefer\n```\n\nYou need Vue 3.4+ (Composition API) available in your project.\n\n## Quick start\n\n1. **Create a dialog host when using Teleport.** The package does not append or manage a DOM host; add it to your HTML shell for SSR:\n\n```html\n<body>\n  <div id=\"app\"></div>\n  <div id=\"affino-dialog-host\" data-affino-dialog-host=\"true\"></div>\n</body>\n```\n\n2. **Wire the controller inside a component.**\n\n```vue\n<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { useDialogController, createDialogFocusOrchestrator } from \"@affino/dialog-vue\"\n\nconst triggerRef = ref<HTMLElement | null>(null)\nconst dialogRef = ref<HTMLDivElement | null>(null)\nconst FOCUSABLE_SELECTOR =\n  'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n\nconst focusOrchestrator = createDialogFocusOrchestrator({\n  dialog: () => dialogRef.value,\n  initialFocus: () => dialogRef.value?.querySelector<HTMLElement>(\"[data-dialog-initial]\"),\n  returnFocus: () => triggerRef.value,\n})\n\nconst dialog = useDialogController({ focusOrchestrator })\n\nfunction loopFocus(edge: \"start\" | \"end\") {\n  const container = dialogRef.value\n  if (!container) return\n  const nodes = Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR))\n  if (!nodes.length) {\n    container.focus()\n    return\n  }\n  const target = edge === \"start\" ? nodes[0] : nodes[nodes.length - 1]\n  target?.focus()\n}\n</script>\n\n<template>\n  <button ref=\"triggerRef\" class=\"cta\" @click=\"dialog.open('trigger')\">Launch dialog</button>\n\n  <Teleport to=\"#affino-dialog-host\">\n    <transition name=\"dialog-layer\">\n      <div v-if=\"dialog.snapshot.isOpen\" class=\"overlay\" @click.self=\"dialog.close('backdrop')\">\n        <div ref=\"dialogRef\" class=\"surface\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"demo-title\" tabindex=\"-1\">\n          <span class=\"sr-only\" tabindex=\"0\" aria-hidden=\"true\" @focus=\"loopFocus('end')\" />\n          <h2 id=\"demo-title\">Example dialog</h2>\n          <p>Drop in your content here.</p>\n          <button class=\"ghost\" data-dialog-initial @click=\"dialog.close('primary-action')\">Apply</button>\n          <button class=\"text\" @click=\"dialog.close('cancel')\">Cancel</button>\n          <span class=\"sr-only\" tabindex=\"0\" aria-hidden=\"true\" @focus=\"loopFocus('start')\" />\n        </div>\n      </div>\n    </transition>\n  </Teleport>\n</template>\n\n```css\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0 0 0 0);\n  white-space: nowrap;\n  border: 0;\n}\n```\n```\n\n> The Teleport host keeps z-index predictable and prevents stacking context clashes.\n\n3. **Bring your own styles and modal behavior.** The package is headless, so you can rely on Tailwind, UnoCSS, CSS Modules, or an application modal primitive.\n\n## Overlay kernel integration\n\n`useDialogController` automatically registers dialogs with the shared `@affino/overlay-kernel` manager whenever `document` is available. Customize stacking behavior by passing `overlayKind`, `overlayEntryTraits`, `overlayManager`, or `getOverlayManager` through the hook options. During SSR the hook simply defers registration until hydration so servers stay overlay-agnostic.\n\nWhen a Teleport target is mounted or changes after the controller opens, update the manager entry without destroying/re-registering the dialog:\n\n```ts\nconst dialog = useDialogController({ overlayManager })\n\nonMounted(() => {\n  dialog.setOverlayRoot(teleportedSurface.value)\n})\n```\n\nPass `null` when the root is removed. The method is idempotent and safe after disposal; it updates the existing overlay entry so stack order and pending close requests remain intact.\n\n## Adding async guards (optional)\n\n```ts\nconst dialog = useDialogController({\n  focusOrchestrator,\n  closeStrategy: \"optimistic\",\n  maxPendingAttempts: 3,\n})\n\ndialog.controller.setCloseGuard(async ({ metadata }) => {\n  await saveDraft()\n  if (!metadata?.confirm) {\n    return { outcome: \"deny\", message: \"Please confirm\" }\n  }\n  return { outcome: \"allow\" }\n})\n\nfunction requestClose() {\n  dialog.close(\"primary-action\", { metadata: { confirm: true } })\n}\n```\n\n## Nested dialogs & stacks\n\nEvery dialog created with `useDialogController` is independent. If you need cascading overlays:\n\n```ts\nconst stack = ref<DialogBinding[]>([])\n\nfunction openStackLayer() {\n  const surfaceRef = ref<HTMLDivElement | null>(null)\n  const binding = useDialogController({\n    focusOrchestrator: createDialogFocusOrchestrator({\n      dialog: () => surfaceRef.value,\n      returnFocus: () => stack.value.at(-1)?.focusOrchestrator?.dialog() ?? dialogRef.value,\n    }),\n  })\n\n  stack.value.push(binding)\n  binding.open(\"programmatic\")\n}\n```\n\nBecause each controller understands `phase`, `isOpen`, and `optimisticCloseInFlight`, you can always determine which layer should react to ESC/backdrop clicks.\n\n## Accessibility checklist\n\n1. **Label every surface** with `aria-labelledby` or `aria-label`.\n2. **Provide a `data-dialog-initial` focus target** (usually a primary button).\n3. **Implement the modal focus boundary**. The orchestrator does not install a Tab trap or sentinels; use the host pattern shown in the example or an equivalent accessible modal primitive.\n4. **Respect motion preferences** with `prefers-reduced-motion` in your CSS.\n5. **Announce guard status** through `dialog.snapshot.guardMessage` or custom alerts.\n\n## Mobile & Safari notes\n\n- Use a Teleport host and scroll-lock `html`/`body` while any dialog is open to prevent iOS “rubber banding”.\n- Gesture close: listen for vertical swipes and call `binding.close('programmatic')` when the delta exceeds your threshold.\n- When focus disappears (e.g., software keyboard dismissed), call `binding.focusOrchestrator?.focusFirstFocusable()` before the next interaction.\n\n## API reference (summary)\n\n| Hook / helper | Description |\n| --- | --- |\n| `useDialogController(options)` | Returns `{ controller, snapshot, open, close, setOverlayRoot, dispose }`. `snapshot` is a shallow ref with `isOpen`, `phase`, `lastCloseReason`, `optimisticCloseInFlight`, etc. |\n| `createDialogFocusOrchestrator(config)` | Configures dialog/return focus getters plus optional `initialFocus` selector. Returns an object consumed by the controller. |\n| `DialogController` | The core instance; call `controller.on(event, listener)` to subscribe to lifecycle events, `controller.setCloseGuard()` to register async guards, `controller.setOverlayRoot(root)` after Teleport mount, and `controller.dispose()` when the component unmounts. |\n\nSee [`packages/dialog-core`](../dialog-core) for the exhaustive controller documentation.\n\n## Troubleshooting\n\n- **Dialog opens without focus**: ensure the DOM node referenced by `dialog: () => dialogRef.value` exists before calling `open` (e.g., mount via `v-if` before focusing, or rely on the orchestrator’s built-in retry by keeping the getter lazily returning the element).\n- **Escape closes too many overlays**: gate your key handlers with `binding.snapshot.phase === 'opening' || binding.snapshot.isOpen` and track a stack so only the top layer responds.\n- **Scrolling the body underneath on iOS**: add `body[data-affino-scroll-lock=\"true\"] { overscroll-behavior: contain; touch-action: none; }` and toggle a dataset flag while dialogs are active.\n\n## Demo\n\nSee [`demo-vue/src/pages/DialogPage.vue`](../../demo-vue/src/pages/DialogPage.vue) for a production-style implementation featuring guards, menus, tooltips, and nested stacks.\n","readmeFilename":""}