@anfo/nuxt-dialogs-plugin 1.0.0

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/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # @anfo/nuxt-dialogs-plugin
2
+
3
+ A Nuxt module that auto-scans a directory for Vue dialog/drawer components and exposes them through a fully type-safe virtual module (`virtual:dialogs`). Dialogs are mounted programmatically into isolated Vue app instances — no `<Teleport>` boilerplate, no global store, automatic cleanup on close.
4
+
5
+ **Nuxt-native extras** — dialog apps automatically inherit the Nuxt app's context, so Pinia, i18n, UI libraries, global components and directives work inside dialogs with zero wiring. `dialogs` and the runtime composables are auto-imported.
6
+
7
+ > This is the Nuxt counterpart of [`@anfo/vite-dialogs-plugin`](https://github.com/) — it is a standalone package and does not depend on it.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install -D @anfo/nuxt-dialogs-plugin
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Register the module — `nuxt.config.ts`
18
+
19
+ ```ts
20
+ export default defineNuxtConfig({
21
+ modules: ["@anfo/nuxt-dialogs-plugin"],
22
+
23
+ // Optional — these are the defaults
24
+ dialogs: {
25
+ dir: "dialogs", // relative to srcDir
26
+ // pattern: /(Dialog|Drawer)\.vue$/,
27
+ // inheritNuxtApp: true,
28
+ },
29
+ });
30
+ ```
31
+
32
+ ### 2. Create a dialog component
33
+
34
+ Every `.vue` file whose name ends with `Dialog.vue` or `Drawer.vue` inside your `dir` becomes a key on the `dialogs` object. No tsconfig changes are needed — the module generates the type declarations for you.
35
+
36
+ ```vue
37
+ <!-- dialogs/ConfirmDialog.vue -->
38
+ <script setup lang="ts">
39
+ import {
40
+ useDialogContext,
41
+ createDialogExpose,
42
+ type DialogExposed,
43
+ } from "@anfo/nuxt-dialogs-plugin/runtime";
44
+
45
+ defineProps<{ message: string }>();
46
+
47
+ const { resolve, reject } = useDialogContext<boolean>();
48
+
49
+ defineExpose<DialogExposed<boolean>>(createDialogExpose<boolean>());
50
+ </script>
51
+
52
+ <template>
53
+ <div class="overlay">
54
+ <div class="box">
55
+ <p>{{ message }}</p>
56
+ <button @click="resolve(true)">Confirm</button>
57
+ <button @click="resolve(false)">Cancel</button>
58
+ </div>
59
+ </div>
60
+ </template>
61
+ ```
62
+
63
+ ### 3. Open dialogs from anywhere
64
+
65
+ `dialogs` is auto-imported — no import statement needed:
66
+
67
+ ```vue
68
+ <script setup lang="ts">
69
+ const result = await dialogs.ConfirmDialog({ message: "Delete this item?" });
70
+ if (result.type === "resolve" && result.value) {
71
+ // confirmed
72
+ }
73
+
74
+ // Or chain callbacks
75
+ dialogs
76
+ .ConfirmDialog({ message: "Are you sure?" })
77
+ .resolve((value) => console.log("resolved:", value))
78
+ .reject((reason) => console.log("rejected:", reason));
79
+ </script>
80
+ ```
81
+
82
+ ## Context inheritance — why dialogs "just work"
83
+
84
+ Each dialog is mounted into its own isolated `createApp()` instance. By default (`inheritNuxtApp: true`) the module captures the Nuxt app and clones its context — global components, directives and `provide`s — onto every dialog app. That means everything your Nuxt app registered is available inside dialogs automatically:
85
+
86
+ - **Pinia** stores (`@pinia/nuxt`)
87
+ - **i18n** (`@nuxtjs/i18n` — `$t`, translations)
88
+ - **UI libraries** (Element Plus, PrimeVue, Vuetify… registered via Nuxt plugins)
89
+ - **Global components and directives**
90
+ - **The `nuxtApp` itself** — `useNuxtApp()`, `useRoute()`… work inside dialogs
91
+
92
+ Set `inheritNuxtApp: false` to keep dialogs fully isolated (parity with plain Vite projects), and register exactly what you need with `configureDialogs()`:
93
+
94
+ ```ts
95
+ // plugins/dialogs.ts
96
+ export default defineNuxtPlugin((nuxtApp) => {
97
+ configureDialogs({
98
+ use: [
99
+ (app) => {
100
+ app.config.globalProperties.$something = "...";
101
+ },
102
+ ],
103
+ });
104
+ });
105
+ ```
106
+
107
+ Both plugin objects (`{ install(app) {} }`) and plain `(app) => void` functions are accepted.
108
+
109
+ ## Module Options
110
+
111
+ | Option | Type | Default | Description |
112
+ |---|---|---|---|
113
+ | `dir` | `string` | `"dialogs"` | Directory containing dialog components; relative paths resolve against `srcDir`. |
114
+ | `pattern` | `RegExp` | `/(Dialog\|Drawer)\.vue$/` | RegExp to identify which files are dialogs. |
115
+ | `inheritNuxtApp` | `boolean` | `true` | Dialog apps inherit the Nuxt app's context (components, directives, provides). |
116
+
117
+ ## Auto-imports
118
+
119
+ | Name | From | Description |
120
+ |---|---|---|
121
+ | `dialogs` | `virtual:dialogs` | Open dialogs — `dialogs.ConfirmDialog(props)`. |
122
+ | `useDialogContext` | `./runtime` | Get `{ resolve, reject }` inside a dialog component. |
123
+ | `createDialogExpose` | `./runtime` | Typed expose helper for `defineExpose`. |
124
+ | `configureDialogs` | `./runtime` | Register extra plugins for every dialog app. |
125
+
126
+ ## Package Exports
127
+
128
+ | Specifier | Contents |
129
+ |---|---|
130
+ | `@anfo/nuxt-dialogs-plugin` | Nuxt module |
131
+ | `@anfo/nuxt-dialogs-plugin/runtime` | Types, `useDialogContext`, `createDialogExpose`, `configureDialogs`, `DialogExposed` |
132
+
133
+ ## SSR
134
+
135
+ Dialogs are DOM-only. Calling `dialogs.*` during server-side rendering is safe — it warns and settles as `{ type: "reject", reason }` instead of crashing the render. Open dialogs from event handlers or `onMounted`.
136
+
137
+ > Do not import `virtual:dialogs` from Nitro server code (`server/api`, `server/routes`) — the virtual module is only available in the app build.
138
+
139
+ ## Project Structure
140
+
141
+ ```
142
+ <srcDir>/
143
+ ├── dialogs/ ← your dialog components (configurable)
144
+ │ ├── ConfirmDialog.vue
145
+ │ ├── AlertDialog.vue
146
+ │ └── UserDrawer.vue
147
+ └── nuxt.config.ts
148
+ ```
149
+
150
+ ## How It Works
151
+
152
+ 1. On startup the module registers a Vite plugin, a tiny runtime plugin (context capture), auto-imports and a type template.
153
+ 2. The Vite plugin generates a virtual module (`virtual:dialogs`) that imports each component through the `#dialogs-components` alias and wraps it in `mountDialog()`.
154
+ 3. `mountDialog()` creates an isolated `createApp()` instance per call, inherits the Nuxt app context, provides a `DialogController`, mounts it into a temporary `<div>`, and returns a Promise-based handle.
155
+ 4. When the component calls `resolve(value)` or `reject(reason)`, the app is unmounted and the host element removed automatically.
156
+ 5. A `.d.ts` type template is written into `.nuxt/` so every `dialogs.*` entry is typed end-to-end — props, return value, and callbacks. It regenerates when dialog files are added or removed during dev.
157
+
158
+ > See [SKILL.md](SKILL.md) for step-by-step examples covering all dialog patterns.
159
+
160
+ ## License
161
+
162
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,274 @@
1
+ # Skill Guide — Creating Dialog Components in Nuxt
2
+
3
+ This guide shows every pattern for building dialog and drawer components that work with `@anfo/nuxt-dialogs-plugin`. All types and composables are auto-imported in Nuxt — explicit imports from `@anfo/nuxt-dialogs-plugin/runtime` also work and are shown here for clarity.
4
+
5
+ ---
6
+
7
+ ## Runtime API Reference
8
+
9
+ Import from `@anfo/nuxt-dialogs-plugin/runtime` (or rely on auto-imports):
10
+
11
+ | Export | Auto-imported | Description |
12
+ | ------------------------- | ------------- | --------------------------------------------------------------------------------- |
13
+ | `dialogs` | ✅ | Open dialogs — `dialogs.ConfirmDialog(props)` (virtual module `virtual:dialogs`) |
14
+ | `useDialogContext<T>()` | ✅ | Composable — returns `{ resolve, reject }` inside a dialog component |
15
+ | `createDialogExpose<T>()` | ✅ | Helper — creates the typed expose object (shorthand for `{} as DialogExposed<T>`) |
16
+ | `configureDialogs()` | ✅ | Register extra Vue plugins to `app.use()` on every dialog app |
17
+ | `DialogExposed<T>` | — type | Marker type — put in `defineExpose<>` to declare the resolve value type |
18
+ | `DialogController<T>` | — type | `{ resolve: DialogResolve<T>, reject: DialogReject }` |
19
+ | `DialogSettledResult<T>` | — type | Union of `DialogResolvedResult<T>` and `DialogRejectedResult` |
20
+ | `DialogResolvedResult<T>` | — type | `{ type: "resolve" }` or `{ type: "resolve"; value: T }` |
21
+ | `DialogRejectedResult` | — type | `{ type: "reject"; reason?: unknown }` |
22
+ | `DialogAppPlugin` | — type | Alias of Vue's `Plugin` — a plugin object or a function taking the app |
23
+
24
+ ---
25
+
26
+ ## Nuxt setup
27
+
28
+ ```ts
29
+ // nuxt.config.ts
30
+ export default defineNuxtConfig({
31
+ modules: ["@anfo/nuxt-dialogs-plugin"],
32
+
33
+ // Optional — defaults shown
34
+ dialogs: {
35
+ dir: "dialogs", // relative to srcDir
36
+ // pattern: /(Dialog|Drawer)\.vue$/,
37
+ // inheritNuxtApp: true,
38
+ },
39
+ });
40
+ ```
41
+
42
+ Place dialog components in `<srcDir>/dialogs/` (or configure `dir`). Files ending in `Dialog.vue` or `Drawer.vue` become keys on the `dialogs` object. Types are generated into `.nuxt/` automatically — no tsconfig changes needed.
43
+
44
+ ---
45
+
46
+ ## Context inheritance
47
+
48
+ By default every dialog app **inherits the Nuxt app's context** — global components, directives and provides. Inside dialog components this means:
49
+
50
+ - Pinia stores work (`@pinia/nuxt`)
51
+ - `$t` / i18n works (`@nuxtjs/i18n`)
52
+ - UI library components registered in Nuxt work
53
+ - `useNuxtApp()`, `useRoute()` work
54
+
55
+ No wiring needed. To opt out, set `inheritNuxtApp: false` and register exactly what you need with `configureDialogs()`:
56
+
57
+ ```ts
58
+ // plugins/dialogs.ts
59
+ export default defineNuxtPlugin((nuxtApp) => {
60
+ configureDialogs({
61
+ use: [
62
+ // plain functions and plugin objects both work
63
+ (app) => {
64
+ app.config.globalProperties.$t = translate;
65
+ },
66
+ ],
67
+ });
68
+ });
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Pattern 1 — Simple dialog, no return value
74
+
75
+ The simplest case: open, the user clicks a button, dialog closes.
76
+
77
+ ```vue
78
+ <!-- dialogs/AlertDialog.vue -->
79
+ <script setup lang="ts">
80
+ import { useDialogContext } from "@anfo/nuxt-dialogs-plugin/runtime";
81
+
82
+ defineProps<{ message: string }>();
83
+
84
+ const { resolve } = useDialogContext();
85
+ </script>
86
+
87
+ <template>
88
+ <div class="overlay">
89
+ <div class="box">
90
+ <p>{{ message }}</p>
91
+ <button @click="resolve()">OK</button>
92
+ </div>
93
+ </div>
94
+ </template>
95
+ ```
96
+
97
+ ```vue
98
+ <!-- Caller (auto-imported `dialogs`) -->
99
+ <script setup lang="ts">
100
+ await dialogs.AlertDialog({ message: "File saved." });
101
+ // resolves with { type: "resolve" } — no value
102
+ </script>
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Pattern 2 — Dialog that returns a typed value
108
+
109
+ Use `defineExpose<DialogExposed<T>>` to declare what `resolve()` carries. The generic `T` flows all the way to the caller's return type.
110
+
111
+ ```vue
112
+ <!-- dialogs/ConfirmDialog.vue -->
113
+ <script setup lang="ts">
114
+ import {
115
+ useDialogContext,
116
+ createDialogExpose,
117
+ type DialogExposed,
118
+ } from "@anfo/nuxt-dialogs-plugin/runtime";
119
+
120
+ defineProps<{
121
+ message: string;
122
+ confirmLabel?: string;
123
+ }>();
124
+
125
+ const { resolve } = useDialogContext<boolean>();
126
+
127
+ // Declare the resolve type — T = boolean
128
+ defineExpose<DialogExposed<boolean>>(createDialogExpose<boolean>());
129
+ </script>
130
+
131
+ <template>
132
+ <div class="overlay">
133
+ <div class="box">
134
+ <p>{{ message }}</p>
135
+ <button @click="resolve(true)">{{ confirmLabel ?? "Confirm" }}</button>
136
+ <button @click="resolve(false)">Cancel</button>
137
+ </div>
138
+ </div>
139
+ </template>
140
+ ```
141
+
142
+ ```ts
143
+ // Caller — result.value is typed as boolean | undefined
144
+ const result = await dialogs.ConfirmDialog({ message: "Delete item?" });
145
+ if (result.type === "resolve" && result.value) {
146
+ await deleteItem();
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Pattern 3 — Dialog with reject
153
+
154
+ Call `reject(reason)` for explicit cancellation that the caller can distinguish from a normal close.
155
+
156
+ ```vue
157
+ <!-- dialogs/PromptDialog.vue -->
158
+ <script setup lang="ts">
159
+ import { ref } from "vue";
160
+ import {
161
+ useDialogContext,
162
+ createDialogExpose,
163
+ type DialogExposed,
164
+ } from "@anfo/nuxt-dialogs-plugin/runtime";
165
+
166
+ defineProps<{ label: string }>();
167
+
168
+ const { resolve, reject } = useDialogContext<string>();
169
+ const input = ref("");
170
+
171
+ defineExpose<DialogExposed<string>>(createDialogExpose<string>());
172
+ </script>
173
+
174
+ <template>
175
+ <div class="overlay">
176
+ <div class="box">
177
+ <label>{{ label }}</label>
178
+ <input v-model="input" @keydown.enter="resolve(input)" />
179
+ <button @click="resolve(input)">OK</button>
180
+ <button @click="reject('cancelled')">Cancel</button>
181
+ </div>
182
+ </div>
183
+ </template>
184
+ ```
185
+
186
+ ```ts
187
+ // Caller — chain style
188
+ dialogs
189
+ .PromptDialog({ label: "Enter a name" })
190
+ .resolve((name) => console.log("Got:", name))
191
+ .reject((reason) => console.log("Cancelled:", reason));
192
+
193
+ // Caller — await style
194
+ const result = await dialogs.PromptDialog({ label: "Enter a name" });
195
+ if (result.type === "resolve") {
196
+ console.log(result.value); // string | undefined
197
+ } else {
198
+ console.log(result.reason); // "cancelled"
199
+ }
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Pattern 4 — Drawer
205
+
206
+ Files ending in `Drawer.vue` are matched by the default pattern and work identically to dialogs.
207
+
208
+ ```vue
209
+ <!-- dialogs/UserDrawer.vue -->
210
+ <script setup lang="ts">
211
+ import { useDialogContext } from "@anfo/nuxt-dialogs-plugin/runtime";
212
+
213
+ defineProps<{ userId: string }>();
214
+ const { resolve } = useDialogContext();
215
+ </script>
216
+
217
+ <template>
218
+ <aside class="drawer">
219
+ <p>User: {{ userId }}</p>
220
+ <button @click="resolve()">Close</button>
221
+ </aside>
222
+ </template>
223
+ ```
224
+
225
+ ```ts
226
+ await dialogs.UserDrawer({ userId: "abc123" });
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Pattern 5 — Dialog using Pinia / global components
232
+
233
+ Thanks to context inheritance, dialogs can use anything registered in the Nuxt app — e.g. a Pinia store and UI-library components:
234
+
235
+ ```vue
236
+ <!-- dialogs/CartDialog.vue -->
237
+ <script setup lang="ts">
238
+ import { useCartStore } from "~/stores/cart";
239
+ import { useDialogContext } from "@anfo/nuxt-dialogs-plugin/runtime";
240
+
241
+ const cart = useCartStore();
242
+ const { resolve } = useDialogContext();
243
+ </script>
244
+
245
+ <template>
246
+ <div class="overlay">
247
+ <div class="box">
248
+ <p>{{ cart.items.length }} items — {{ cart.total }}</p>
249
+ <button @click="resolve()">Done</button>
250
+ </div>
251
+ </div>
252
+ </template>
253
+ ```
254
+
255
+ ---
256
+
257
+ ## SSR behavior
258
+
259
+ Dialogs are DOM-only. Calling `dialogs.*` during SSR warns and settles as `{ type: "reject", reason }` — it never crashes the server render. Open dialogs from event handlers or `onMounted`. Don't import `virtual:dialogs` from Nitro server code (`server/api`, `server/routes`).
260
+
261
+ ---
262
+
263
+ ## Summary
264
+
265
+ | Task | How |
266
+ | ------------------------ | -------------------------------------------------------------------------- |
267
+ | Get resolve/reject | `const { resolve, reject } = useDialogContext<T>()` |
268
+ | Declare return type | `defineExpose<DialogExposed<T>>(createDialogExpose<T>())` |
269
+ | Open from app code | `dialogs.MyDialog(props)` — auto-imported, or import from `virtual:dialogs` |
270
+ | Await result | `const result = await dialogs.MyDialog(props)` |
271
+ | Chain callbacks | `.resolve(cb).reject(cb)` |
272
+ | Share Pinia / UI libs | Automatic via `inheritNuxtApp` (default); extras via `configureDialogs()` |
273
+
274
+ The module auto-regenerates the virtual module and types whenever you add or remove dialog files during dev — no manual registration needed.