@nexdom/uimed-vue 2.0.0-beta.16 → 2.0.0-beta.18

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
 
@@ -200,8 +203,19 @@ Use an existing composable (e.g. `use-toast`) as the reference, and deliver in t
200
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()`.
201
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.
202
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.
203
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`).
204
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
+
205
219
  ## Library documentation and dependencies
206
220
 
207
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;
@@ -429,8 +429,8 @@ declare const __VLS_export$12: import("vue", { with: { "resolution-mode": "impor
429
429
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
430
430
  }>, {
431
431
  variant: TextFieldVariant;
432
- type: TextFieldType;
433
432
  clearable: boolean;
433
+ type: TextFieldType;
434
434
  }, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>;
435
435
  //#endregion
436
436
  //#region src/components/inputs/checkbox/types.d.ts
@@ -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
  };
@@ -651,6 +651,22 @@ type __VLS_WithSlots$7<T, S> = T & {
651
651
  * Props exposed by the {@link Row} component.
652
652
  */
653
653
  type RowProps = {
654
+ /**
655
+ * Vertical alignment of the row's columns, relative to the tallest column on the same line.
656
+ * Available options are:
657
+ * - `"start"`: aligns the columns to the top;
658
+ * - `"center"`: centers the columns vertically;
659
+ * - `"end"`: aligns the columns to the bottom;
660
+ * - `"stretch"`: stretches every column to the height of the tallest one.
661
+ *
662
+ * Use `"stretch"` to keep side by side columns at the same height, e.g. cards made of
663
+ * `USection`s with `fullHeight`.
664
+ *
665
+ * Columns sit side by side only from the `sm` breakpoint up. Below it, each column takes a
666
+ * whole line, so the alignment has no visible effect.
667
+ * @default "start"
668
+ */
669
+ align?: RowAlign;
654
670
  /**
655
671
  * Renders the row as a list (`<ul>`), without the list's default indentation
656
672
  * and bullets, so assistive technologies announce its columns as the items
@@ -664,6 +680,7 @@ type RowProps = {
664
680
  */
665
681
  dataTestid?: string;
666
682
  };
683
+ type RowAlign = "start" | "center" | "end" | "stretch";
667
684
  //#endregion
668
685
  //#region src/components/grid/row/row.vue.d.ts
669
686
  /**
@@ -673,7 +690,7 @@ type RowProps = {
673
690
  * ```vue
674
691
  * <template>
675
692
  * <u-container>
676
- * <u-row>
693
+ * <u-row align="center">
677
694
  * <!-- uimed-column -->
678
695
  * </u-row>
679
696
  * </u-container>
@@ -682,11 +699,13 @@ type RowProps = {
682
699
  *
683
700
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/layout | Row Guide}
684
701
  */
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>, {
702
+ declare const _default$12: typeof __VLS_export$9;
703
+ 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<{}>, {
704
+ align: RowAlign;
705
+ }, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
687
706
  default?: (props: {}) => any;
688
707
  }>;
689
- type __VLS_WithSlots$6<T, S> = T & {
708
+ type __VLS_WithSlots$7<T, S> = T & {
690
709
  new (): {
691
710
  $slots: S;
692
711
  };
@@ -738,13 +757,13 @@ type Cols = "auto" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10"
738
757
  *
739
758
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/layout | Column Guide}
740
759
  */
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<{}>, {
760
+ declare const _default$3: typeof __VLS_export$8;
761
+ 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
762
  cols: "12" | "auto" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "11";
744
763
  }, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
745
764
  default?: (props: {}) => any;
746
765
  }>;
747
- type __VLS_WithSlots$5<T, S> = T & {
766
+ type __VLS_WithSlots$6<T, S> = T & {
748
767
  new (): {
749
768
  $slots: S;
750
769
  };
@@ -781,15 +800,15 @@ type FormProps = {
781
800
  *
782
801
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/form | Form Guide}
783
802
  */
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, {} & {
803
+ declare const _default$10: typeof __VLS_export$7;
804
+ 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
805
  submit: (event: SubmitEvent) => any;
787
806
  }, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<FormProps> & Readonly<{
788
807
  onSubmit?: ((event: SubmitEvent) => any) | undefined;
789
808
  }>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
790
809
  default?: (props: {}) => any;
791
810
  }>;
792
- type __VLS_WithSlots$4<T, S> = T & {
811
+ type __VLS_WithSlots$5<T, S> = T & {
793
812
  new (): {
794
813
  $slots: S;
795
814
  };
@@ -811,11 +830,11 @@ type __VLS_WithSlots$4<T, S> = T & {
811
830
  *
812
831
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/section | Section Guide}
813
832
  */
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>, {
833
+ declare const _default$14: typeof __VLS_export$6;
834
+ 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
835
  default?: (props: {}) => any;
817
836
  }>;
818
- type __VLS_WithSlots$3<T, S> = T & {
837
+ type __VLS_WithSlots$4<T, S> = T & {
819
838
  new (): {
820
839
  $slots: S;
821
840
  };
@@ -853,6 +872,9 @@ type SectionProps = {
853
872
  fullWidth?: boolean;
854
873
  /**
855
874
  * Makes the section occupy 100% of its parent's height.
875
+ *
876
+ * Inside a `URow`, set its `align` to `"stretch"` so the section takes the height of the
877
+ * tallest column on the same line.
856
878
  * @default false
857
879
  */
858
880
  fullHeight?: boolean;
@@ -897,11 +919,11 @@ interface SectionAction extends ButtonProps {
897
919
  *
898
920
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/section | Section Guide}
899
921
  */
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>, {
922
+ declare const _default$13: typeof __VLS_export$5;
923
+ 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
924
  default?: (props: {}) => any;
903
925
  }>;
904
- type __VLS_WithSlots$2<T, S> = T & {
926
+ type __VLS_WithSlots$3<T, S> = T & {
905
927
  new (): {
906
928
  $slots: S;
907
929
  };
@@ -972,8 +994,8 @@ type TableProps = {
972
994
  *
973
995
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/table | Table Guide}
974
996
  */
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>;
997
+ declare const _default$15: typeof __VLS_export$4;
998
+ 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
999
  //#endregion
978
1000
  //#region src/components/details/types.d.ts
979
1001
  type DetailsVariant = "primary" | "secondary";
@@ -1020,11 +1042,11 @@ type DetailsProps = {
1020
1042
  *
1021
1043
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/details | Details Guide}
1022
1044
  */
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>, {
1045
+ declare const _default$8: typeof __VLS_export$3;
1046
+ 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
1047
  default?: (props: {}) => any;
1026
1048
  }>;
1027
- type __VLS_WithSlots$1<T, S> = T & {
1049
+ type __VLS_WithSlots$2<T, S> = T & {
1028
1050
  new (): {
1029
1051
  $slots: S;
1030
1052
  };
@@ -1125,8 +1147,8 @@ type DataSetSlots<T extends object> = {
1125
1147
  *
1126
1148
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/data-set | DataSet Guide}
1127
1149
  */
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<{
1150
+ declare const _default$5: typeof __VLS_export$2;
1151
+ 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
1152
  props: import("vue").PublicProps & __VLS_PrettifyLocal<(DataSetProps<T> & {
1131
1153
  page?: number;
1132
1154
  search?: string;
@@ -1201,12 +1223,89 @@ type DataSetItemSlots = {
1201
1223
  *
1202
1224
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/data-set | DataSet Guide}
1203
1225
  */
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>;
1226
+ declare const _default$6: typeof __VLS_export$1;
1227
+ 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>;
1228
+ type __VLS_WithSlots$1<T, S> = T & {
1229
+ new (): {
1230
+ $slots: S;
1231
+ };
1232
+ };
1233
+ //#endregion
1234
+ //#region src/components/dialogs/dialog/types.d.ts
1235
+ type DialogSize = "small" | "medium" | "large";
1236
+ /**
1237
+ * Props exposed by the {@link Dialog} component.
1238
+ */
1239
+ type DialogProps = {
1240
+ /**
1241
+ * Plain text displayed as the dialog's title, in a single line. HTML isn't interpreted.
1242
+ * Also used as the dialog's accessible name. Without it, the content is the accessible name.
1243
+ */
1244
+ title?: string;
1245
+ /**
1246
+ * Buttons displayed at the bottom of the dialog, right-aligned, in the given order.
1247
+ * Place the main action last. When undefined or empty, no actions area is rendered.
1248
+ * Clicking an action doesn't close the dialog: set `v-model` to `false` in `onClick`.
1249
+ */
1250
+ actions?: DialogButtonAction[];
1251
+ /**
1252
+ * Maximum width of the dialog.
1253
+ * One of `small` (400px), `medium` (560px), or `large` (800px).
1254
+ * @default "medium"
1255
+ */
1256
+ size?: DialogSize;
1257
+ /**
1258
+ * Component id to use on automated tests.
1259
+ */
1260
+ dataTestid?: string;
1261
+ };
1262
+ /**
1263
+ * A single action rendered inside {@link Dialog}'s actions area. Same shape as `USection`'s
1264
+ * actions.
1265
+ */
1266
+ interface DialogButtonAction extends ButtonProps {
1267
+ /**
1268
+ * Text displayed on the action button.
1269
+ */
1270
+ label: string;
1271
+ /**
1272
+ * Called when the action button is clicked. The dialog stays open.
1273
+ */
1274
+ onClick?: (event: MouseEvent) => void;
1275
+ }
1276
+ //#endregion
1277
+ //#region src/components/dialogs/dialog/dialog.vue.d.ts
1278
+ /**
1279
+ * Dialog component to display content in a modal window, over the page, until it's closed.
1280
+ * Its title and actions stay in place while the content scrolls.
1281
+ *
1282
+ * @example
1283
+ * ```vue
1284
+ * <template>
1285
+ * <u-dialog v-model="open" title="Title" :actions="[{ label: 'Close', onClick: () => (open = false) }]">
1286
+ * Content
1287
+ * </u-dialog>
1288
+ * </template>
1289
+ * ```
1290
+ *
1291
+ * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/dialog | Dialog Guide}
1292
+ */
1293
+ declare const _default$9: typeof __VLS_export;
1294
+ declare const __VLS_export: __VLS_WithSlots<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<DialogProps & {
1295
+ modelValue?: boolean;
1296
+ }, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {
1297
+ "update:modelValue": (value: boolean) => any;
1298
+ }, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<DialogProps & {
1299
+ modelValue?: boolean;
1300
+ }> & Readonly<{
1301
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
1302
+ }>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
1303
+ default?: (props: {}) => any;
1304
+ }>;
1206
1305
  type __VLS_WithSlots<T, S> = T & {
1207
1306
  new (): {
1208
1307
  $slots: S;
1209
1308
  };
1210
1309
  };
1211
1310
  //#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 };
1311
+ 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,7 +1,7 @@
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";
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";
3
3
  import { pt } from "vuetify/locale";
4
- 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 { 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
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) {
@@ -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
  })]),
@@ -1325,6 +1458,11 @@ var row_default = /* @__PURE__ */ defineComponent({
1325
1458
  inheritAttrs: false,
1326
1459
  __name: "row",
1327
1460
  props: {
1461
+ align: {
1462
+ type: String,
1463
+ required: false,
1464
+ default: "start"
1465
+ },
1328
1466
  list: {
1329
1467
  type: Boolean,
1330
1468
  required: false
@@ -1336,12 +1474,19 @@ var row_default = /* @__PURE__ */ defineComponent({
1336
1474
  },
1337
1475
  setup(__props) {
1338
1476
  const props = __props;
1477
+ const rowAlignToClass = {
1478
+ start: "align-start",
1479
+ center: "align-center",
1480
+ end: "align-end",
1481
+ stretch: "align-stretch"
1482
+ };
1483
+ const alignClass = computed(() => rowAlignToClass[props.align]);
1339
1484
  const tag = computed(() => props.list ? "ul" : void 0);
1340
1485
  const listClasses = computed(() => props.list ? "pa-0 ma-0" : void 0);
1341
1486
  return (_ctx, _cache) => {
1342
1487
  return openBlock(), createBlock(unref(VRow), {
1343
1488
  tag: tag.value,
1344
- class: normalizeClass(listClasses.value),
1489
+ class: normalizeClass([alignClass.value, listClasses.value]),
1345
1490
  "data-testid": props.dataTestid
1346
1491
  }, {
1347
1492
  default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
@@ -1892,7 +2037,10 @@ var data_set_default = /* @__PURE__ */ defineComponent({
1892
2037
  type: "search",
1893
2038
  label: "Pesquisar"
1894
2039
  }, null, 8, ["modelValue"])) : createCommentVNode("v-if", true)]),
1895
- default: withCtx(({ items: pageItems }) => [createVNode(row_default, { list: "" }, {
2040
+ default: withCtx(({ items: pageItems }) => [createCommentVNode(" Stretched, so the items of the same line (e.g. full-height cards) share its height "), createVNode(row_default, {
2041
+ list: "",
2042
+ align: "stretch"
2043
+ }, {
1896
2044
  default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(pageItems, (pageItem, index) => {
1897
2045
  return openBlock(), createBlock(column_default, {
1898
2046
  key: index,
@@ -1983,4 +2131,4 @@ var data_set_item_default = /* @__PURE__ */ defineComponent({
1983
2131
  }
1984
2132
  });
1985
2133
  //#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 };
2134
+ 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdom/uimed-vue",
3
- "version": "2.0.0-beta.16",
3
+ "version": "2.0.0-beta.18",
4
4
  "description": "A ui framework for NEXDOM applications.",
5
5
  "keywords": [
6
6
  "lib",