{"_id":"@alexpricedev/billet-dialog","name":"@alexpricedev/billet-dialog","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@alexpricedev/billet-dialog","version":"0.1.0","description":"Standalone, accessible confirm dialog for destructive actions. Zero dependencies, vanilla DOM, scoped CSS tokens, progressive-enhancement form gating. Designed for Billet but framework-agnostic.","type":"module","module":"src/index.ts","types":"src/index.ts","exports":{".":{"types":"./src/index.ts","import":"./src/index.ts","default":"./src/index.ts"},"./styles.css":"./src/styles.css"},"scripts":{"test":"bun test","typecheck":"tsc --noEmit","check":"tsc --noEmit && bun test"},"publishConfig":{"access":"public"},"engines":{"bun":">=1.0.0"},"license":"MIT","author":{"name":"Alex Price"},"repository":{"type":"git","url":"git+https://github.com/alexpricedev/billet-dialog.git"},"bugs":{"url":"https://github.com/alexpricedev/billet-dialog/issues"},"homepage":"https://github.com/alexpricedev/billet-dialog#readme","keywords":["dialog","confirm","modal","alertdialog","accessibility","progressive-enhancement","billet","bun"],"devDependencies":{"@happy-dom/global-registrator":"^20.9.0","@types/bun":"latest","happy-dom":"^15.0.0","typescript":"^5.4.0"},"gitHead":"0d712acfb9e88f8c0ef8b57073b4e1737f31fad6","_id":"@alexpricedev/billet-dialog@0.1.0","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-gqImJ1CeGfLREgpIm6EzMJGlSXiNvGsI52g3L55ql9BuzkqGRI77/aF7dXpMXiT02Oz0UXeM10Uksdp7T6Rlow==","shasum":"f58416c8471650a55dac4c3eba736665fdc1beef","tarball":"https://registry.npmjs.org/@alexpricedev/billet-dialog/-/billet-dialog-0.1.0.tgz","fileCount":9,"unpackedSize":28513,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDjQoMG0Ul27fa6xHrciV3CFSWhUCAUjCOh450VC6AipgIgaxSiYFpHKIgn+0DY1YLXwxXgl9ZZIqpYJRUXDNkR+E0="}]},"_npmUser":{"name":"alexpricedev","email":"npm@alexprice.dev"},"directories":{},"maintainers":[{"name":"alexpricedev","email":"npm@alexprice.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/billet-dialog_0.1.0_1785757512802_0.979085184640109"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-03T11:45:12.632Z","0.1.0":"2026-08-03T11:45:12.936Z","modified":"2026-08-03T11:45:13.132Z"},"maintainers":[{"name":"alexpricedev","email":"npm@alexprice.dev"}],"description":"Standalone, accessible confirm dialog for destructive actions. Zero dependencies, vanilla DOM, scoped CSS tokens, progressive-enhancement form gating. Designed for Billet but framework-agnostic.","homepage":"https://github.com/alexpricedev/billet-dialog#readme","keywords":["dialog","confirm","modal","alertdialog","accessibility","progressive-enhancement","billet","bun"],"repository":{"type":"git","url":"git+https://github.com/alexpricedev/billet-dialog.git"},"author":{"name":"Alex Price"},"bugs":{"url":"https://github.com/alexpricedev/billet-dialog/issues"},"license":"MIT","readme":"# @alexpricedev/billet-dialog\n\nAccessible confirm dialog for destructive actions. Vanilla DOM, zero dependencies, scoped CSS tokens. Gates `<form>` submissions as progressive enhancement — no JavaScript, and the form still posts. Framework-agnostic — designed to drop into Billet but works anywhere.\n\n## Copy this to your coding agent\n\nPaste this into your agent (Claude Code, Cursor, etc.):\n\n```text\nInstall and wire up @alexpricedev/billet-dialog in this repo.\n\n1. Read the package README first:\n   node_modules/@alexpricedev/billet-dialog/README.md\n   (or https://github.com/alexpricedev/billet-dialog#readme).\n   Follow its \"Wire up in 3 steps\" section. Adapt snippets to this\n   repo's template / client-entry signatures where they differ —\n   don't paste blindly.\n\n2. If this repo has a CLAUDE.md or AGENTS.md, read it and respect its\n   conventions (filenames, lint rules, where tests go). Those override\n   anything generic in the package README.\n\n3. Find every destructive action in the app — delete, revoke, cancel,\n   remove, \"reset all\", anything irreversible or anything that breaks\n   a link already shared. List them before you edit. Gate each one by\n   adding data-confirm attributes to its form, per README step 3.\n\n4. Keep the no-JS fallback intact. The form must still post normally\n   when the dialog never loads. Do not convert forms to fetch/XHR,\n   and do not move the guard into a click handler on the button.\n\n5. Apply the README's Theming block: map this repo's existing\n   --color-* / --font-* (or equivalent) tokens to --bd-* on\n   [data-bd-root]. Otherwise the dialog ships in the package's default\n   dark look instead of matching the site.\n\n6. Verify with the repo's check/test commands, plus the /browse skill\n   (or equivalent headless browser tool), for at least one gated\n   action: click destructive button → dialog appears → Cancel leaves\n   the record intact → click again → Confirm actually performs it.\n   Check the server-rendered page still has the plain form in its\n   HTML. Do not scaffold a new browser-test harness — use whatever's\n   already wired in.\n\n7. Open a PR.\n   - Title: \"Confirm before destructive actions\" — but check\n     `git log` first and adapt to the repo's commit convention if it\n     uses one (e.g. conventional commits → `feat(dialog): confirm\n     before destructive actions`). Commit conventions usually live in\n     history, not in CLAUDE.md.\n   - Body: the list of actions you gated + the verification steps you\n     performed.\n   - Screenshot: `gh pr create --body` can't embed local images.\n     Capture the dialog with the browser tool and attach it as a\n     follow-up PR comment (`gh pr comment <n> --body-file …`) or via\n     the GitHub web UI.\n\nOut of scope: replacing native window.confirm() calls in admin-only\ntooling, toast/undo patterns, and any redesign of the actions\nthemselves.\n```\n\n## For AI agents\n\n- **Package**: `@alexpricedev/billet-dialog`\n- **Install**: `bun add @alexpricedev/billet-dialog`\n- **Peer deps**: none\n- **Bundle side effects**: none on import. Appends one `<div data-bd-root>` to `<body>` on first `Dialog.confirm()` / `Dialog.wireForms()` / `createDialog()`\n- **Server runtime**: none — this is a client-only package\n- **Wiring**: 3 client steps — see [Wire up in 3 steps](#wire-up-in-3-steps)\n- **Verify install**: `bun test` in the consumer should still pass; `document.querySelector('[data-bd-root]')` returns a node after the first prompt\n\n## Install\n\n```bash\nbun add @alexpricedev/billet-dialog\n# or: npm install / pnpm add / yarn add\n```\n\n## Wire up in 3 steps\n\n### 1. Import the CSS once\n\nIn your global stylesheet (e.g. `src/client/style.css`):\n\n```css\n@import \"@alexpricedev/billet-dialog/styles.css\";\n```\n\nOr in your client entry, if your bundler handles CSS imports from JS:\n\n```ts\nimport \"@alexpricedev/billet-dialog/styles.css\";\n```\n\n### 2. Turn on form gating\n\nOnce, in your client entry:\n\n```ts\nimport { Dialog } from \"@alexpricedev/billet-dialog\";\n\nDialog.wireForms();\n```\n\nThat attaches one delegated `submit` listener to `document`. Every form carrying a `data-confirm` attribute is gated; every other form is untouched. `wireForms` returns a function that removes the listener, for page-scoped setups:\n\n```ts\nlet unwire: (() => void) | null = null;\n\nexport function init() {\n  unwire = Dialog.wireForms();\n}\n\nexport function cleanup() {\n  unwire?.();\n  unwire = null;\n  Dialog.destroy();\n}\n```\n\n### 3. Mark the destructive forms\n\nServer-render the form exactly as before, plus the attributes:\n\n```html\n<form method=\"post\" action=\"/lists/abc/delete\"\n      data-confirm=\"Deleting this list is permanent, and its link will stop working for anyone you've shared it with.\"\n      data-confirm-title=\"Delete this list?\"\n      data-confirm-label=\"Delete list\">\n  <button type=\"submit\">Delete</button>\n</form>\n```\n\nWithout JavaScript the form posts on the first click, exactly as it did before. With JavaScript the submit is held, the dialog opens, and the form only posts if the user confirms.\n\n## Form attributes\n\n| Attribute | Required | Effect |\n|---|---|---|\n| `data-confirm` | yes | The message, and the opt-in. A form without it is never gated. |\n| `data-confirm-title` | no | Heading. Falls back to `defaultTitle`, then `\"Are you sure?\"`. |\n| `data-confirm-label` | no | Confirm button label. Falls back to `defaultConfirmLabel`, then `\"Confirm\"`. |\n| `data-confirm-danger` | no | Set to `\"false\"` to drop the destructive colour. Anything else (including absent) keeps it. |\n| `data-confirmed` | — | Set by the library on the approved resubmit. Do not set it yourself; it is the \"let this one through\" marker. |\n\n## Prompting directly\n\nFor a destructive action that isn't a form submit:\n\n```ts\nimport { Dialog } from \"@alexpricedev/billet-dialog\";\n\nconst ok = await Dialog.confirm({\n  title: \"Revoke this invite?\",\n  message: \"The link stops working immediately.\",\n  confirmLabel: \"Revoke\",\n  danger: true,\n});\n\nif (ok) await revoke();\n```\n\n`Dialog` is a shared, lazily-built instance — the common case of one dialog per app. Nothing touches the DOM until the first prompt.\n\n## Multiple instances\n\n`createDialog()` returns an independent instance with its own overlay, its own listener, and its own ARIA ids. Reach for it when you need two dialogs at once, or one inside an iframe:\n\n```ts\nimport { createDialog, wireConfirmForms } from \"@alexpricedev/billet-dialog\";\n\nconst frame = document.querySelector(\"iframe\")!.contentDocument!;\nconst dialog = createDialog({ container: frame.body });\nconst unwire = wireConfirmForms(dialog, { root: frame });\n```\n\nThe keyboard listener binds to the container's owning document, so this works without extra wiring.\n\n## Theming\n\nEvery value is a token declared on `[data-bd-root]`. Re-declare the ones you care about — on `[data-bd-root]`, or on any ancestor, since custom properties inherit:\n\n```css\n[data-bd-root] {\n  --bd-surface: var(--color-ground);\n  --bd-border: var(--color-edge);\n  --bd-fg: var(--color-bone);\n  --bd-muted: var(--color-text-secondary);\n  --bd-accent: var(--color-primary);\n  --bd-accent-hover: var(--color-primary-hover);\n  --bd-accent-fg: var(--color-on-primary);\n  --bd-danger: var(--color-danger);\n  --bd-danger-fg: var(--color-void);\n  --bd-font: var(--font-body);\n  --bd-radius: var(--radius);\n  --bd-radius-sm: var(--radius-sm);\n  --bd-shadow: var(--shadow);\n}\n```\n\n| Token | Default | What it colours |\n|---|---|---|\n| `--bd-surface` | `#121114` | Panel background |\n| `--bd-border` | `#2c2934` | Panel border, cancel button border |\n| `--bd-fg` | `#ece9e2` | Title, cancel button on hover |\n| `--bd-muted` | `#b5b0ba` | Message, cancel button at rest |\n| `--bd-accent` / `--bd-accent-hover` / `--bd-accent-fg` | `#e5b35b` / `#f0c87e` / `#0c0b0e` | Confirm button, non-danger |\n| `--bd-danger` / `--bd-danger-hover` / `--bd-danger-fg` | `#f0776b` / `#f39288` / `#0c0b0e` | Confirm button, danger |\n| `--bd-scrim` | `rgba(0,0,0,0.7)` | Backdrop |\n| `--bd-font` | system stack | Everything |\n| `--bd-radius` / `--bd-radius-sm` | `14px` / `8px` | Panel / buttons |\n| `--bd-shadow` | `0 12px 40px rgba(0,0,0,0.4)` | Panel |\n| `--bd-z` | `2147483000` | Overlay stacking |\n| `--bd-max-w` / `--bd-pad` / `--bd-gap` | `26rem` / `1.75rem` / `0.5rem` | Panel width, padding, button gap |\n\nThe stylesheet has no `:root` rules, no global resets, and no bare element selectors. Both buttons carry their own base styles, so a host app's global `button` rule neither leaks in nor gets fought.\n\n## API reference\n\n### `Dialog.confirm(opts): Promise<boolean>`\n\nPrompt through the shared instance, building it if needed. Resolves `true` only when the confirm button is clicked. Cancel, Escape, and a backdrop click all resolve `false`.\n\n### `Dialog.wireForms(opts?): () => void`\n\nGate `[data-confirm]` forms through the shared instance. Returns the unwire function.\n\n### `Dialog.init(config?): DialogInstance`\n\nConfigure the shared instance up front. Calling it when one already exists replaces it, resolving any pending prompt `false`.\n\n### `Dialog.current(): DialogInstance | null`\n\nThe shared instance, or `null` if nothing has built it yet.\n\n### `Dialog.destroy(): void`\n\nTear the shared instance down. Safe when nothing was ever built.\n\n### `createDialog(config?): DialogInstance`\n\nAn independent instance. `config.container` (default `document.body`) is where the overlay mounts and whose document gets the key listener. `config.texts` overrides the default `title` / `confirmLabel` / `cancelLabel`.\n\n### `wireConfirmForms(dialog, opts?): () => void`\n\nGate `[data-confirm]` forms through a specific instance. `opts.root` (default `document`) scopes the delegation; `opts.defaultTitle`, `opts.defaultConfirmLabel`, and `opts.cancelLabel` set per-wiring fallback copy.\n\n### `instance.confirm(opts)` / `instance.isOpen()` / `instance.element` / `instance.destroy()`\n\nPer-instance equivalents. `confirm` on a destroyed instance rejects.\n\n### `ConfirmOptions`\n\n| Field | Type | Notes |\n|---|---|---|\n| `title` | `string` | Required. Empty string falls back to the instance's text. |\n| `message` | `string?` | Omitted entirely when absent — the element is hidden, not blank. |\n| `confirmLabel` | `string?` | Defaults to the instance's text. |\n| `cancelLabel` | `string?` | Defaults to the instance's text. |\n| `danger` | `boolean?` | Destructive colour on the confirm button. |\n\n## Accessibility\n\n- Panel is `role=\"alertdialog\"` with `aria-modal=\"true\"`, wired to the title and message through `aria-labelledby` / `aria-describedby`. Each instance gets unique ids.\n- Focus starts on **Cancel**, never on the destructive choice — a stray Enter must not destroy anything.\n- Tab is trapped between the two buttons.\n- Escape and a backdrop click both cancel.\n- Focus returns to the triggering element on close.\n\n## Common pitfalls\n\n- **Assigning `document.body.innerHTML` after the dialog mounts** wipes the overlay out from under it. Append instead.\n- **Moving the guard to the button's click handler** breaks the no-JS fallback and misses Enter-to-submit. Gate the form's `submit` event, which is what this package does.\n- **Converting the form to `fetch`** also breaks the fallback. Leave the form a form.\n- **Forgetting the CSS import** leaves an unstyled, un-hidden dialog: the overlay's `display: none` lives in the stylesheet.\n- **Two copies of the package at different versions** each mount their own overlay. Dedupe your lockfile.\n\n## Verifying the install\n\n```ts\nDialog.wireForms();\ndocument.querySelector(\"[data-bd-root]\"); // → <div data-bd-root class=\"bd-overlay\">\n```\n\nIn a browser: click a gated button, confirm the dialog appears with focus on Cancel, press Escape, and check the record still exists.\n\n## Development\n\n```bash\nbun install\nbun run check   # tsc --noEmit && bun test\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-88217a90b3bbd15884cd576f1600e58b"}