@nexdom/uimed-vue 2.0.0-beta.15 → 2.0.0-beta.17

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/AGENTS.md CHANGED
@@ -165,7 +165,10 @@ Use an existing composable (e.g. `use-toast`) as the reference, and deliver in t
165
165
  - All `src` code is written in English. `docs` content is written in Portuguese (aimed at Brazilian users), even though file/dir names stay in English.
166
166
  - `docs` must not mention Vuetify
167
167
  - A new component/composable must be listed in its guide page (`docs/guide/…`), its API page (`docs/api/…`), both sidebars in `docs/.vitepress/config.ts`, the guide index table (`docs/guide/index.md`) and the API index list (`docs/api/index.md`).
168
- - Never write CSS or `<style>` blocks, and expose styling to consumers only through props. Inside the library's own components, a Vuetify utility class is acceptable when no Vuetify prop covers the need (e.g. `text-wrap` on a card title), with a comment saying why.
168
+ - Never write CSS or `<style>` blocks, and expose styling to consumers only through props.
169
+ - Don't add spacing to Vuetify components: no padding or margin utility classes (`pa-*`, `pt-6`, `px-4`, `ma-2`, ...) and no spacing props overriding the defaults. Vuetify already applies Material Design 3 spacing, and overriding it breaks what it handles on its own, such as button paddings and the scrollbar of scrollable content. If the default spacing looks wrong, stop and ask instead of adjusting it.
170
+ - Prefer a Vuetify component's own props to building its parts by hand (e.g. `title`/`text` on `v-card` instead of `v-card-item` > `v-card-title` > `v-card-text`). A non-spacing utility class is acceptable only when no prop covers the need (e.g. `text-wrap` on a card title), with a comment saying why.
171
+ - Keep components thin: a component wires props, slots and events to Vuetify. Logic beyond that (focus management, queues, DOM lookups, comparisons) goes into an internal composable with its own unit tests.
169
172
  - Path aliases: `@/*` → `src/*`, `@e2e/*` → `e2e/*`.
170
173
  - Every public component follows this pattern to block access to internals and give it an editor-hover description:
171
174
 
@@ -187,6 +190,7 @@ Use an existing composable (e.g. `use-toast`) as the reference, and deliver in t
187
190
 
188
191
  - Component layout: `src/components/<name>/<name>.vue`, `types.ts` for prop/option types, `__tests__/<name>.test.ts` for unit tests. Components that belong to a group live in the group's folder (e.g. the internal `src/components/dialogs/toast.vue`).
189
192
  - Composables live in a group folder: `src/composables/<group>/<name>.ts`, with `types.ts` and `__tests__/` in the group folder (e.g. `src/composables/dialogs/use-toast.ts`).
193
+ - Library-wide configuration constants that aren't components or composables live in `src/consts/<name>.ts` (e.g. `src/consts/locale.ts`, the language set by `createUimed()` and `vueTestUtilsPluginUimed()`).
190
194
  - Public components/composables are re-exported from `src/components/index.ts` and `src/composables/index.ts`. Prop types stay internal (`types.ts` is not re-exported); consumers derive them with `ComponentProps` (see [Usage](#usage)).
191
195
  - Every publicly exported component must use the `U` prefix on its export identifier (e.g., `UButton`, `UMain`), while internal file names and component names remain unprefixed.
192
196
  - Use [JSDoc](https://jsdoc.app/about-getting-started) on every method/prop/type intended to be part of the public API — it's the primary documentation surface and supports markdown/code examples.
@@ -199,8 +203,19 @@ Use an existing composable (e.g. `use-toast`) as the reference, and deliver in t
199
203
  - With fake timers in jsdom, Vuetify transitions (e.g. of `VDialog`, `VMenu`) don't finish on their own. Emit the transition events on the Vuetify component (`wrapper.findComponent(VDialog).vm.$emit("afterLeave")`) or advance the timers with `await vi.runAllTimersAsync()`.
200
204
  - Coverage threshold is 100%; mutation testing threshold is 100% (break at 100). Don't add code paths without covering tests. Mutants that don't compile are reported as compile errors and don't count toward the score.
201
205
  - E2E tests (Playwright, `e2e/`) run against the built docs preview (`http://localhost:4173/uimed-vue/`). Snapshots/screenshots live under `__snapshots__`/`__screenshot__` next to each spec.
206
+ - Before keeping a new screenshot as a baseline, open it and check it against how plain Vuetify renders the same component: overflowing content shows its scrollbar, buttons and cards keep their native paddings, nothing touches the edges or gets clipped. A screenshot that only "looks fine" isn't enough: name what you checked in your report.
207
+ - Never run Stryker at the same time as another test command: they share build output and Stryker fails with `ENOENT`.
208
+ - Unit tests that mount several apps at once (e.g. nested dialogs) need a distinct `global.config.idPrefix` per mount, since `useId()` restarts in each app.
202
209
  - In E2E, a paused `page.clock` also freezes transitions, so overlays never finish leaving. Use `page.clock.pauseAt` to hold time-based work (e.g. a demo action with `setTimeout`), `page.clock.fastForward` to complete it, then `page.clock.resume()` before expecting the overlay to be hidden (see `e2e/composables/use-confirm.spec.ts`).
203
210
 
211
+ ## Public API design
212
+
213
+ The public API is hard to change once released, and every prop, event or option is something consumers can misuse and maintainers must keep. Keep it minimal:
214
+
215
+ - Only add what the issue asks for. Anything beyond it (an extra prop, event, option or variant) is a separate decision the maintainers must approve explicitly, not a default.
216
+ - Behaviors meant to be consistent across the library aren't per-instance options. For example, every dialog closes with `Esc`, a click outside it or the browser's back button; a flag to block that stays internal.
217
+ - Reuse existing contracts before creating new ones: `v-model` for open/closed state instead of extra lifecycle events such as `afterLeave`, the `actions` shape of `USection` for buttons, the option names of existing composables.
218
+
204
219
  ## Library documentation and dependencies
205
220
 
206
221
  The stack is newer than most AI models' training data (Vue 3.5, Vuetify 4, Vue Router 5, Vite+ 0.2, Vitest 4, TypeScript 6, VitePress 2 alpha, Stryker 10, Playwright 1.62), so don't rely on memory for library APIs.
@@ -182,8 +182,8 @@ type MainProps = {
182
182
  *
183
183
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/main | Main Guide}
184
184
  */
185
- declare const _default$10: typeof __VLS_export$15;
186
- declare const __VLS_export$15: __VLS_WithSlots$9<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<MainProps & {
185
+ declare const _default$11: typeof __VLS_export$16;
186
+ declare const __VLS_export$16: __VLS_WithSlots$10<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<MainProps & {
187
187
  notificationsOpen?: boolean;
188
188
  }, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {
189
189
  "update:notificationsOpen": (value: boolean) => any;
@@ -194,7 +194,7 @@ declare const __VLS_export$15: __VLS_WithSlots$9<import("vue", { with: { "resolu
194
194
  }>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
195
195
  default?: (props: {}) => any;
196
196
  }>;
197
- type __VLS_WithSlots$9<T, S> = T & {
197
+ type __VLS_WithSlots$10<T, S> = T & {
198
198
  new (): {
199
199
  $slots: S;
200
200
  };
@@ -215,15 +215,15 @@ type __VLS_WithSlots$9<T, S> = T & {
215
215
  *
216
216
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/button | Button Guide}
217
217
  */
218
- declare const _default$1: typeof __VLS_export$14;
219
- declare const __VLS_export$14: __VLS_WithSlots$8<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<ButtonProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {} & {
218
+ declare const _default$1: typeof __VLS_export$15;
219
+ declare const __VLS_export$15: __VLS_WithSlots$9<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<ButtonProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {} & {
220
220
  click: (event: MouseEvent) => any;
221
221
  }, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<ButtonProps> & Readonly<{
222
222
  onClick?: ((event: MouseEvent) => any) | undefined;
223
223
  }>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
224
224
  default?: (props: {}) => any;
225
225
  }>;
226
- type __VLS_WithSlots$8<T, S> = T & {
226
+ type __VLS_WithSlots$9<T, S> = T & {
227
227
  new (): {
228
228
  $slots: S;
229
229
  };
@@ -387,8 +387,8 @@ type AutocompleteFieldProps<T = string, Multiple extends boolean = false> = {
387
387
  *
388
388
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/autocomplete-field | AutocompleteField Guide}
389
389
  */
390
- declare const _default: typeof __VLS_export$13;
391
- declare const __VLS_export$13: <T = string, Multiple extends boolean = false>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal$2<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
390
+ declare const _default: typeof __VLS_export$14;
391
+ declare const __VLS_export$14: <T = string, Multiple extends boolean = false>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal$2<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
392
392
  props: import("vue").PublicProps & __VLS_PrettifyLocal$2<(AutocompleteFieldProps<T, Multiple> & {
393
393
  modelValue?: Multiple extends true ? T[] : T;
394
394
  }) & {
@@ -418,8 +418,8 @@ type __VLS_PrettifyLocal$2<T> = (T extends any ? { [K in keyof T]: T[K]; } : { [
418
418
  *
419
419
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/text-field | TextField Guide}
420
420
  */
421
- declare const _default$15: typeof __VLS_export$12;
422
- declare const __VLS_export$12: import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<TextFieldProps & {
421
+ declare const _default$16: typeof __VLS_export$13;
422
+ declare const __VLS_export$13: import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<TextFieldProps & {
423
423
  modelValue?: string;
424
424
  }, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {
425
425
  "update:modelValue": (value: string) => any;
@@ -481,8 +481,8 @@ type CheckboxProps<T = boolean> = {
481
481
  *
482
482
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/checkbox | Checkbox Guide}
483
483
  */
484
- declare const _default$2: typeof __VLS_export$11;
485
- declare const __VLS_export$11: <T = any>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal$1<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
484
+ declare const _default$2: typeof __VLS_export$12;
485
+ declare const __VLS_export$12: <T = any>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal$1<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
486
486
  props: import("vue").PublicProps & __VLS_PrettifyLocal$1<(CheckboxProps<boolean | T> & {
487
487
  modelValue?: T;
488
488
  }) & {
@@ -596,8 +596,8 @@ type DateTimeFieldProps = {
596
596
  *
597
597
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/date-time-field | DateTimeField Guide}
598
598
  */
599
- declare const _default$7: typeof __VLS_export$10;
600
- declare const __VLS_export$10: import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<DateTimeFieldProps & {
599
+ declare const _default$7: typeof __VLS_export$11;
600
+ declare const __VLS_export$11: import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<DateTimeFieldProps & {
601
601
  modelValue?: string;
602
602
  }, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {
603
603
  "update:modelValue": (value: string) => any;
@@ -636,11 +636,11 @@ type ContainerProps = {
636
636
  *
637
637
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/layout | Container Guide}
638
638
  */
639
- declare const _default$4: typeof __VLS_export$9;
640
- declare const __VLS_export$9: __VLS_WithSlots$7<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<ContainerProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<ContainerProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
639
+ declare const _default$4: typeof __VLS_export$10;
640
+ declare const __VLS_export$10: __VLS_WithSlots$8<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<ContainerProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<ContainerProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
641
641
  default?: (props: {}) => any;
642
642
  }>;
643
- type __VLS_WithSlots$7<T, S> = T & {
643
+ type __VLS_WithSlots$8<T, S> = T & {
644
644
  new (): {
645
645
  $slots: S;
646
646
  };
@@ -682,11 +682,11 @@ type RowProps = {
682
682
  *
683
683
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/layout | Row Guide}
684
684
  */
685
- declare const _default$11: typeof __VLS_export$8;
686
- declare const __VLS_export$8: __VLS_WithSlots$6<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<RowProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<RowProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
685
+ declare const _default$12: typeof __VLS_export$9;
686
+ declare const __VLS_export$9: __VLS_WithSlots$7<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<RowProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<RowProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
687
687
  default?: (props: {}) => any;
688
688
  }>;
689
- type __VLS_WithSlots$6<T, S> = T & {
689
+ type __VLS_WithSlots$7<T, S> = T & {
690
690
  new (): {
691
691
  $slots: S;
692
692
  };
@@ -738,13 +738,13 @@ type Cols = "auto" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10"
738
738
  *
739
739
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/layout | Column Guide}
740
740
  */
741
- declare const _default$3: typeof __VLS_export$7;
742
- declare const __VLS_export$7: __VLS_WithSlots$5<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<ColumnProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<ColumnProps> & Readonly<{}>, {
741
+ declare const _default$3: typeof __VLS_export$8;
742
+ declare const __VLS_export$8: __VLS_WithSlots$6<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<ColumnProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<ColumnProps> & Readonly<{}>, {
743
743
  cols: "12" | "auto" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11";
744
744
  }, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
745
745
  default?: (props: {}) => any;
746
746
  }>;
747
- type __VLS_WithSlots$5<T, S> = T & {
747
+ type __VLS_WithSlots$6<T, S> = T & {
748
748
  new (): {
749
749
  $slots: S;
750
750
  };
@@ -781,15 +781,15 @@ type FormProps = {
781
781
  *
782
782
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/form | Form Guide}
783
783
  */
784
- declare const _default$9: typeof __VLS_export$6;
785
- declare const __VLS_export$6: __VLS_WithSlots$4<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<FormProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {} & {
784
+ declare const _default$10: typeof __VLS_export$7;
785
+ declare const __VLS_export$7: __VLS_WithSlots$5<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<FormProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {} & {
786
786
  submit: (event: SubmitEvent) => any;
787
787
  }, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<FormProps> & Readonly<{
788
788
  onSubmit?: ((event: SubmitEvent) => any) | undefined;
789
789
  }>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
790
790
  default?: (props: {}) => any;
791
791
  }>;
792
- type __VLS_WithSlots$4<T, S> = T & {
792
+ type __VLS_WithSlots$5<T, S> = T & {
793
793
  new (): {
794
794
  $slots: S;
795
795
  };
@@ -811,11 +811,11 @@ type __VLS_WithSlots$4<T, S> = T & {
811
811
  *
812
812
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/section | Section Guide}
813
813
  */
814
- declare const _default$13: typeof __VLS_export$5;
815
- declare const __VLS_export$5: __VLS_WithSlots$3<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<{}, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, true, {}, any>, {
814
+ declare const _default$14: typeof __VLS_export$6;
815
+ declare const __VLS_export$6: __VLS_WithSlots$4<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<{}, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, true, {}, any>, {
816
816
  default?: (props: {}) => any;
817
817
  }>;
818
- type __VLS_WithSlots$3<T, S> = T & {
818
+ type __VLS_WithSlots$4<T, S> = T & {
819
819
  new (): {
820
820
  $slots: S;
821
821
  };
@@ -897,11 +897,11 @@ interface SectionAction extends ButtonProps {
897
897
  *
898
898
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/section | Section Guide}
899
899
  */
900
- declare const _default$12: typeof __VLS_export$4;
901
- declare const __VLS_export$4: __VLS_WithSlots$2<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<SectionProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<SectionProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
900
+ declare const _default$13: typeof __VLS_export$5;
901
+ declare const __VLS_export$5: __VLS_WithSlots$3<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<SectionProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<SectionProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
902
902
  default?: (props: {}) => any;
903
903
  }>;
904
- type __VLS_WithSlots$2<T, S> = T & {
904
+ type __VLS_WithSlots$3<T, S> = T & {
905
905
  new (): {
906
906
  $slots: S;
907
907
  };
@@ -972,8 +972,8 @@ type TableProps = {
972
972
  *
973
973
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/table | Table Guide}
974
974
  */
975
- declare const _default$14: typeof __VLS_export$3;
976
- declare const __VLS_export$3: import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<TableProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<TableProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>;
975
+ declare const _default$15: typeof __VLS_export$4;
976
+ declare const __VLS_export$4: import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<TableProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<TableProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>;
977
977
  //#endregion
978
978
  //#region src/components/details/types.d.ts
979
979
  type DetailsVariant = "primary" | "secondary";
@@ -1020,11 +1020,11 @@ type DetailsProps = {
1020
1020
  *
1021
1021
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/details | Details Guide}
1022
1022
  */
1023
- declare const _default$8: typeof __VLS_export$2;
1024
- declare const __VLS_export$2: __VLS_WithSlots$1<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<DetailsProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<DetailsProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
1023
+ declare const _default$8: typeof __VLS_export$3;
1024
+ declare const __VLS_export$3: __VLS_WithSlots$2<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<DetailsProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<DetailsProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
1025
1025
  default?: (props: {}) => any;
1026
1026
  }>;
1027
- type __VLS_WithSlots$1<T, S> = T & {
1027
+ type __VLS_WithSlots$2<T, S> = T & {
1028
1028
  new (): {
1029
1029
  $slots: S;
1030
1030
  };
@@ -1125,8 +1125,8 @@ type DataSetSlots<T extends object> = {
1125
1125
  *
1126
1126
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/data-set | DataSet Guide}
1127
1127
  */
1128
- declare const _default$5: typeof __VLS_export$1;
1129
- declare const __VLS_export$1: <T extends object>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
1128
+ declare const _default$5: typeof __VLS_export$2;
1129
+ declare const __VLS_export$2: <T extends object>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
1130
1130
  props: import("vue").PublicProps & __VLS_PrettifyLocal<(DataSetProps<T> & {
1131
1131
  page?: number;
1132
1132
  search?: string;
@@ -1201,12 +1201,89 @@ type DataSetItemSlots = {
1201
1201
  *
1202
1202
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/data-set | DataSet Guide}
1203
1203
  */
1204
- declare const _default$6: typeof __VLS_export;
1205
- declare const __VLS_export: __VLS_WithSlots<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<DataSetItemProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<DataSetItemProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, DataSetItemSlots>;
1204
+ declare const _default$6: typeof __VLS_export$1;
1205
+ declare const __VLS_export$1: __VLS_WithSlots$1<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<DataSetItemProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<DataSetItemProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, DataSetItemSlots>;
1206
+ type __VLS_WithSlots$1<T, S> = T & {
1207
+ new (): {
1208
+ $slots: S;
1209
+ };
1210
+ };
1211
+ //#endregion
1212
+ //#region src/components/dialogs/dialog/types.d.ts
1213
+ type DialogSize = "small" | "medium" | "large";
1214
+ /**
1215
+ * Props exposed by the {@link Dialog} component.
1216
+ */
1217
+ type DialogProps = {
1218
+ /**
1219
+ * Plain text displayed as the dialog's title, in a single line. HTML isn't interpreted.
1220
+ * Also used as the dialog's accessible name. Without it, the content is the accessible name.
1221
+ */
1222
+ title?: string;
1223
+ /**
1224
+ * Buttons displayed at the bottom of the dialog, right-aligned, in the given order.
1225
+ * Place the main action last. When undefined or empty, no actions area is rendered.
1226
+ * Clicking an action doesn't close the dialog: set `v-model` to `false` in `onClick`.
1227
+ */
1228
+ actions?: DialogButtonAction[];
1229
+ /**
1230
+ * Maximum width of the dialog.
1231
+ * One of `small` (400px), `medium` (560px), or `large` (800px).
1232
+ * @default "medium"
1233
+ */
1234
+ size?: DialogSize;
1235
+ /**
1236
+ * Component id to use on automated tests.
1237
+ */
1238
+ dataTestid?: string;
1239
+ };
1240
+ /**
1241
+ * A single action rendered inside {@link Dialog}'s actions area. Same shape as `USection`'s
1242
+ * actions.
1243
+ */
1244
+ interface DialogButtonAction extends ButtonProps {
1245
+ /**
1246
+ * Text displayed on the action button.
1247
+ */
1248
+ label: string;
1249
+ /**
1250
+ * Called when the action button is clicked. The dialog stays open.
1251
+ */
1252
+ onClick?: (event: MouseEvent) => void;
1253
+ }
1254
+ //#endregion
1255
+ //#region src/components/dialogs/dialog/dialog.vue.d.ts
1256
+ /**
1257
+ * Dialog component to display content in a modal window, over the page, until it's closed.
1258
+ * Its title and actions stay in place while the content scrolls.
1259
+ *
1260
+ * @example
1261
+ * ```vue
1262
+ * <template>
1263
+ * <u-dialog v-model="open" title="Title" :actions="[{ label: 'Close', onClick: () => (open = false) }]">
1264
+ * Content
1265
+ * </u-dialog>
1266
+ * </template>
1267
+ * ```
1268
+ *
1269
+ * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/dialog | Dialog Guide}
1270
+ */
1271
+ declare const _default$9: typeof __VLS_export;
1272
+ declare const __VLS_export: __VLS_WithSlots<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<DialogProps & {
1273
+ modelValue?: boolean;
1274
+ }, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {
1275
+ "update:modelValue": (value: boolean) => any;
1276
+ }, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<DialogProps & {
1277
+ modelValue?: boolean;
1278
+ }> & Readonly<{
1279
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
1280
+ }>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
1281
+ default?: (props: {}) => any;
1282
+ }>;
1206
1283
  type __VLS_WithSlots<T, S> = T & {
1207
1284
  new (): {
1208
1285
  $slots: S;
1209
1286
  };
1210
1287
  };
1211
1288
  //#endregion
1212
- export { _default as UAutocompleteField, _default$1 as UButton, _default$2 as UCheckbox, _default$3 as UColumn, _default$4 as UContainer, _default$5 as UDataSet, _default$6 as UDataSetItem, _default$7 as UDateTimeField, _default$8 as UDetails, _default$9 as UForm, _default$10 as UMain, _default$11 as URow, _default$12 as USection, _default$13 as USectionContent, _default$14 as UTable, _default$15 as UTextField };
1289
+ export { _default as UAutocompleteField, _default$1 as UButton, _default$2 as UCheckbox, _default$3 as UColumn, _default$4 as UContainer, _default$5 as UDataSet, _default$6 as UDataSetItem, _default$7 as UDateTimeField, _default$8 as UDetails, _default$9 as UDialog, _default$10 as UForm, _default$11 as UMain, _default$12 as URow, _default$13 as USection, _default$14 as USectionContent, _default$15 as UTable, _default$16 as UTextField };
@@ -1,8 +1,8 @@
1
1
  import { a as messages, n as owners, r as requests, s as useVuetifyColor } from "./use-dialog-Ck2_ZW7T.js";
2
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, isRef, mergeModels, mergeProps, normalizeClass, onMounted, onScopeDispose, onUnmounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toValue, unref, useId, useModel, useSlots, useTemplateRef, watch, withCtx, withModifiers } from "vue";
3
- import { VApp, VAppBar, VAppBarNavIcon, VAppBarTitle, VAutocomplete, VAvatar, VBadge, VBtn, VCard, VCardActions, VCardItem, VCardText, VCardTitle, VCheckboxBtn, VCol, VCombobox, VContainer, VDataIterator, VDatePicker, VDialog, VDivider, VEmptyState, VExpansionPanel, VExpansionPanelText, VExpansionPanelTitle, VExpansionPanels, VForm, VIcon, VIconBtn, VImg, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VLocaleProvider, VMain, VMenu, VNavigationDrawer, VPagination, VRow, VSkeletonLoader, VSnackbarQueue, VTable, VTextField, VTimePicker } from "vuetify/components";
4
- import { formatDateTime, isEmail, isEmpty, isPhone, isUrl } from "@nexdom/shared/utils";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, inject, isRef, mergeModels, mergeProps, normalizeClass, onMounted, onScopeDispose, onUnmounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toValue, unref, useId, useModel, useSlots, useTemplateRef, watch, withCtx, withModifiers } from "vue";
5
3
  import { pt } from "vuetify/locale";
4
+ import { VApp, VAppBar, VAppBarNavIcon, VAppBarTitle, VAutocomplete, VAvatar, VBadge, VBtn, VCard, VCardActions, VCardText, VCheckboxBtn, VCol, VCombobox, VContainer, VDataIterator, VDatePicker, VDialog, VDivider, VEmptyState, VExpansionPanel, VExpansionPanelText, VExpansionPanelTitle, VExpansionPanels, VForm, VIcon, VIconBtn, VImg, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VLocaleProvider, VMain, VMenu, VNavigationDrawer, VPagination, VRow, VSkeletonLoader, VSnackbarQueue, VTable, VTextField, VTimePicker } from "vuetify/components";
5
+ import { formatDateTime, isEmail, isEmpty, isPhone, isUrl } from "@nexdom/shared/utils";
6
6
  //#region src/composables/navigation/use-route-props.ts
7
7
  function useRouteProps(route) {
8
8
  return computed(() => routeToProps(route));
@@ -16,7 +16,7 @@ function isExternalRoute(route) {
16
16
  }
17
17
  //#endregion
18
18
  //#region src/components/app-bar/app-bar.vue?vue&type=script&setup=true&lang.ts
19
- const _hoisted_1$2 = { class: "opacity-60" };
19
+ const _hoisted_1$3 = { class: "opacity-60" };
20
20
  //#endregion
21
21
  //#region src/components/app-bar/app-bar.vue
22
22
  var app_bar_default = /* @__PURE__ */ defineComponent({
@@ -137,7 +137,7 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
137
137
  }, createSlots({ _: 2 }, [notification.when ? {
138
138
  name: "append",
139
139
  fn: withCtx(() => [createVNode(unref(VListItemAction), { end: "" }, {
140
- default: withCtx(() => [createElementVNode("small", _hoisted_1$2, toDisplayString(notification.when), 1)]),
140
+ default: withCtx(() => [createElementVNode("small", _hoisted_1$3, toDisplayString(notification.when), 1)]),
141
141
  _: 2
142
142
  }, 1024)]),
143
143
  key: "0"
@@ -412,7 +412,7 @@ var text_field_default = /* @__PURE__ */ defineComponent({
412
412
  });
413
413
  //#endregion
414
414
  //#region src/components/navigation-menu/navigation-menu.vue?vue&type=script&setup=true&lang.ts
415
- const _hoisted_1$1 = { class: "px-4 pt-2" };
415
+ const _hoisted_1$2 = { class: "px-4 pt-2" };
416
416
  //#endregion
417
417
  //#region src/components/navigation-menu/navigation-menu.vue
418
418
  var navigation_menu_default = /* @__PURE__ */ defineComponent({
@@ -475,7 +475,7 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
475
475
  absolute: "",
476
476
  temporary: ""
477
477
  }, {
478
- prepend: withCtx(() => [createElementVNode("div", _hoisted_1$1, [createVNode(text_field_default, {
478
+ prepend: withCtx(() => [createElementVNode("div", _hoisted_1$2, [createVNode(text_field_default, {
479
479
  modelValue: search.value,
480
480
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event),
481
481
  type: "search",
@@ -664,31 +664,225 @@ var button_default = /* @__PURE__ */ defineComponent({
664
664
  }
665
665
  });
666
666
  //#endregion
667
- //#region src/components/dialogs/dialog.vue
667
+ //#region src/components/dialogs/dialog/dialog-host-key.ts
668
+ /**
669
+ * Injection key of {@link DialogHostContext}, provided by the internal `DialogHost`. Besides the
670
+ * role, the blocking while an action runs and the end of the leave transitions, it makes the
671
+ * dialog's content describe the dialog (`aria-describedby`), since `DialogHost` only displays text
672
+ * messages. Not part of the public API.
673
+ */
674
+ const dialogHostKey = Symbol("dialog-host");
675
+ //#endregion
676
+ //#region src/composables/dialogs/use-dialog-focus.ts
677
+ /** Focus state of the closed dialogs, by their card, for the dialogs opened from them. */
678
+ const closedDialogs = /* @__PURE__ */ new WeakMap();
679
+ /** Elements that can get the focus when the dialog opens. The first one in the dialog gets it. */
680
+ const focusable = [
681
+ "button:not([disabled])",
682
+ "[href]",
683
+ "input:not([disabled]):not([type=\"hidden\"])",
684
+ "select:not([disabled])",
685
+ "textarea:not([disabled])",
686
+ "[tabindex]:not([tabindex=\"-1\"])"
687
+ ].join(", ");
688
+ /**
689
+ * Moves the focus into a dialog once it opens and back to where it was when it closes, including
690
+ * when dialogs are opened from others. Internal to the dialog components.
691
+ * @param isActive - Whether the dialog is displayed. It turns `false` as soon as the dialog starts
692
+ * leaving, so the focus returns right away.
693
+ * @param card - The dialog's card, marked with `data-u-dialog`, or nothing while it isn't rendered.
694
+ * @returns `focusFirstElement`, to call once the dialog finishes entering.
695
+ */
696
+ function useDialogFocus(isActive, card) {
697
+ /** Focus state of this dialog, shared with the dialogs opened from it. */
698
+ const focusState = {
699
+ isOpen: isActive,
700
+ origin: null
701
+ };
702
+ onMounted(() => onActiveChange(isActive()));
703
+ watch(isActive, onActiveChange);
704
+ function onActiveChange(active) {
705
+ if (active) {
706
+ focusState.origin = document.activeElement;
707
+ return;
708
+ }
709
+ const element = card();
710
+ if (element) closedDialogs.set(element, focusState);
711
+ const target = findFocusTarget(focusState.origin);
712
+ if (hasFocus(element) && (target instanceof HTMLElement || target instanceof SVGElement)) target.focus();
713
+ }
714
+ function focusFirstElement() {
715
+ card()?.querySelector(focusable)?.focus();
716
+ }
717
+ return { focusFirstElement };
718
+ }
719
+ /**
720
+ * Element that gets the focus back when a dialog closes: its origin or, when the origin is inside
721
+ * another dialog that closed meanwhile (and can't get the focus anymore), that dialog's.
722
+ */
723
+ function findFocusTarget(origin) {
724
+ const card = origin?.closest("[data-u-dialog]");
725
+ const owner = card && closedDialogs.get(card);
726
+ return owner && !owner.isOpen() ? findFocusTarget(owner.origin) : origin;
727
+ }
728
+ /**
729
+ * Whether the focus is on the dialog or lost (on nothing or on the page's body), so it can move.
730
+ * Otherwise it moved elsewhere, e.g. to another dialog opened over this one.
731
+ */
732
+ function hasFocus(card) {
733
+ const active = document.activeElement;
734
+ if (!active) return true;
735
+ return !!card && (card.contains(active) || active.contains(card));
736
+ }
737
+ //#endregion
738
+ //#region src/components/dialogs/dialog/dialog.vue?vue&type=script&setup=true&lang.ts
739
+ const _hoisted_1$1 = ["id"];
740
+ //#endregion
741
+ //#region src/components/dialogs/dialog/dialog.vue
668
742
  var dialog_default = /* @__PURE__ */ defineComponent({
669
743
  inheritAttrs: false,
670
744
  __name: "dialog",
745
+ props: /*@__PURE__*/ mergeModels({
746
+ title: {
747
+ type: String,
748
+ required: false
749
+ },
750
+ actions: {
751
+ type: Array,
752
+ required: false
753
+ },
754
+ size: {
755
+ type: String,
756
+ required: false
757
+ },
758
+ dataTestid: {
759
+ type: String,
760
+ required: false
761
+ }
762
+ }, {
763
+ "modelValue": {
764
+ type: Boolean,
765
+ default: false
766
+ },
767
+ "modelModifiers": {}
768
+ }),
769
+ emits: ["update:modelValue"],
770
+ setup(__props) {
771
+ const props = __props;
772
+ const model = useModel(__props, "modelValue");
773
+ const maxWidths = {
774
+ small: 400,
775
+ medium: 560,
776
+ large: 800
777
+ };
778
+ const host = inject(dialogHostKey, null);
779
+ const contentId = useId();
780
+ const card = useTemplateRef("card");
781
+ const title = computed(() => props.title || void 0);
782
+ const maxWidth = computed(() => maxWidths[props.size ?? "medium"]);
783
+ const role = computed(() => host?.role() ?? "dialog");
784
+ /**
785
+ * Whether the dialog is displayed. It follows `v-model`, but turns `false` as soon as the user
786
+ * closes the dialog, while `v-model` only does once the leave transition ends.
787
+ */
788
+ const isActive = ref(model.value);
789
+ watch(model, (value) => isActive.value = value);
790
+ const { focusFirstElement } = useDialogFocus(() => isActive.value, () => card.value?.$el);
791
+ function onAfterLeave() {
792
+ model.value = false;
793
+ host?.afterLeave();
794
+ }
795
+ return (_ctx, _cache) => {
796
+ return openBlock(), createBlock(unref(VDialog), {
797
+ modelValue: isActive.value,
798
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => isActive.value = $event),
799
+ persistent: unref(host)?.persistent(),
800
+ "max-width": maxWidth.value,
801
+ role: role.value,
802
+ "aria-label": title.value,
803
+ "aria-labelledby": title.value ? void 0 : unref(contentId),
804
+ "aria-describedby": unref(host) ? unref(contentId) : void 0,
805
+ "data-testid": props.dataTestid,
806
+ scrollable: "",
807
+ onAfterEnter: unref(focusFirstElement),
808
+ onAfterLeave
809
+ }, {
810
+ default: withCtx(() => [createCommentVNode(" `data-u-dialog` identifies the card to the dialogs opened from this one "), createVNode(unref(VCard), {
811
+ ref_key: "card",
812
+ ref: card,
813
+ title: title.value,
814
+ "data-u-dialog": ""
815
+ }, createSlots({
816
+ text: withCtx(() => [createElementVNode("div", { id: unref(contentId) }, [renderSlot(_ctx.$slots, "default")], 8, _hoisted_1$1)]),
817
+ _: 2
818
+ }, [props.actions?.length ? {
819
+ name: "actions",
820
+ fn: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(props.actions, ({ label, onClick, ...action }, index) => {
821
+ return openBlock(), createBlock(button_default, mergeProps({ key: index }, { ref_for: true }, action, { onClick }), {
822
+ default: withCtx(() => [createTextVNode(toDisplayString(label), 1)]),
823
+ _: 2
824
+ }, 1040, ["onClick"]);
825
+ }), 128))]),
826
+ key: "0"
827
+ } : void 0]), 1032, ["title"])]),
828
+ _: 3
829
+ }, 8, [
830
+ "modelValue",
831
+ "persistent",
832
+ "max-width",
833
+ "role",
834
+ "aria-label",
835
+ "aria-labelledby",
836
+ "aria-describedby",
837
+ "data-testid",
838
+ "onAfterEnter"
839
+ ]);
840
+ };
841
+ }
842
+ });
843
+ //#endregion
844
+ //#region src/components/dialogs/dialog-host.vue
845
+ var dialog_host_default = /* @__PURE__ */ defineComponent({
846
+ inheritAttrs: false,
847
+ __name: "dialog-host",
671
848
  setup(__props) {
672
849
  const owner = Symbol("dialog");
673
- const titleId = useId();
674
- const messageId = useId();
675
- const actions = useTemplateRef("actions");
676
850
  const isOwner = computed(() => owners.value[0] === owner);
677
851
  const next = computed(() => isOwner.value ? requests.value[0] : void 0);
678
852
  /** Dialog on display, kept until its leave transition ends. */
679
853
  const request = shallowRef();
854
+ /**
855
+ * Changes for every dialog on display, so each one gets its own `Dialog`, open from the start.
856
+ * The next dialog can then open right after a dismissed one, whose `v-model` stays `true`.
857
+ */
858
+ const key = shallowRef();
680
859
  const isOpen = ref(false);
681
860
  const selected = shallowRef();
682
861
  const isRunning = computed(() => selected.value !== void 0);
683
- /** Element focused before the dialog opened, which gets the focus back when it closes. */
684
- let origin = null;
862
+ const actions = computed(() => {
863
+ const current = request.value;
864
+ if (!current) return void 0;
865
+ return current.actions.map((action) => ({
866
+ label: action.text,
867
+ variant: action.variant,
868
+ color: action.color,
869
+ loading: selected.value === action,
870
+ disabled: isRunning.value && selected.value !== action,
871
+ onClick: () => void select(current, action)
872
+ }));
873
+ });
874
+ provide(dialogHostKey, {
875
+ role: () => request.value?.role,
876
+ persistent: () => isRunning.value,
877
+ afterLeave: showNext
878
+ });
685
879
  onMounted(() => owners.value.push(owner));
686
880
  onUnmounted(() => owners.value = owners.value.filter((item) => item !== owner));
687
881
  watch(next, show);
688
882
  function show() {
689
883
  if (request.value) return;
690
- origin = document.activeElement;
691
884
  request.value = next.value;
885
+ key.value = Symbol();
692
886
  isOpen.value = next.value !== void 0;
693
887
  }
694
888
  function showNext() {
@@ -701,88 +895,27 @@ var dialog_default = /* @__PURE__ */ defineComponent({
701
895
  selected.value = action;
702
896
  await current.select(action?.value);
703
897
  selected.value = void 0;
704
- isOpen.value = false;
705
- if (origin instanceof HTMLElement || origin instanceof SVGElement) origin.focus();
898
+ if (action) isOpen.value = false;
706
899
  }
900
+ /** Called once a dialog closed by the user leaves, right before the next one is displayed. */
707
901
  function dismiss() {
708
902
  if (request.value) select(request.value);
709
903
  }
710
- function focusFirstAction() {
711
- actions.value?.$el.querySelector("button").focus();
712
- }
713
904
  return (_ctx, _cache) => {
714
- return openBlock(), createBlock(unref(VDialog), {
905
+ return openBlock(), createElementBlock(Fragment, null, [createCommentVNode(" PascalCase, since `<dialog>` in the template would render the native HTML element instead "), (openBlock(), createBlock(dialog_default, {
906
+ key: key.value,
715
907
  "model-value": isOpen.value,
716
- persistent: isRunning.value,
717
- role: request.value?.role,
718
- "aria-labelledby": request.value?.title ? unref(titleId) : unref(messageId),
719
- "aria-describedby": unref(messageId),
720
- "max-width": "560",
721
- "onUpdate:modelValue": dismiss,
722
- onAfterEnter: focusFirstAction,
723
- onAfterLeave: showNext
908
+ title: request.value?.title,
909
+ actions: actions.value,
910
+ "onUpdate:modelValue": dismiss
724
911
  }, {
725
- default: withCtx(() => [createCommentVNode("\n MD3 dialogs have a 24px padding, 16px between title and message, 24px between message and\n actions, and 8px between the end-aligned actions. Inside a dialog, the card parts already\n have 24px sides, 16px below the title, 24px below the message, and end-aligned actions 8px\n apart, but no prop sets the remaining paddings, hence the spacing classes.\n "), request.value ? (openBlock(), createBlock(unref(VCard), { key: 0 }, {
726
- default: withCtx(() => [
727
- request.value.title ? (openBlock(), createBlock(unref(VCardItem), {
728
- key: 0,
729
- class: "pt-6"
730
- }, {
731
- default: withCtx(() => [createCommentVNode(" Titles wrap instead of being truncated, since they're part of the message "), createVNode(unref(VCardTitle), {
732
- id: unref(titleId),
733
- class: "text-wrap"
734
- }, {
735
- default: withCtx(() => [createTextVNode(toDisplayString(request.value.title), 1)]),
736
- _: 1
737
- }, 8, ["id"])]),
738
- _: 1
739
- })) : createCommentVNode("v-if", true),
740
- createCommentVNode(" Below a title, the card already removes the message's top padding "),
741
- createVNode(unref(VCardText), {
742
- id: unref(messageId),
743
- class: normalizeClass({ "pt-6": !request.value.title })
744
- }, {
745
- default: withCtx(() => [createTextVNode(toDisplayString(request.value.message), 1)]),
746
- _: 1
747
- }, 8, ["id", "class"]),
748
- request.value.actions.length > 0 ? (openBlock(), createBlock(unref(VCardActions), {
749
- key: 1,
750
- ref_key: "actions",
751
- ref: actions,
752
- class: "px-6 pt-0 pb-6"
753
- }, {
754
- default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(request.value.actions, (action, index) => {
755
- return openBlock(), createBlock(button_default, {
756
- key: index,
757
- variant: action.variant,
758
- color: action.color,
759
- loading: selected.value === action,
760
- disabled: isRunning.value && selected.value !== action,
761
- onClick: ($event) => select(request.value, action)
762
- }, {
763
- default: withCtx(() => [createTextVNode(toDisplayString(action.text), 1)]),
764
- _: 2
765
- }, 1032, [
766
- "variant",
767
- "color",
768
- "loading",
769
- "disabled",
770
- "onClick"
771
- ]);
772
- }), 128))]),
773
- _: 1
774
- }, 512)) : createCommentVNode("v-if", true)
775
- ]),
776
- _: 1
777
- })) : createCommentVNode("v-if", true)]),
912
+ default: withCtx(() => [createTextVNode(toDisplayString(request.value?.message), 1)]),
778
913
  _: 1
779
914
  }, 8, [
780
915
  "model-value",
781
- "persistent",
782
- "role",
783
- "aria-labelledby",
784
- "aria-describedby"
785
- ]);
916
+ "title",
917
+ "actions"
918
+ ]))], 2112);
786
919
  };
787
920
  }
788
921
  });
@@ -849,7 +982,7 @@ var main_default = /* @__PURE__ */ defineComponent({
849
982
  default: withCtx(() => [
850
983
  renderSlot(_ctx.$slots, "default"),
851
984
  createVNode(toast_default),
852
- createVNode(dialog_default)
985
+ createVNode(dialog_host_default)
853
986
  ]),
854
987
  _: 3
855
988
  })]),
@@ -1983,4 +2116,4 @@ var data_set_item_default = /* @__PURE__ */ defineComponent({
1983
2116
  }
1984
2117
  });
1985
2118
  //#endregion
1986
- export { autocomplete_field_default as UAutocompleteField, button_default as UButton, checkbox_default as UCheckbox, column_default as UColumn, container_default as UContainer, data_set_default as UDataSet, data_set_item_default as UDataSetItem, date_time_field_default as UDateTimeField, details_default as UDetails, form_default as UForm, main_default as UMain, row_default as URow, section_default as USection, section_content_default as USectionContent, table_default as UTable, text_field_default as UTextField };
2119
+ export { autocomplete_field_default as UAutocompleteField, button_default as UButton, checkbox_default as UCheckbox, column_default as UColumn, container_default as UContainer, data_set_default as UDataSet, data_set_item_default as UDataSetItem, date_time_field_default as UDateTimeField, details_default as UDetails, dialog_default as UDialog, form_default as UForm, main_default as UMain, row_default as URow, section_default as USection, section_content_default as USectionContent, table_default as UTable, text_field_default as UTextField };
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { t as localeOptions } from "./locale-CM4o8cD_.js";
1
2
  import "@mdi/font/css/materialdesignicons.css";
2
3
  import "vuetify/styles";
3
4
  import "./styles/fonts/fonts.css";
@@ -9,26 +10,29 @@ import "vue";
9
10
  * @returns an instance to be used with [app.use](https://vuejs.org/guide/essentials/application.html#the-application-instance)
10
11
  */
11
12
  function createUimed() {
12
- return createVuetify({ theme: {
13
- defaultTheme: "light",
14
- themes: { light: {
15
- dark: false,
16
- colors: {
17
- background: "#EAEDF0",
18
- surface: "#FCFCFD",
19
- primary: "#00995D",
20
- secondary: "#FCFCFD",
21
- success: "#A9EFC5",
22
- warning: "#FFE596",
23
- error: "#D92D20",
24
- info: "#9ACBE5"
25
- },
26
- variables: {
27
- "border-color": "#D0D5DD",
28
- "font-body": "\"Unimed Slab\", sans-serif"
29
- }
30
- } }
31
- } });
13
+ return createVuetify({
14
+ locale: localeOptions,
15
+ theme: {
16
+ defaultTheme: "light",
17
+ themes: { light: {
18
+ dark: false,
19
+ colors: {
20
+ background: "#EAEDF0",
21
+ surface: "#FCFCFD",
22
+ primary: "#00995D",
23
+ secondary: "#FCFCFD",
24
+ success: "#A9EFC5",
25
+ warning: "#FFE596",
26
+ error: "#D92D20",
27
+ info: "#9ACBE5"
28
+ },
29
+ variables: {
30
+ "border-color": "#D0D5DD",
31
+ "font-body": "\"Unimed Slab\", sans-serif"
32
+ }
33
+ } }
34
+ }
35
+ });
32
36
  }
33
37
  //#endregion
34
38
  export { createUimed };
@@ -0,0 +1,12 @@
1
+ import { pt } from "vuetify/locale";
2
+ //#region src/consts/locale.ts
3
+ /**
4
+ * Language of the texts the components generate on their own, such as
5
+ * screen reader announcements and pagination labels. Fixed in pt-BR.
6
+ */
7
+ const localeOptions = {
8
+ locale: "pt-BR",
9
+ messages: { "pt-BR": pt }
10
+ };
11
+ //#endregion
12
+ export { localeOptions as t };
package/dist/unit-test.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { t as localeOptions } from "./locale-CM4o8cD_.js";
1
2
  import { createVuetify } from "vuetify";
2
3
  import * as components from "vuetify/components";
3
4
  import * as directives from "vuetify/directives";
@@ -10,7 +11,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
10
11
  //#region src/unit-test.ts
11
12
  const vuetify = createVuetify({
12
13
  components,
13
- directives
14
+ directives,
15
+ locale: localeOptions
14
16
  });
15
17
  /**
16
18
  * Vue Test Utils plugin for Uimed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdom/uimed-vue",
3
- "version": "2.0.0-beta.15",
3
+ "version": "2.0.0-beta.17",
4
4
  "description": "A ui framework for NEXDOM applications.",
5
5
  "keywords": [
6
6
  "lib",