@nexdom/uimed-vue 2.0.0-beta.8 → 2.0.0-beta.9

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
@@ -30,7 +30,7 @@ Add the Vite plugin:
30
30
 
31
31
  ```ts
32
32
  // vite.config.ts
33
- import { vitePluginUimed } from "@nexdom/uimed-vue/plugins.ts";
33
+ import { vitePluginUimed } from "@nexdom/uimed-vue/plugins";
34
34
 
35
35
  // ...
36
36
 
@@ -55,18 +55,18 @@ createApp(App).use(uimed).mount("#app");
55
55
 
56
56
  ### Usage
57
57
 
58
- Place the `Root` component at the top of `App.vue`, then add other components as needed:
58
+ Place the `UMain` component at the top of `App.vue`, then add other components as needed:
59
59
 
60
60
  ```vue
61
61
  <!-- App.vue -->
62
62
  <template>
63
- <root>
63
+ <u-main>
64
64
  <!-- ... -->
65
- </root>
65
+ </u-main>
66
66
  </template>
67
67
 
68
68
  <script setup lang="ts">
69
- import { Root } from "@nexdom/uimed-vue/components";
69
+ import { UMain } from "@nexdom/uimed-vue/components";
70
70
  </script>
71
71
  ```
72
72
 
@@ -102,24 +102,70 @@ vp env setup # create shims like vpr and vpx
102
102
  vp install # install dependencies
103
103
  ```
104
104
 
105
+ On Windows, prefer VSCode's "Dev Containers: Clone Repository in Container Volume" over opening a folder cloned on the host:
106
+
107
+ - A host clone with `core.autocrlf=true` checks files out with CRLF, and `vpr check` then reports formatting issues on every file. `.gitattributes` enforces LF, but clones made before it was added need `git rm -rq --cached . && git reset --hard` to be re-normalized (commit or stash local changes first, since `reset --hard` discards them).
108
+ - A host folder bind-mounted into the container is roughly 10x slower (unit tests alone go from seconds to minutes), which makes Stryker's initial test run time out.
109
+
105
110
  ## Commands
106
111
 
107
- - `vpr check` — lint, formatter, and type-check (requires `build`/`pack` to have run first for the type-check step)
112
+ - `vpr check` — lint, formatter, and type-check (builds the library first, since the type-check reads `dist`)
108
113
  - `vp test --coverage` — unit tests with coverage (Vitest, jsdom, 100% coverage threshold enforced)
109
- - `vpr test:mutations` — mutation tests (Stryker; thresholds: high 100, low 95, break 95)
110
- - `vpr test:e2e` — E2E tests (Playwright, runs against the built docs preview site)
114
+ - `vpr test:mutations` — mutation tests (Stryker; thresholds: high 100, low 100, break 100). It starts one test runner per CPU core; on machines with limited memory, run `vpx stryker run --concurrency 4` instead
115
+ - `vpr test:e2e` — E2E tests (Playwright, runs against the built docs preview site). Outside CI it reuses any server already on port 4173, which is also `vpr docs:dev`'s port: stop the dev server first, or the tests run against it instead of the built site
111
116
  - `vpr depcruise` — architecture/dependency rules (dependency-cruiser)
112
117
  - `vp pack` / `vpr build` — build the library
113
118
  - `vpr docs` / `vpr docs:dev` — run docs site (imports the lib from `dist`, not `src` — run `vpr dev` in a second terminal to keep `dist` updated while iterating)
114
119
 
115
120
  CI (`.github/workflows/ci.yml`) runs, in order: commitlint on PR commits, `vp pack`, `vpr check`, `vpr depcruise`, `vp test --coverage`, `vpr test:mutations`, `vpr test:e2e`. Match this locally before opening a PR.
116
121
 
122
+ ### Focused runs while iterating
123
+
124
+ The full suite takes minutes; while working on a single component or composable, scope each step to it (`button` below is an example) and run the full suite only before opening a PR:
125
+
126
+ ```bash
127
+ # Unit tests of one or more folders
128
+ vp test src/components/button
129
+
130
+ # Same, with coverage limited to the files you changed. Without `--coverage.include`, files that
131
+ # are only imported (e.g. the children of `main.vue`) count as uncovered and fail the 100% threshold
132
+ vp test src/components/button --coverage --coverage.include="src/components/button/**"
133
+
134
+ # Mutation tests of specific files. List several files in a single comma-separated `--mutate`
135
+ # (repeating the flag keeps only the last one)
136
+ vpx stryker run --mutate "src/components/button/button.vue,src/composables/button/button.ts"
137
+
138
+ # One E2E spec
139
+ vpr test:e2e e2e/components/button.spec.ts
140
+ ```
141
+
142
+ ## Adding a new component
143
+
144
+ Use an existing component (e.g. `button`) as the reference, and deliver all of the following in the same PR:
145
+
146
+ 1. `src/components/<name>/<name>.vue` (following the pattern in [Code conventions](#code-conventions)), `types.ts` and `__tests__/<name>.test.ts`.
147
+ 2. The `U`-prefixed export in `src/components/index.ts`.
148
+ 3. A usage guide at `docs/guide/components/<name>.md`, with `<demo>` examples (and a `<playground>` section when the component has configurable props), and an API reference at `docs/api/components/<name>.md` (props, events and slots tables). Both in Portuguese.
149
+ 4. Both pages registered in the sidebar at `docs/.vitepress/config.ts`.
150
+ 5. `e2e/components/<name>.spec.ts` covering the guide's interactive examples and a "UI consistency" screenshot check (plus accessible snapshots where relevant). Commit the generated files under `__snapshots__`/`__screenshot__`.
151
+ 6. The full CI sequence passing locally.
152
+
153
+ ## Adding a new composable
154
+
155
+ Use an existing composable (e.g. `use-toast`) as the reference, and deliver in the same PR:
156
+
157
+ 1. `src/composables/<group>/<name>.ts` and `__tests__/<name>.test.ts`, plus the types in the group's `types.ts`. If it relies on an internal component (as `useToast` relies on the internal `Toast` rendered by `UMain`), that component follows the component layout but isn't exported.
158
+ 2. The export in `src/composables/index.ts`, and its assertion in `src/composables/__tests__/index.test.ts`.
159
+ 3. A usage guide at `docs/guide/composables/<name>.md` and an API reference at `docs/api/composables/<name>.md`, both in Portuguese, registered in both sidebars at `docs/.vitepress/config.ts` and listed in `docs/api/index.md` and in the table of `docs/guide/index.md`. If it relies on `UMain` (like `useToast`), also mention it in `UMain`'s guide and API pages.
160
+ 4. `e2e/composables/<name>.spec.ts` covering the guide's examples and a "UI consistency" screenshot check.
161
+ 5. The full CI sequence passing locally.
162
+
117
163
  ## Code conventions
118
164
 
119
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.
120
166
  - `docs` must not mention Vuetify
121
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`).
122
- - Never write CSS, classes or any kind of styling. Always use component props.
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.
123
169
  - Path aliases: `@/*` → `src/*`, `@e2e/*` → `e2e/*`.
124
170
  - Every public component follows this pattern to block access to internals and give it an editor-hover description:
125
171
 
@@ -139,8 +185,9 @@ CI (`.github/workflows/ci.yml`) runs, in order: commitlint on PR commits, `vp pa
139
185
  </script>
140
186
  ```
141
187
 
142
- - Component layout: `src/components/<name>/<name>.vue`, `types.ts` for prop/option types, `__tests__/<name>.test.ts` for unit tests. Composables follow the same shape under `src/composables/<name>/`.
143
- - Public components/composables/types are re-exported from `src/components/index.ts` and `src/composables/index.ts`.
188
+ - 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
+ - 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`).
190
+ - 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)).
144
191
  - 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.
145
192
  - 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.
146
193
  - Known workarounds (see CONTRIBUTING.md before touching related config): `stryker-vue-ignorer` patches a Stryker/Vue macro-hoisting issue; `vue-tsc` is used for type-check instead of Vite+'s built-in one due to an oxlint/Vue support gap. Both are meant to be removed once their upstream issues are fixed — don't build further on top of them without checking if they're still needed.
@@ -149,8 +196,34 @@ CI (`.github/workflows/ci.yml`) runs, in order: commitlint on PR commits, `vp pa
149
196
 
150
197
  - Unit tests use Vitest + `@vue/test-utils`, with `vueTestUtilsPluginUimed()` from `@/unit-test.ts` to mount a Vuetify instance.
151
198
  - Global unit test setup (`src/__tests__/setup.ts`) stubs `visualViewport`, uses fake timers, and silences `console.error/warn/log`.
152
- - Coverage threshold is 100%; mutation testing threshold is 100% (break at 100). Don't add code paths without covering tests.
199
+ - 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
+ - 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.
153
201
  - 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.
202
+ - 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
+
204
+ ## Library documentation and dependencies
205
+
206
+ 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.
207
+
208
+ - Before using an API this repo doesn't use yet, or implementing something from scratch, look it up in the version declared in `package.json`. The project's `.mcp.json` provides two servers for that:
209
+ - `context7`, with these library IDs: Vue `/websites/vuejs`, Vuetify `/websites/vuetifyjs_en`, Vue Router `/websites/router_vuejs`, Vite+ `/websites/viteplus_dev`, Vitest `/vitest-dev/vitest`, Vue Test Utils `/vuejs/test-utils`, Playwright `/microsoft/playwright`, VitePress `/vuejs/vitepress`, Stryker `/stryker-mutator/stryker-js`.
210
+ - `vuetify`, Vuetify's own server, for component and composable APIs and release notes. Its tools that create or update bins, links, playgrounds or bug reports publish content outside the repo and are denied in `.claude/settings.json`.
211
+ - Queries to both servers leave your machine: describe what you need in generic terms and never include source code or business rules.
212
+ - Before adding a dependency, check whether Vuetify, `@nexdom/shared` or the current dependencies already cover the need. If not, confirm with the requester, check the package's docs, maintenance and license, and declare runtime dependencies as `peerDependencies` (enforced by dependency-cruiser's `use-peer-deps` rule).
213
+ - Vuetify is an internal detail: consumers use the library's props, and `docs` never mention Vuetify.
214
+
215
+ ## AI agents
216
+
217
+ Besides this file, the repo ships Claude Code subagents in `.claude/agents/`:
218
+
219
+ - `issue-planner`: turns an issue into an API proposal plus open questions, before any code is written.
220
+ - `issue-implementer`: implements an issue end to end (source, tests, docs, E2E).
221
+ - `code-reviewer`: reviews a branch or PR against these conventions, without changing code.
222
+ - `dependency-updater`: evaluates and applies dependency updates, such as Dependabot PRs.
223
+
224
+ Issues and PRs live in `nexdom-healthtech/uimed-vue`. Pass `-R nexdom-healthtech/uimed-vue` to `gh`, since clones from forks have issues disabled, and read issues with `gh issue view <number> -R nexdom-healthtech/uimed-vue --json title,body,comments` (`--comments` prints nothing in non-interactive shells).
225
+
226
+ To resolve an issue, run `/resolve-issue <number>` (`.claude/skills/resolve-issue/`). It chains planner, implementer and reviewer, asks you about open questions and waits for your approval of the public API before any code is written.
154
227
 
155
228
  ## Git workflow
156
229
 
package/README.md CHANGED
@@ -41,7 +41,7 @@ Add Vite config:
41
41
 
42
42
  ```ts
43
43
  // vite.config.ts
44
- import { vitePluginUimed } from "@nexdom/uimed-vue/plugins.ts";
44
+ import { vitePluginUimed } from "@nexdom/uimed-vue/plugins";
45
45
 
46
46
  /// ...
47
47
 
@@ -1,4 +1,4 @@
1
- import { t as ColorVariant } from "./types-BPXXtbmW.js";
1
+ import { t as ButtonProps } from "./types-HPMP8IrV.js";
2
2
  import { RouteLocationRaw } from "vue-router";
3
3
  //#region src/components/app-bar/types.d.ts
4
4
  /**
@@ -189,50 +189,6 @@ type __VLS_WithSlots$8<T, S> = T & {
189
189
  };
190
190
  };
191
191
  //#endregion
192
- //#region src/components/button/types.d.ts
193
- type ButtonVariant = "primary" | "secondary" | "ghost";
194
- /**
195
- * Props exposed by the {@link Button} component.
196
- */
197
- type ButtonProps = {
198
- /**
199
- * Applies a distinct style variation to the button.
200
- * One of `primary`, `secondary`, or `ghost`.
201
- * @default "primary"
202
- */
203
- variant?: ButtonVariant;
204
- /**
205
- * Applies a distinct behavior to the button.
206
- * One of `button` or `submit`.
207
- * @default "button"
208
- */
209
- type?: "submit" | "button";
210
- /**
211
- * Applies a color to the button.
212
- * One of `primary`, `secondary`, `positive`, `informative`, `caution`, or `danger`.
213
- * @default "primary"
214
- */
215
- color?: ColorVariant;
216
- /**
217
- * Removes the ability to click or target the button.
218
- * @default false
219
- */
220
- disabled?: boolean;
221
- /**
222
- * Displays a loading indicator on the button, disabling interaction.
223
- * @default false
224
- */
225
- loading?: boolean;
226
- /**
227
- * Associated [form](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/form) id.
228
- */
229
- form?: string;
230
- /**
231
- * Component id to use on automated tests.
232
- */
233
- dataTestid?: string;
234
- };
235
- //#endregion
236
192
  //#region src/components/button/button.vue.d.ts
237
193
  /**
238
194
  * Button component to be used throughout the application.
@@ -1,6 +1,6 @@
1
- import { r as useVuetifyColor, t as messages } from "./use-toast-CACuYhK_.js";
2
- import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, isRef, mergeModels, mergeProps, normalizeClass, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, toValue, unref, useModel, withCtx, withModifiers } from "vue";
3
- import { VApp, VAppBar, VAppBarNavIcon, VAppBarTitle, VAutocomplete, VAvatar, VBadge, VBtn, VCard, VCardActions, VCardText, VCheckboxBtn, VCol, VCombobox, VContainer, VDivider, VExpansionPanel, VExpansionPanelText, VExpansionPanelTitle, VExpansionPanels, VForm, VIcon, VIconBtn, VImg, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VMain, VMenu, VNavigationDrawer, VRow, VSkeletonLoader, VSnackbarQueue, VTable, VTextField } from "vuetify/components";
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, onUnmounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toValue, unref, useId, useModel, 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, VDialog, VDivider, VExpansionPanel, VExpansionPanelText, VExpansionPanelTitle, VExpansionPanels, VForm, VIcon, VIconBtn, VImg, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VMain, VMenu, VNavigationDrawer, VRow, VSkeletonLoader, VSnackbarQueue, VTable, VTextField } from "vuetify/components";
4
4
  import { formatDateTime, isEmail, isEmpty, isPhone, isUrl, toPeriodInterval } from "@nexdom/shared/utils";
5
5
  //#region src/composables/navigation/use-route-props.ts
6
6
  function useRouteProps(route) {
@@ -549,71 +549,6 @@ var toast_default = /* @__PURE__ */ defineComponent({
549
549
  }
550
550
  });
551
551
  //#endregion
552
- //#region src/components/main/main.vue
553
- var main_default = /* @__PURE__ */ defineComponent({
554
- inheritAttrs: false,
555
- __name: "main",
556
- props: /*@__PURE__*/ mergeModels({
557
- dataTestid: {
558
- type: String,
559
- required: false
560
- },
561
- logo: {
562
- type: String,
563
- required: false
564
- },
565
- appBar: {
566
- type: Object,
567
- required: false
568
- },
569
- navigationMenu: {
570
- type: Object,
571
- required: false
572
- }
573
- }, {
574
- "notificationsOpen": {
575
- type: Boolean,
576
- default: false
577
- },
578
- "notificationsOpenModifiers": {}
579
- }),
580
- emits: ["update:notificationsOpen"],
581
- setup(__props) {
582
- const props = __props;
583
- const appBarProps = computed(() => props.appBar ? {
584
- ...props.appBar,
585
- logo: props.logo,
586
- navigation: props.navigationMenu !== void 0
587
- } : void 0);
588
- const notificationsOpen = useModel(__props, "notificationsOpen");
589
- const navigationMenuOpen = ref(false);
590
- return (_ctx, _cache) => {
591
- return openBlock(), createBlock(unref(VApp), { "data-testid": props.dataTestid }, {
592
- default: withCtx(() => [
593
- appBarProps.value ? (openBlock(), createBlock(app_bar_default, mergeProps({ key: 0 }, appBarProps.value, {
594
- "notifications-open": notificationsOpen.value,
595
- "onUpdate:notificationsOpen": _cache[0] || (_cache[0] = ($event) => notificationsOpen.value = $event),
596
- "navigation-open": navigationMenuOpen.value,
597
- "onUpdate:navigationOpen": _cache[1] || (_cache[1] = ($event) => navigationMenuOpen.value = $event)
598
- }), null, 16, ["notifications-open", "navigation-open"])) : createCommentVNode("v-if", true),
599
- props.navigationMenu ? (openBlock(), createBlock(navigation_menu_default, mergeProps({ key: 1 }, props.navigationMenu, {
600
- modelValue: navigationMenuOpen.value,
601
- "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => navigationMenuOpen.value = $event)
602
- }), null, 16, ["modelValue"])) : createCommentVNode("v-if", true),
603
- createVNode(unref(VMain), null, {
604
- default: withCtx(() => [createVNode(container_default, null, {
605
- default: withCtx(() => [renderSlot(_ctx.$slots, "default"), createVNode(toast_default)]),
606
- _: 3
607
- })]),
608
- _: 3
609
- })
610
- ]),
611
- _: 3
612
- }, 8, ["data-testid"]);
613
- };
614
- }
615
- });
616
- //#endregion
617
552
  //#region src/composables/button/button.ts
618
553
  const buttonVariantToVuetifyVariant = {
619
554
  primary: "elevated",
@@ -706,6 +641,198 @@ var button_default = /* @__PURE__ */ defineComponent({
706
641
  }
707
642
  });
708
643
  //#endregion
644
+ //#region src/components/dialogs/dialog.vue
645
+ var dialog_default = /* @__PURE__ */ defineComponent({
646
+ inheritAttrs: false,
647
+ __name: "dialog",
648
+ setup(__props) {
649
+ const owner = Symbol("dialog");
650
+ const titleId = useId();
651
+ const messageId = useId();
652
+ const actions = useTemplateRef("actions");
653
+ const isOwner = computed(() => owners.value[0] === owner);
654
+ const next = computed(() => isOwner.value ? requests.value[0] : void 0);
655
+ /** Dialog on display, kept until its leave transition ends. */
656
+ const request = shallowRef();
657
+ const isOpen = ref(false);
658
+ const selected = shallowRef();
659
+ const isRunning = computed(() => selected.value !== void 0);
660
+ /** Element focused before the dialog opened, which gets the focus back when it closes. */
661
+ let origin = null;
662
+ onMounted(() => owners.value.push(owner));
663
+ onUnmounted(() => owners.value = owners.value.filter((item) => item !== owner));
664
+ watch(next, show);
665
+ function show() {
666
+ if (request.value) return;
667
+ origin = document.activeElement;
668
+ request.value = next.value;
669
+ isOpen.value = next.value !== void 0;
670
+ }
671
+ function showNext() {
672
+ request.value = void 0;
673
+ show();
674
+ }
675
+ async function select(current, action) {
676
+ if (isRunning.value) return;
677
+ requests.value.shift();
678
+ selected.value = action;
679
+ await current.select(action?.value);
680
+ selected.value = void 0;
681
+ isOpen.value = false;
682
+ if (origin instanceof HTMLElement || origin instanceof SVGElement) origin.focus();
683
+ }
684
+ function dismiss() {
685
+ if (request.value) select(request.value);
686
+ }
687
+ function focusFirstAction() {
688
+ actions.value?.$el.querySelector("button").focus();
689
+ }
690
+ return (_ctx, _cache) => {
691
+ return openBlock(), createBlock(unref(VDialog), {
692
+ "model-value": isOpen.value,
693
+ persistent: isRunning.value,
694
+ role: request.value?.role,
695
+ "aria-labelledby": request.value?.title ? unref(titleId) : unref(messageId),
696
+ "aria-describedby": unref(messageId),
697
+ "max-width": "560",
698
+ "onUpdate:modelValue": dismiss,
699
+ onAfterEnter: focusFirstAction,
700
+ onAfterLeave: showNext
701
+ }, {
702
+ 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 }, {
703
+ default: withCtx(() => [
704
+ request.value.title ? (openBlock(), createBlock(unref(VCardItem), {
705
+ key: 0,
706
+ class: "pt-6"
707
+ }, {
708
+ default: withCtx(() => [createCommentVNode(" Titles wrap instead of being truncated, since they're part of the message "), createVNode(unref(VCardTitle), {
709
+ id: unref(titleId),
710
+ class: "text-wrap"
711
+ }, {
712
+ default: withCtx(() => [createTextVNode(toDisplayString(request.value.title), 1)]),
713
+ _: 1
714
+ }, 8, ["id"])]),
715
+ _: 1
716
+ })) : createCommentVNode("v-if", true),
717
+ createCommentVNode(" Below a title, the card already removes the message's top padding "),
718
+ createVNode(unref(VCardText), {
719
+ id: unref(messageId),
720
+ class: normalizeClass({ "pt-6": !request.value.title })
721
+ }, {
722
+ default: withCtx(() => [createTextVNode(toDisplayString(request.value.message), 1)]),
723
+ _: 1
724
+ }, 8, ["id", "class"]),
725
+ request.value.actions.length > 0 ? (openBlock(), createBlock(unref(VCardActions), {
726
+ key: 1,
727
+ ref_key: "actions",
728
+ ref: actions,
729
+ class: "px-6 pt-0 pb-6"
730
+ }, {
731
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(request.value.actions, (action, index) => {
732
+ return openBlock(), createBlock(button_default, {
733
+ key: index,
734
+ variant: action.variant,
735
+ color: action.color,
736
+ loading: selected.value === action,
737
+ disabled: isRunning.value && selected.value !== action,
738
+ onClick: ($event) => select(request.value, action)
739
+ }, {
740
+ default: withCtx(() => [createTextVNode(toDisplayString(action.text), 1)]),
741
+ _: 2
742
+ }, 1032, [
743
+ "variant",
744
+ "color",
745
+ "loading",
746
+ "disabled",
747
+ "onClick"
748
+ ]);
749
+ }), 128))]),
750
+ _: 1
751
+ }, 512)) : createCommentVNode("v-if", true)
752
+ ]),
753
+ _: 1
754
+ })) : createCommentVNode("v-if", true)]),
755
+ _: 1
756
+ }, 8, [
757
+ "model-value",
758
+ "persistent",
759
+ "role",
760
+ "aria-labelledby",
761
+ "aria-describedby"
762
+ ]);
763
+ };
764
+ }
765
+ });
766
+ //#endregion
767
+ //#region src/components/main/main.vue
768
+ var main_default = /* @__PURE__ */ defineComponent({
769
+ inheritAttrs: false,
770
+ __name: "main",
771
+ props: /*@__PURE__*/ mergeModels({
772
+ dataTestid: {
773
+ type: String,
774
+ required: false
775
+ },
776
+ logo: {
777
+ type: String,
778
+ required: false
779
+ },
780
+ appBar: {
781
+ type: Object,
782
+ required: false
783
+ },
784
+ navigationMenu: {
785
+ type: Object,
786
+ required: false
787
+ }
788
+ }, {
789
+ "notificationsOpen": {
790
+ type: Boolean,
791
+ default: false
792
+ },
793
+ "notificationsOpenModifiers": {}
794
+ }),
795
+ emits: ["update:notificationsOpen"],
796
+ setup(__props) {
797
+ const props = __props;
798
+ const appBarProps = computed(() => props.appBar ? {
799
+ ...props.appBar,
800
+ logo: props.logo,
801
+ navigation: props.navigationMenu !== void 0
802
+ } : void 0);
803
+ const notificationsOpen = useModel(__props, "notificationsOpen");
804
+ const navigationMenuOpen = ref(false);
805
+ return (_ctx, _cache) => {
806
+ return openBlock(), createBlock(unref(VApp), { "data-testid": props.dataTestid }, {
807
+ default: withCtx(() => [
808
+ appBarProps.value ? (openBlock(), createBlock(app_bar_default, mergeProps({ key: 0 }, appBarProps.value, {
809
+ "notifications-open": notificationsOpen.value,
810
+ "onUpdate:notificationsOpen": _cache[0] || (_cache[0] = ($event) => notificationsOpen.value = $event),
811
+ "navigation-open": navigationMenuOpen.value,
812
+ "onUpdate:navigationOpen": _cache[1] || (_cache[1] = ($event) => navigationMenuOpen.value = $event)
813
+ }), null, 16, ["notifications-open", "navigation-open"])) : createCommentVNode("v-if", true),
814
+ props.navigationMenu ? (openBlock(), createBlock(navigation_menu_default, mergeProps({ key: 1 }, props.navigationMenu, {
815
+ modelValue: navigationMenuOpen.value,
816
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => navigationMenuOpen.value = $event)
817
+ }), null, 16, ["modelValue"])) : createCommentVNode("v-if", true),
818
+ createVNode(unref(VMain), null, {
819
+ default: withCtx(() => [createVNode(container_default, null, {
820
+ default: withCtx(() => [
821
+ renderSlot(_ctx.$slots, "default"),
822
+ createVNode(toast_default),
823
+ createVNode(dialog_default)
824
+ ]),
825
+ _: 3
826
+ })]),
827
+ _: 3
828
+ })
829
+ ]),
830
+ _: 3
831
+ }, 8, ["data-testid"]);
832
+ };
833
+ }
834
+ });
835
+ //#endregion
709
836
  //#region src/composables/inputs/autocomplete-field.ts
710
837
  function createAutocompleteRequiredRule(options) {
711
838
  return createRequiredRule(options.required, (value) => {
@@ -1086,7 +1213,7 @@ var section_default = /* @__PURE__ */ defineComponent({
1086
1213
  }, {
1087
1214
  default: withCtx(() => [renderSlot(_ctx.$slots, "default"), showActions.value ? (openBlock(), createBlock(unref(VCardActions), {
1088
1215
  key: 0,
1089
- class: "mt-auto"
1216
+ class: "mt-auto justify-end"
1090
1217
  }, {
1091
1218
  default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(props.actions, ({ label, onClick, ...action }, index) => {
1092
1219
  return openBlock(), createBlock(button_default, mergeProps({ key: index }, { ref_for: true }, action, { onClick }), {
@@ -1,4 +1,4 @@
1
- import { n as FeedbackColorVariant } from "./types-BPXXtbmW.js";
1
+ import { i as FeedbackColorVariant, n as ButtonVariant, r as ColorVariant } from "./types-HPMP8IrV.js";
2
2
  //#region src/composables/dialogs/types.d.ts
3
3
  interface ToastOptions {
4
4
  /**
@@ -6,12 +6,84 @@ interface ToastOptions {
6
6
  */
7
7
  message: string;
8
8
  /**
9
- * Applies a color to the button.
9
+ * Applies a color to the toast.
10
10
  * One of `positive`, `informative`, `caution`, or `danger`.
11
11
  * @default "informative"
12
12
  */
13
13
  color?: FeedbackColorVariant;
14
14
  }
15
+ /**
16
+ * A button displayed at the bottom of a dialog opened by `useDialog`'s `dialog`.
17
+ */
18
+ interface DialogAction<T extends string = string> {
19
+ /**
20
+ * Text displayed on the button.
21
+ */
22
+ text: string;
23
+ /**
24
+ * Value the dialog resolves with when the button is clicked.
25
+ */
26
+ value: T;
27
+ /**
28
+ * Applies a distinct style variation to the button.
29
+ * One of `primary`, `secondary`, or `ghost`.
30
+ * @default "ghost", or "primary" for the last action
31
+ */
32
+ variant?: ButtonVariant;
33
+ /**
34
+ * Applies a color to the button.
35
+ * One of `primary`, `secondary`, `positive`, `informative`, `caution`, or `danger`.
36
+ * @default "primary"
37
+ */
38
+ color?: ColorVariant;
39
+ }
40
+ /**
41
+ * Options of a dialog opened by `useDialog`'s `dialog`.
42
+ */
43
+ interface DialogOptions<T extends string = string> {
44
+ /**
45
+ * Plain text displayed as the dialog's title. HTML isn't interpreted.
46
+ */
47
+ title?: string;
48
+ /**
49
+ * Plain text displayed as the dialog's message. HTML isn't interpreted.
50
+ */
51
+ message: string;
52
+ /**
53
+ * Buttons displayed at the bottom of the dialog, right-aligned, in the given order.
54
+ * @default [{ text: "Fechar" }], which resolves `undefined`
55
+ */
56
+ actions?: DialogAction<T>[];
57
+ }
58
+ /**
59
+ * Options of a dialog opened by `useConfirm`'s `confirm`.
60
+ */
61
+ interface ConfirmOptions {
62
+ /**
63
+ * Plain text displayed as the dialog's title. HTML isn't interpreted.
64
+ */
65
+ title?: string;
66
+ /**
67
+ * Plain text displayed as the dialog's message. HTML isn't interpreted.
68
+ */
69
+ message: string;
70
+ /**
71
+ * Text displayed on the confirm button.
72
+ * @default "Confirmar"
73
+ */
74
+ confirmText?: string;
75
+ /**
76
+ * Text displayed on the cancel button.
77
+ * @default "Cancelar"
78
+ */
79
+ cancelText?: string;
80
+ /**
81
+ * Applies a color to the confirm button.
82
+ * One of `primary`, `secondary`, `positive`, `informative`, `caution`, or `danger`.
83
+ * @default "primary"
84
+ */
85
+ color?: ColorVariant;
86
+ }
15
87
  //#endregion
16
88
  //#region src/composables/dialogs/use-toast.d.ts
17
89
  /**
@@ -37,7 +109,7 @@ declare function useToast(): {
37
109
  */
38
110
  declare function toast(options: ToastOptions): void;
39
111
  //#endregion
40
- //#region src/composables/dialogs/use-run-or-toast..d.ts
112
+ //#region src/composables/dialogs/use-run-or-toast.d.ts
41
113
  /**
42
114
  * Generate `run` method and a computed property `isRunning`.
43
115
  * Calling `run` will reactively update `isRunning`.
@@ -64,4 +136,58 @@ declare function useRunOrToast(): {
64
136
  isRunning: import("vue").ComputedRef<boolean>;
65
137
  };
66
138
  //#endregion
67
- export { useRunOrToast, useToast };
139
+ //#region src/composables/dialogs/use-dialog.d.ts
140
+ /**
141
+ * Composable which returns a function to display dialogs.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const { dialog } = useDialog();
146
+ *
147
+ * const answer = await dialog({
148
+ * title: "Sessão expirando",
149
+ * message: "Deseja continuar conectado?",
150
+ * actions: [
151
+ * { text: "Sair", value: "leave" },
152
+ * { text: "Continuar", value: "stay" },
153
+ * ],
154
+ * });
155
+ * ```
156
+ */
157
+ declare function useDialog(): {
158
+ dialog: typeof dialog;
159
+ };
160
+ /**
161
+ * Displays a dialog. Dialogs are displayed one at a time, in the order they were requested.
162
+ * @param {DialogOptions} options - Options for the dialog.
163
+ * @returns the `value` of the clicked action, or `undefined` when the dialog is closed by the
164
+ * default action, the `Esc` key, a click outside it or the browser's back button.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * await dialog({ title: "Aviso", message: "Seu cadastro foi enviado para análise." });
169
+ * ```
170
+ */
171
+ declare function dialog<T extends string = never>(options: DialogOptions<T>): Promise<T | undefined>;
172
+ //#endregion
173
+ //#region src/composables/dialogs/use-confirm.d.ts
174
+ /**
175
+ * Composable which returns a function to run actions only after the user confirms them in a
176
+ * dialog, and a computed property `isRunning`.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * const { confirm, isRunning } = useConfirm();
181
+ *
182
+ * const removed = await confirm(
183
+ * { title: "Excluir paciente", message: "Esta ação não pode ser desfeita.", confirmText: "Excluir", color: "danger" },
184
+ * () => api.deletePatient(id),
185
+ * );
186
+ * ```
187
+ */
188
+ declare function useConfirm(): {
189
+ confirm: <F extends (...args: Parameters<F>) => PromiseLike<Awaited<ReturnType<F>>>>(options: ConfirmOptions, action: F, ...params: Parameters<F>) => Promise<Awaited<ReturnType<F>> | false>;
190
+ isRunning: import("vue").ComputedRef<boolean>;
191
+ };
192
+ //#endregion
193
+ export { useConfirm, useDialog, useRunOrToast, useToast };
@@ -1,6 +1,6 @@
1
- import { n as useToast } from "./use-toast-CACuYhK_.js";
1
+ import { i as useDialog, o as useToast, t as openDialog } from "./use-dialog-Ck2_ZW7T.js";
2
2
  import { computed, ref } from "vue";
3
- //#region src/composables/dialogs/use-run-or-toast..ts
3
+ //#region src/composables/dialogs/use-run-or-toast.ts
4
4
  const { toast } = useToast();
5
5
  /**
6
6
  * Generate `run` method and a computed property `isRunning`.
@@ -62,4 +62,56 @@ function useRunOrToast() {
62
62
  };
63
63
  }
64
64
  //#endregion
65
- export { useRunOrToast, useToast };
65
+ //#region src/composables/dialogs/use-confirm.ts
66
+ /**
67
+ * Composable which returns a function to run actions only after the user confirms them in a
68
+ * dialog, and a computed property `isRunning`.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * const { confirm, isRunning } = useConfirm();
73
+ *
74
+ * const removed = await confirm(
75
+ * { title: "Excluir paciente", message: "Esta ação não pode ser desfeita.", confirmText: "Excluir", color: "danger" },
76
+ * () => api.deletePatient(id),
77
+ * );
78
+ * ```
79
+ */
80
+ function useConfirm() {
81
+ const { run, isRunning } = useRunOrToast();
82
+ /**
83
+ * Displays a confirmation dialog and, if the user confirms, runs the given function while the
84
+ * dialog shows the confirm button loading. If the function throws an error, the dialog closes
85
+ * and a toast shows the error message.
86
+ * @returns the function returned value, or `false` if the user cancels or the function fails
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * const { confirm } = useConfirm();
91
+ *
92
+ * // Call like this:
93
+ * const result1 = await confirm({ message: "Salvar alterações?" }, () => save(form));
94
+ *
95
+ * // Or this:
96
+ * const result2 = await confirm({ message: "Salvar alterações?" }, save, form);
97
+ * ```
98
+ */
99
+ function confirm(options, action, ...params) {
100
+ return openDialog({
101
+ title: options.title,
102
+ message: options.message,
103
+ role: "alertdialog",
104
+ actions: [{ text: options.cancelText ?? "Cancelar" }, {
105
+ text: options.confirmText ?? "Confirmar",
106
+ value: "confirm",
107
+ color: options.color
108
+ }]
109
+ }, (value) => value === "confirm" ? run(action, ...params) : false);
110
+ }
111
+ return {
112
+ confirm,
113
+ isRunning
114
+ };
115
+ }
116
+ //#endregion
117
+ export { useConfirm, useDialog, useRunOrToast, useToast };
@@ -0,0 +1,49 @@
1
+ //#region src/composables/colors/types.d.ts
2
+ type ColorVariant = "primary" | "secondary" | FeedbackColorVariant;
3
+ type FeedbackColorVariant = "positive" | "informative" | "caution" | "danger";
4
+ //#endregion
5
+ //#region src/components/button/types.d.ts
6
+ type ButtonVariant = "primary" | "secondary" | "ghost";
7
+ /**
8
+ * Props exposed by the {@link Button} component.
9
+ */
10
+ type ButtonProps = {
11
+ /**
12
+ * Applies a distinct style variation to the button.
13
+ * One of `primary`, `secondary`, or `ghost`.
14
+ * @default "primary"
15
+ */
16
+ variant?: ButtonVariant;
17
+ /**
18
+ * Applies a distinct behavior to the button.
19
+ * One of `button` or `submit`.
20
+ * @default "button"
21
+ */
22
+ type?: "submit" | "button";
23
+ /**
24
+ * Applies a color to the button.
25
+ * One of `primary`, `secondary`, `positive`, `informative`, `caution`, or `danger`.
26
+ * @default "primary"
27
+ */
28
+ color?: ColorVariant;
29
+ /**
30
+ * Removes the ability to click or target the button.
31
+ * @default false
32
+ */
33
+ disabled?: boolean;
34
+ /**
35
+ * Displays a loading indicator on the button, disabling interaction.
36
+ * @default false
37
+ */
38
+ loading?: boolean;
39
+ /**
40
+ * Associated [form](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/form) id.
41
+ */
42
+ form?: string;
43
+ /**
44
+ * Component id to use on automated tests.
45
+ */
46
+ dataTestid?: string;
47
+ };
48
+ //#endregion
49
+ export { FeedbackColorVariant as i, ButtonVariant as n, ColorVariant as r, ButtonProps as t };
@@ -0,0 +1,124 @@
1
+ import { computed, ref, toValue } from "vue";
2
+ const colorToVuetifyColor = {
3
+ primary: "primary",
4
+ secondary: "secondary",
5
+ positive: "success",
6
+ informative: "info",
7
+ caution: "warning",
8
+ danger: "error"
9
+ };
10
+ //#endregion
11
+ //#region src/composables/colors/use-vuetify-color.ts
12
+ function useVuetifyColor(color) {
13
+ return computed(() => {
14
+ const colorValue = toValue(color);
15
+ return colorValue ? colorToVuetifyColor[colorValue] : colorToVuetifyColor.primary;
16
+ });
17
+ }
18
+ //#endregion
19
+ //#region src/composables/dialogs/use-toast.ts
20
+ const messages = ref([]);
21
+ /**
22
+ * Composable which returns a function to display toast messages.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const { toast } = useToast();
27
+ * toast({ message: "Hello, World!" });
28
+ * ```
29
+ */
30
+ function useToast() {
31
+ return { toast };
32
+ }
33
+ /**
34
+ * Displays a toast message.
35
+ * @param {ToastOptions} options - Options for the toast message.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * toast({ message: "Hello, World!" });
40
+ * ```
41
+ */
42
+ function toast(options) {
43
+ const text = options.message;
44
+ const color = useVuetifyColor(options.color ?? "informative");
45
+ const toastMessage = {
46
+ text,
47
+ color: color.value,
48
+ prependIcon: `$${color.value}`
49
+ };
50
+ messages.value.push(toastMessage);
51
+ }
52
+ //#endregion
53
+ //#region src/composables/dialogs/use-dialog.ts
54
+ /**
55
+ * Dialogs waiting to be displayed, shared by `useDialog` and `useConfirm`. The first one is the
56
+ * dialog on display (or about to be).
57
+ */
58
+ const requests = ref([]);
59
+ /**
60
+ * Mounted `Dialog` components, in mounting order. Only the first one displays dialogs, so pages
61
+ * with more than one `UMain` don't display duplicates.
62
+ */
63
+ const owners = ref([]);
64
+ /**
65
+ * Queues a dialog and resolves with what `onSelect` returns for the clicked action's value
66
+ * (`undefined` when the dialog is dismissed).
67
+ */
68
+ function openDialog(options, onSelect) {
69
+ const { actions, ...content } = options;
70
+ const lastIndex = actions.length - 1;
71
+ return new Promise((resolve) => {
72
+ requests.value.push({
73
+ ...content,
74
+ actions: actions.map((action, index) => ({
75
+ ...action,
76
+ variant: action.variant ?? (index === lastIndex ? "primary" : "ghost")
77
+ })),
78
+ async select(value) {
79
+ resolve(await onSelect(value));
80
+ }
81
+ });
82
+ });
83
+ }
84
+ /**
85
+ * Composable which returns a function to display dialogs.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * const { dialog } = useDialog();
90
+ *
91
+ * const answer = await dialog({
92
+ * title: "Sessão expirando",
93
+ * message: "Deseja continuar conectado?",
94
+ * actions: [
95
+ * { text: "Sair", value: "leave" },
96
+ * { text: "Continuar", value: "stay" },
97
+ * ],
98
+ * });
99
+ * ```
100
+ */
101
+ function useDialog() {
102
+ return { dialog };
103
+ }
104
+ /**
105
+ * Displays a dialog. Dialogs are displayed one at a time, in the order they were requested.
106
+ * @param {DialogOptions} options - Options for the dialog.
107
+ * @returns the `value` of the clicked action, or `undefined` when the dialog is closed by the
108
+ * default action, the `Esc` key, a click outside it or the browser's back button.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * await dialog({ title: "Aviso", message: "Seu cadastro foi enviado para análise." });
113
+ * ```
114
+ */
115
+ function dialog(options) {
116
+ return openDialog({
117
+ title: options.title,
118
+ message: options.message,
119
+ role: "dialog",
120
+ actions: options.actions ?? [{ text: "Fechar" }]
121
+ }, (value) => value);
122
+ }
123
+ //#endregion
124
+ export { messages as a, useDialog as i, owners as n, useToast as o, requests as r, useVuetifyColor as s, openDialog as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdom/uimed-vue",
3
- "version": "2.0.0-beta.8",
3
+ "version": "2.0.0-beta.9",
4
4
  "description": "A ui framework for NEXDOM applications.",
5
5
  "keywords": [
6
6
  "lib",
@@ -1,5 +0,0 @@
1
- //#region src/composables/colors/types.d.ts
2
- type ColorVariant = "primary" | "secondary" | FeedbackColorVariant;
3
- type FeedbackColorVariant = "positive" | "informative" | "caution" | "danger";
4
- //#endregion
5
- export { FeedbackColorVariant as n, ColorVariant as t };
@@ -1,53 +0,0 @@
1
- import { computed, ref, toValue } from "vue";
2
- const colorToVuetifyColor = {
3
- primary: "primary",
4
- secondary: "secondary",
5
- positive: "success",
6
- informative: "info",
7
- caution: "warning",
8
- danger: "error"
9
- };
10
- //#endregion
11
- //#region src/composables/colors/use-vuetify-color.ts
12
- function useVuetifyColor(color) {
13
- return computed(() => {
14
- const colorValue = toValue(color);
15
- return colorValue ? colorToVuetifyColor[colorValue] : colorToVuetifyColor.primary;
16
- });
17
- }
18
- //#endregion
19
- //#region src/composables/dialogs/use-toast.ts
20
- const messages = ref([]);
21
- /**
22
- * Composable which returns a function to display toast messages.
23
- *
24
- * @example
25
- * ```ts
26
- * const { toast } = useToast();
27
- * toast({ message: "Hello, World!" });
28
- * ```
29
- */
30
- function useToast() {
31
- return { toast };
32
- }
33
- /**
34
- * Displays a toast message.
35
- * @param {ToastOptions} options - Options for the toast message.
36
- *
37
- * @example
38
- * ```ts
39
- * toast({ message: "Hello, World!" });
40
- * ```
41
- */
42
- function toast(options) {
43
- const text = options.message;
44
- const color = useVuetifyColor(options.color ?? "informative");
45
- const toastMessage = {
46
- text,
47
- color: color.value,
48
- prependIcon: `$${color.value}`
49
- };
50
- messages.value.push(toastMessage);
51
- }
52
- //#endregion
53
- export { useToast as n, useVuetifyColor as r, messages as t };