@nexdom/uimed-vue 1.1.0 → 1.2.0-beta.2

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 ADDED
@@ -0,0 +1,145 @@
1
+ # AGENTS.md
2
+
3
+ ## Project
4
+
5
+ `@nexdom/uimed-vue` is a Vue 3 UI component library for NEXDOM applications, built on top of [Vuetify](https://vuetifyjs.com/) and [Material Design 3](https://m3.material.io/). It's published to npm and documented via VitePress at https://nexdom-healthtech.github.io/uimed-vue/.
6
+
7
+ Tooling is built around [Vite+](https://github.com/voidzero-dev/vite-plus) (`vp`/`vpr`/`vpx` CLIs), not raw `vite`/`vitest`/`eslint` invocations.
8
+
9
+ ## Using this library in an app
10
+
11
+ These steps are for developers _consuming_ `@nexdom/uimed-vue` in a Vue application (not for contributing to this repo — see the rest of this document for that).
12
+
13
+ ### Install
14
+
15
+ ```bash
16
+ vp add @nexdom/uimed-vue
17
+ # But, if you're not using Vite+ yet...
18
+ npm i @nexdom/uimed-vue
19
+ # Or
20
+ pnpm add @nexdom/uimed-vue
21
+ # Or
22
+ yarn add @nexdom/uimed-vue
23
+ ```
24
+
25
+ Peer dependencies (`@fontsource/roboto`, `@mdi/font`, `@nexdom/shared`, `resize-observer-polyfill`, `vite-plugin-vuetify`, `vite-plus`, `vue`, `vue-router`, `vuetify`) must be installed alongside it — most package managers install these automatically, but confirm versions match what's declared in `package.json`'s `peerDependencies`.
26
+
27
+ ### Setup
28
+
29
+ Add the Vite plugin:
30
+
31
+ ```ts
32
+ // vite.config.ts
33
+ import { vitePluginUimed } from "@nexdom/uimed-vue/plugins.ts";
34
+
35
+ // ...
36
+
37
+ plugins: [vue(), vitePluginUimed()];
38
+
39
+ // ...
40
+ ```
41
+
42
+ Register the plugin in the Vue app:
43
+
44
+ ```ts
45
+ // main.js or main.ts
46
+ import { createApp } from "vue";
47
+ import { createUimed } from "@nexdom/uimed-vue";
48
+
49
+ import App from "./App.vue";
50
+
51
+ const uimed = createUimed();
52
+
53
+ createApp(App).use(uimed).mount("#app");
54
+ ```
55
+
56
+ ### Usage
57
+
58
+ Place the `Root` component at the top of `App.vue`, then add other components as needed:
59
+
60
+ ```vue
61
+ <!-- App.vue -->
62
+ <template>
63
+ <root>
64
+ <!-- ... -->
65
+ </root>
66
+ </template>
67
+
68
+ <script setup lang="ts">
69
+ import { Root } from "@nexdom/uimed-vue/components";
70
+ </script>
71
+ ```
72
+
73
+ Never write CSS, classes or any kind of styling. Always use component props.
74
+
75
+ Available entry points: `@nexdom/uimed-vue` (root), `@nexdom/uimed-vue/components`, `@nexdom/uimed-vue/composables`, `@nexdom/uimed-vue/plugins`, `@nexdom/uimed-vue/unit-test` (test helpers, e.g. `vueTestUtilsPluginUimed()` for mounting components with Vuetify in Vitest).
76
+
77
+ Full component/composable reference lives in the [docs](https://nexdom-healthtech.github.io/uimed-vue/).
78
+
79
+ ## Contributing to this repo
80
+
81
+ The sections below are for developers working _on_ this library itself.
82
+
83
+ ### Dev environment setup
84
+
85
+ This project expects to be opened inside its devcontainer (VSCode). If commands below fail with missing shims/commands, run:
86
+
87
+ ```bash
88
+ vp env doctor # diagnose missing parts
89
+ vp env setup # create shims like vpr and vpx
90
+ vp install # install dependencies
91
+ ```
92
+
93
+ ## Commands
94
+
95
+ - `vpr check` — lint, formatter, and type-check (requires `build`/`pack` to have run first for the type-check step)
96
+ - `vp test --coverage` — unit tests (Vitest, jsdom, 100% coverage threshold enforced)
97
+ - `vpr test:mutations` — mutation tests (Stryker; thresholds: high 100, low 95, break 95)
98
+ - `vpr test:e2e` — E2E tests (Playwright, runs against the built docs preview site)
99
+ - `vpr depcruise` — architecture/dependency rules (dependency-cruiser)
100
+ - `vp pack` / `vpr build` — build the library
101
+ - `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)
102
+
103
+ 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.
104
+
105
+ ## Code conventions
106
+
107
+ - 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.
108
+ - Never write CSS, classes or any kind of styling. Always use component props.
109
+ - Path aliases: `@/*` → `src/*`, `@e2e/*` → `e2e/*`.
110
+ - Every public component follows this pattern to block access to internals and give it an editor-hover description:
111
+
112
+ ```vue
113
+ <script lang="ts">
114
+ /**
115
+ * A short description of the component, shown when hovering it in the
116
+ * editor.
117
+ */
118
+ export default {
119
+ inheritAttrs: false,
120
+ };
121
+ </script>
122
+
123
+ <script setup lang="ts">
124
+ // component's implementation
125
+ </script>
126
+ ```
127
+
128
+ - 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>/`.
129
+ - Public components/composables/types are re-exported from `src/components/index.ts` and `src/composables/index.ts`.
130
+ - 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.
131
+ - 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.
132
+
133
+ ## Testing conventions
134
+
135
+ - Unit tests use Vitest + `@vue/test-utils`, with `vueTestUtilsPluginUimed()` from `@/unit-test.ts` to mount a Vuetify instance.
136
+ - Global unit test setup (`src/__tests__/setup.ts`) stubs `visualViewport`, uses fake timers, and silences `console.error/warn/log`.
137
+ - Coverage threshold is 100%; mutation testing threshold is 100% (break at 100). Don't add code paths without covering tests.
138
+ - 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.
139
+
140
+ ## Git workflow
141
+
142
+ - Trunk-based development. `main` is the only long-lived branch (`beta`/`alpha` exist only for pre-release/prototype work — see CONTRIBUTING.md for when to use them). `main`, `beta`, `alpha` are all protected against direct pushes.
143
+ - Short-lived branches are named after their intention (e.g. `fix-some-method-behavior`) and are deleted after merging back via PR.
144
+ - Commit messages MUST follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) — enforced by commitlint in CI. Recent examples: `feat(components): ...`, `fix(composables): ...`, `test(e2e): ...`, `docs(root): ...`, `chore: ...`.
145
+ - Releases are automated via `semantic-release` (conventional-commits preset) — don't hand-edit versions/changelogs.
@@ -1,3 +1,97 @@
1
+ import { t as ColorVariant } from "./types-BPXXtbmW.js";
2
+ import { RouteLocationRaw } from "vue-router";
3
+ //#region src/components/app-bar/types.d.ts
4
+ /**
5
+ * Props exposed by the {@link AppBar} component.
6
+ */
7
+ type AppBarProps = {
8
+ /**
9
+ * Component id to use on automated tests.
10
+ */
11
+ dataTestid?: string;
12
+ /**
13
+ * Main title to be presented.
14
+ */
15
+ title?: string;
16
+ /**
17
+ * Help page route.
18
+ */
19
+ help?: RouteLocationRaw;
20
+ /**
21
+ * Resources available to the user in the app-bar
22
+ */
23
+ user?: AppBarUserProps;
24
+ /**
25
+ * List of notifications to show in the {@link AppBar}.
26
+ */
27
+ notifications?: Array<AppBarNotification>;
28
+ };
29
+ /**
30
+ * A single notification entry shown in the {@link AppBar}.
31
+ */
32
+ interface AppBarNotification {
33
+ /**
34
+ * Main text describing the notification.
35
+ */
36
+ title: string;
37
+ /**
38
+ * Complementary text with additional details.
39
+ */
40
+ subtitle?: string;
41
+ /**
42
+ * Whether the notification has already been read.
43
+ */
44
+ read?: boolean;
45
+ /**
46
+ * Date the notification was generated.
47
+ */
48
+ date?: Date;
49
+ }
50
+ interface AppBarUserProps {
51
+ /**
52
+ * Major identifier, like the user full name.
53
+ */
54
+ title?: string;
55
+ /**
56
+ * Complement to user identifier, like it's e-mail or username.
57
+ */
58
+ subtitle?: string;
59
+ /**
60
+ * URL leading to a user picture.
61
+ */
62
+ img?: string;
63
+ /**
64
+ * Options to show the user when its menu is opened.
65
+ * Can be plain options, grouped or both.
66
+ */
67
+ options?: Array<ParentOptionOrOption>;
68
+ }
69
+ type ParentOptionOrOption = ParentOption | Option;
70
+ type ParentOption = {
71
+ /**
72
+ * Text to present for the group.
73
+ */
74
+ description: string;
75
+ /**
76
+ * Child {@link Option} list.
77
+ */
78
+ items: Array<Option>;
79
+ };
80
+ interface Option {
81
+ /**
82
+ * Text to present for the action.
83
+ */
84
+ description: string;
85
+ /**
86
+ * Route to navigate when item is clicked.
87
+ */
88
+ route?: RouteLocationRaw;
89
+ /**
90
+ * Callback to be triggered when the item is clicked.
91
+ */
92
+ action?: () => void;
93
+ }
94
+ //#endregion
1
95
  //#region src/components/root/types.d.ts
2
96
  /**
3
97
  * Props exposed by the {@link Root} component.
@@ -7,6 +101,10 @@ type RootProps = {
7
101
  * Component id to use on automated tests.
8
102
  */
9
103
  dataTestid?: string;
104
+ /**
105
+ * App bar properties.
106
+ */
107
+ appBar?: AppBarProps;
10
108
  };
11
109
  //#endregion
12
110
  //#region src/components/root/root.vue.d.ts
@@ -23,7 +121,15 @@ type RootProps = {
23
121
  * @see {@link https://nexdom-healthtech.github.io/uimed-vue/guide/components/root | Root Guide}
24
122
  */
25
123
  declare const _default$4: typeof __VLS_export$6;
26
- declare const __VLS_export$6: __VLS_WithSlots$5<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<RootProps, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {}, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<RootProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
124
+ declare const __VLS_export$6: __VLS_WithSlots$5<import("vue", { with: { "resolution-mode": "import" } }).DefineComponent<RootProps & {
125
+ notificationsOpen?: boolean;
126
+ }, {}, {}, {}, {}, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, import("vue", { with: { "resolution-mode": "import" } }).ComponentOptionsMixin, {
127
+ "update:notificationsOpen": (value: boolean) => any;
128
+ }, string, import("vue", { with: { "resolution-mode": "import" } }).PublicProps, Readonly<RootProps & {
129
+ notificationsOpen?: boolean;
130
+ }> & Readonly<{
131
+ "onUpdate:notificationsOpen"?: ((value: boolean) => any) | undefined;
132
+ }>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>, {
27
133
  default?: (props: {}) => any;
28
134
  }>;
29
135
  type __VLS_WithSlots$5<T, S> = T & {
@@ -34,7 +140,6 @@ type __VLS_WithSlots$5<T, S> = T & {
34
140
  //#endregion
35
141
  //#region src/components/btn/types.d.ts
36
142
  type BtnVariant = "primary" | "secondary" | "ghost";
37
- type BtnColor = "primary" | "secondary" | "positive" | "informative" | "caution" | "danger";
38
143
  /**
39
144
  * Props exposed by the {@link Btn} component.
40
145
  */
@@ -56,7 +161,7 @@ type BtnProps = {
56
161
  * One of `primary`, `secondary`, `positive`, `informative`, `caution`, or `danger`.
57
162
  * @default "primary"
58
163
  */
59
- color?: BtnColor;
164
+ color?: ColorVariant;
60
165
  /**
61
166
  * Removes the ability to click or target the button.
62
167
  * @default false
@@ -352,4 +457,4 @@ type __VLS_WithSlots<T, S> = T & {
352
457
  };
353
458
  };
354
459
  //#endregion
355
- export { _default as Btn, _default$1 as Column, _default$2 as Container, _default$3 as Frm, _default$4 as Root, _default$5 as Row, _default$6 as TextField };
460
+ export { type AppBarProps as AppBarConfig, type AppBarUserProps as AppBarUserConfig, _default as Btn, _default$1 as Column, _default$2 as Container, _default$3 as Frm, _default$4 as Root, _default$5 as Row, _default$6 as TextField };
@@ -1,11 +1,211 @@
1
- import { computed, createBlock, createPropsRestProxy, defineComponent, mergeModels, openBlock, renderSlot, toValue, unref, useModel, withCtx, withModifiers } from "vue";
2
- import { VApp, VBtn, VCol, VContainer, VForm, VRow, VTextField } from "vuetify/components";
3
- import { isEmail, isEmpty, isPhone, isUrl } from "@nexdom/shared/utils";
1
+ import { r as useVuetifyColor, t as messages } from "./use-toast-CACuYhK_.js";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createPropsRestProxy, createSlots, createTextVNode, createVNode, defineComponent, isRef, mergeModels, mergeProps, openBlock, renderList, renderSlot, toDisplayString, toValue, unref, useModel, withCtx, withModifiers } from "vue";
3
+ import { VApp, VAppBar, VAvatar, VBadge, VBtn, VCard, VCardText, VCol, VContainer, VDivider, VForm, VIcon, VIconBtn, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VMain, VMenu, VRow, VSnackbarQueue, VTextField } from "vuetify/components";
4
+ import { formatDateTime, isEmail, isEmpty, isPhone, isUrl, toPeriodInterval } from "@nexdom/shared/utils";
5
+ //#region src/composables/navigation/use-route-props.ts
6
+ function useRouteProps(route) {
7
+ return computed(() => routeToProps(route));
8
+ }
9
+ function routeToProps(route) {
10
+ const location = toValue(route);
11
+ return isExternalRoute(location) ? { href: location } : { to: location };
12
+ }
13
+ function isExternalRoute(route) {
14
+ return typeof route === "string" && route.startsWith("http");
15
+ }
4
16
  //#endregion
5
- //#region src/components/root/root.vue
6
- var root_default = /* @__PURE__ */ defineComponent({
17
+ //#region src/components/app-bar/app-bar.vue?vue&type=script&setup=true&lang.ts
18
+ const _hoisted_1 = { class: "opacity-60" };
19
+ //#endregion
20
+ //#region src/components/app-bar/app-bar.vue
21
+ var app_bar_default = /* @__PURE__ */ defineComponent({
7
22
  inheritAttrs: false,
8
- __name: "root",
23
+ __name: "app-bar",
24
+ props: /*@__PURE__*/ mergeModels({
25
+ dataTestid: {
26
+ type: String,
27
+ required: false
28
+ },
29
+ title: {
30
+ type: String,
31
+ required: false
32
+ },
33
+ help: {
34
+ type: null,
35
+ required: false
36
+ },
37
+ user: {
38
+ type: Object,
39
+ required: false
40
+ },
41
+ notifications: {
42
+ type: Array,
43
+ required: false
44
+ }
45
+ }, {
46
+ "notificationsOpen": {
47
+ type: Boolean,
48
+ default: false
49
+ },
50
+ "notificationsOpenModifiers": {}
51
+ }),
52
+ emits: ["update:notificationsOpen"],
53
+ setup(__props) {
54
+ const props = __props;
55
+ const notificationsOpen = useModel(__props, "notificationsOpen");
56
+ const helpBtnDataTestid = computed(() => `${props.dataTestid}-help`);
57
+ const notificationsBtnDataTestid = computed(() => `${props.dataTestid}-notifications`);
58
+ const notificationsMenuDataTestid = computed(() => `${props.dataTestid}-notifications-menu`);
59
+ const userBtnDataTestid = computed(() => `${props.dataTestid}-user`);
60
+ const userMenuDataTestid = computed(() => `${props.dataTestid}-user-menu`);
61
+ const showHelp = computed(() => !!props.help);
62
+ const routeProps = useRouteProps(() => props.help);
63
+ const showNotifications = computed(() => !!props.notifications);
64
+ const unreadCount = computed(() => props.notifications.filter((notification) => !notification.read).length);
65
+ const hasUnreadNotifications = computed(() => unreadCount.value > 0);
66
+ const showUser = computed(() => !!props.user);
67
+ const userAvatarProps = computed(() => props.user.img ? { image: props.user.img } : { icon: "mdi-account-outline" });
68
+ function isParentOption(option) {
69
+ return "items" in option;
70
+ }
71
+ function formatNotificationDate(date) {
72
+ const interval = toPeriodInterval(date);
73
+ if (wasThisMonth(interval)) {
74
+ if (wasToday(interval)) return formatDateTime(date, "HH:mm");
75
+ else if (wasYesterday(interval)) return "Ontem";
76
+ }
77
+ return formatDateTime(date, "DD/MM/YYYY");
78
+ }
79
+ function wasThisMonth(interval) {
80
+ return interval.months === 0 && interval.years === 0;
81
+ }
82
+ function wasToday(interval) {
83
+ return interval.days === 0;
84
+ }
85
+ function wasYesterday(interval) {
86
+ return interval.days === 1;
87
+ }
88
+ return (_ctx, _cache) => {
89
+ return openBlock(), createBlock(unref(VAppBar), {
90
+ title: props.title,
91
+ "data-testid": props.dataTestid
92
+ }, {
93
+ append: withCtx(() => [
94
+ showHelp.value ? (openBlock(), createBlock(unref(VBtn), mergeProps({ key: 0 }, unref(routeProps), {
95
+ "data-testid": helpBtnDataTestid.value,
96
+ icon: "mdi-help-circle-outline"
97
+ }), null, 16, ["data-testid"])) : createCommentVNode("v-if", true),
98
+ showNotifications.value ? (openBlock(), createBlock(unref(VMenu), {
99
+ key: 1,
100
+ modelValue: notificationsOpen.value,
101
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => notificationsOpen.value = $event),
102
+ "close-on-content-click": false
103
+ }, {
104
+ activator: withCtx(({ props }) => [createVNode(unref(VBtn), mergeProps(props, {
105
+ "data-testid": notificationsBtnDataTestid.value,
106
+ icon: ""
107
+ }), {
108
+ default: withCtx(() => [createVNode(unref(VBadge), {
109
+ "model-value": hasUnreadNotifications.value,
110
+ content: unreadCount.value,
111
+ color: "error"
112
+ }, {
113
+ default: withCtx(() => [createVNode(unref(VIcon), { icon: "mdi-bell-outline" })]),
114
+ _: 1
115
+ }, 8, ["model-value", "content"])]),
116
+ _: 1
117
+ }, 16, ["data-testid"])]),
118
+ default: withCtx(() => [createVNode(unref(VCard), { "data-testid": notificationsMenuDataTestid.value }, {
119
+ default: withCtx(() => [props.notifications?.length ? (openBlock(), createBlock(unref(VList), { key: 0 }, {
120
+ default: withCtx(() => [createVNode(unref(VListSubheader), null, {
121
+ default: withCtx(() => [..._cache[1] || (_cache[1] = [createTextVNode("Notificações", -1)])]),
122
+ _: 1
123
+ }), (openBlock(true), createElementBlock(Fragment, null, renderList(props.notifications, (notification, index) => {
124
+ return openBlock(), createBlock(unref(VListItem), {
125
+ key: index,
126
+ active: !notification.read,
127
+ title: notification.title,
128
+ subtitle: notification.subtitle,
129
+ "active-class": "text-primary"
130
+ }, createSlots({ _: 2 }, [notification.date ? {
131
+ name: "append",
132
+ fn: withCtx(() => [createVNode(unref(VListItemAction), { end: "" }, {
133
+ default: withCtx(() => [createElementVNode("small", _hoisted_1, toDisplayString(formatNotificationDate(notification.date)), 1)]),
134
+ _: 2
135
+ }, 1024)]),
136
+ key: "0"
137
+ } : void 0]), 1032, [
138
+ "active",
139
+ "title",
140
+ "subtitle"
141
+ ]);
142
+ }), 128))]),
143
+ _: 1
144
+ })) : (openBlock(), createBlock(unref(VCardText), { key: 1 }, {
145
+ default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode("Nenhuma notificação", -1)])]),
146
+ _: 1
147
+ }))]),
148
+ _: 1
149
+ }, 8, ["data-testid"])]),
150
+ _: 1
151
+ }, 8, ["modelValue"])) : createCommentVNode("v-if", true),
152
+ showUser.value ? (openBlock(), createBlock(unref(VMenu), {
153
+ key: 2,
154
+ "close-on-content-click": false
155
+ }, {
156
+ activator: withCtx(({ props }) => [createVNode(unref(VBtn), mergeProps(props, {
157
+ "data-testid": userBtnDataTestid.value,
158
+ icon: ""
159
+ }), {
160
+ default: withCtx(() => [createVNode(unref(VAvatar), mergeProps(userAvatarProps.value, { color: "primary" }), null, 16)]),
161
+ _: 1
162
+ }, 16, ["data-testid"])]),
163
+ default: withCtx(() => [createVNode(unref(VCard), {
164
+ title: __props.user?.title,
165
+ subtitle: __props.user?.subtitle,
166
+ "data-testid": userMenuDataTestid.value
167
+ }, {
168
+ prepend: withCtx(() => [createVNode(unref(VAvatar), mergeProps(userAvatarProps.value, { color: "primary" }), null, 16)]),
169
+ default: withCtx(() => [createVNode(unref(VDivider)), createVNode(unref(VList), { nav: "" }, {
170
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(props.user?.options, (option, index) => {
171
+ return openBlock(), createElementBlock(Fragment, { key: index }, [index > 0 ? (openBlock(), createBlock(unref(VDivider), {
172
+ key: 0,
173
+ class: "mt-1"
174
+ })) : createCommentVNode("v-if", true), isParentOption(option) ? (openBlock(), createBlock(unref(VListGroup), { key: 1 }, {
175
+ activator: withCtx(({ props }) => [createVNode(unref(VListItem), mergeProps({ ref_for: true }, props, { title: option.description }), null, 16, ["title"])]),
176
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(option.items, (childOption, index) => {
177
+ return openBlock(), createBlock(unref(VListItem), mergeProps({ key: index }, { ref_for: true }, unref(routeToProps)(childOption.route), {
178
+ title: childOption.description,
179
+ onClick: childOption.action
180
+ }), null, 16, ["title", "onClick"]);
181
+ }), 128))]),
182
+ _: 2
183
+ }, 1024)) : (openBlock(), createBlock(unref(VListItem), mergeProps({
184
+ key: 2,
185
+ title: option.description
186
+ }, { ref_for: true }, unref(routeToProps)(option.route), { onClick: option.action }), null, 16, ["title", "onClick"]))], 64);
187
+ }), 128))]),
188
+ _: 1
189
+ })]),
190
+ _: 1
191
+ }, 8, [
192
+ "title",
193
+ "subtitle",
194
+ "data-testid"
195
+ ])]),
196
+ _: 1
197
+ })) : createCommentVNode("v-if", true)
198
+ ]),
199
+ _: 1
200
+ }, 8, ["title", "data-testid"]);
201
+ };
202
+ }
203
+ });
204
+ //#endregion
205
+ //#region src/components/grid/container/container.vue
206
+ var container_default = /* @__PURE__ */ defineComponent({
207
+ inheritAttrs: false,
208
+ __name: "container",
9
209
  props: { dataTestid: {
10
210
  type: String,
11
211
  required: false
@@ -13,7 +213,10 @@ var root_default = /* @__PURE__ */ defineComponent({
13
213
  setup(__props) {
14
214
  const props = __props;
15
215
  return (_ctx, _cache) => {
16
- return openBlock(), createBlock(unref(VApp), { "data-testid": props.dataTestid }, {
216
+ return openBlock(), createBlock(unref(VContainer), {
217
+ "data-testid": props.dataTestid,
218
+ fluid: ""
219
+ }, {
17
220
  default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
18
221
  _: 3
19
222
  }, 8, ["data-testid"]);
@@ -21,32 +224,88 @@ var root_default = /* @__PURE__ */ defineComponent({
21
224
  }
22
225
  });
23
226
  //#endregion
227
+ //#region src/components/dialogs/toast.vue
228
+ var toast_default = /* @__PURE__ */ defineComponent({
229
+ inheritAttrs: false,
230
+ __name: "toast",
231
+ setup(__props) {
232
+ return (_ctx, _cache) => {
233
+ return openBlock(), createBlock(unref(VSnackbarQueue), {
234
+ modelValue: unref(messages),
235
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => isRef(messages) ? messages.value = $event : null),
236
+ "display-strategy": "overflow",
237
+ location: "top end",
238
+ "total-visible": "3",
239
+ collapsed: "",
240
+ closable: "",
241
+ timer: ""
242
+ }, {
243
+ actions: withCtx(({ props }) => [createVNode(unref(VIconBtn), mergeProps(props, {
244
+ "aria-label": "Fechar",
245
+ icon: "$close",
246
+ size: "small",
247
+ variant: "text"
248
+ }), null, 16)]),
249
+ _: 1
250
+ }, 8, ["modelValue"]);
251
+ };
252
+ }
253
+ });
254
+ //#endregion
255
+ //#region src/components/root/root.vue
256
+ var root_default = /* @__PURE__ */ defineComponent({
257
+ inheritAttrs: false,
258
+ __name: "root",
259
+ props: /*@__PURE__*/ mergeModels({
260
+ dataTestid: {
261
+ type: String,
262
+ required: false
263
+ },
264
+ appBar: {
265
+ type: Object,
266
+ required: false
267
+ }
268
+ }, {
269
+ "notificationsOpen": {
270
+ type: Boolean,
271
+ default: false
272
+ },
273
+ "notificationsOpenModifiers": {}
274
+ }),
275
+ emits: ["update:notificationsOpen"],
276
+ setup(__props) {
277
+ const props = __props;
278
+ const notificationsOpen = useModel(__props, "notificationsOpen");
279
+ return (_ctx, _cache) => {
280
+ return openBlock(), createBlock(unref(VApp), { "data-testid": props.dataTestid }, {
281
+ default: withCtx(() => [props.appBar ? (openBlock(), createBlock(app_bar_default, mergeProps({ key: 0 }, props.appBar, {
282
+ "notifications-open": notificationsOpen.value,
283
+ "onUpdate:notificationsOpen": _cache[0] || (_cache[0] = ($event) => notificationsOpen.value = $event)
284
+ }), null, 16, ["notifications-open"])) : createCommentVNode("v-if", true), createVNode(unref(VMain), null, {
285
+ default: withCtx(() => [createVNode(container_default, null, {
286
+ default: withCtx(() => [renderSlot(_ctx.$slots, "default"), createVNode(toast_default)]),
287
+ _: 3
288
+ })]),
289
+ _: 3
290
+ })]),
291
+ _: 3
292
+ }, 8, ["data-testid"]);
293
+ };
294
+ }
295
+ });
296
+ //#endregion
24
297
  //#region src/composables/btn.ts
25
298
  const btnVariantToVuetifyVariant = {
26
299
  primary: "elevated",
27
300
  secondary: "flat",
28
301
  ghost: "outlined"
29
302
  };
30
- const btnColorToVuetifyColor = {
31
- primary: "primary",
32
- secondary: "secondary",
33
- positive: "success",
34
- informative: "info",
35
- caution: "warning",
36
- danger: "error"
37
- };
38
303
  function useBtnVariant(variant) {
39
304
  return computed(() => {
40
305
  const variantValue = toValue(variant);
41
306
  return variantValue ? btnVariantToVuetifyVariant[variantValue] : btnVariantToVuetifyVariant.primary;
42
307
  });
43
308
  }
44
- function useBtnColor(color) {
45
- return computed(() => {
46
- const colorValue = toValue(color);
47
- return colorValue ? btnColorToVuetifyColor[colorValue] : btnColorToVuetifyColor.primary;
48
- });
49
- }
50
309
  function useBtnType(type) {
51
310
  return computed(() => {
52
311
  return toValue(type) ?? "button";
@@ -95,7 +354,7 @@ var btn_default = /* @__PURE__ */ defineComponent({
95
354
  const props = __props;
96
355
  const emit = __emit;
97
356
  const variant = useBtnVariant(() => props.variant);
98
- const color = useBtnColor(() => props.color);
357
+ const color = useVuetifyColor(() => props.color);
99
358
  const type = useBtnType(() => props.type);
100
359
  const form = useBtnForm(() => props.form);
101
360
  function onClick(event) {
@@ -291,28 +550,6 @@ var text_field_default = /* @__PURE__ */ defineComponent({
291
550
  }
292
551
  });
293
552
  //#endregion
294
- //#region src/components/grid/container/container.vue
295
- var container_default = /* @__PURE__ */ defineComponent({
296
- inheritAttrs: false,
297
- __name: "container",
298
- props: { dataTestid: {
299
- type: String,
300
- required: false
301
- } },
302
- setup(__props) {
303
- const props = __props;
304
- return (_ctx, _cache) => {
305
- return openBlock(), createBlock(unref(VContainer), {
306
- "data-testid": props.dataTestid,
307
- fluid: ""
308
- }, {
309
- default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
310
- _: 3
311
- }, 8, ["data-testid"]);
312
- };
313
- }
314
- });
315
- //#endregion
316
553
  //#region src/components/grid/row/row.vue
317
554
  var row_default = /* @__PURE__ */ defineComponent({
318
555
  inheritAttrs: false,
@@ -0,0 +1,67 @@
1
+ import { n as FeedbackColorVariant } from "./types-BPXXtbmW.js";
2
+ //#region src/composables/dialogs/types.d.ts
3
+ interface ToastOptions {
4
+ /**
5
+ * Text to be presented in the toast.
6
+ */
7
+ message: string;
8
+ /**
9
+ * Applies a color to the button.
10
+ * One of `positive`, `informative`, `caution`, or `danger`.
11
+ * @default "informative"
12
+ */
13
+ color?: FeedbackColorVariant;
14
+ }
15
+ //#endregion
16
+ //#region src/composables/dialogs/use-toast.d.ts
17
+ /**
18
+ * Composable which returns a function to display toast messages.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * const { toast } = useToast();
23
+ * toast({ message: "Hello, World!" });
24
+ * ```
25
+ */
26
+ declare function useToast(): {
27
+ toast: typeof toast;
28
+ };
29
+ /**
30
+ * Displays a toast message.
31
+ * @param {ToastOptions} options - Options for the toast message.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * toast({ message: "Hello, World!" });
36
+ * ```
37
+ */
38
+ declare function toast(options: ToastOptions): void;
39
+ //#endregion
40
+ //#region src/composables/dialogs/use-run-or-toast..d.ts
41
+ /**
42
+ * Generate `run` method and a computed property `isRunning`.
43
+ * Calling `run` will reactively update `isRunning`.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const { run, isRunning } = useRunOrToast();
48
+ *
49
+ * // Logs: false
50
+ * console.log(isRunning.value);
51
+ *
52
+ * const promise = run(() => someFunction(param1, param2));
53
+ *
54
+ * // Logs: true
55
+ * console.log(isRunning.value);
56
+ *
57
+ * await promise;
58
+ * // Logs: false
59
+ * console.log(isRunning.value);
60
+ * ```
61
+ */
62
+ declare function useRunOrToast(): {
63
+ run: <T extends (...args: Parameters<T>) => PromiseLike<Awaited<ReturnType<T>>>>(toRun: T, ...params: Parameters<T>) => Promise<Awaited<ReturnType<T>> | false>;
64
+ isRunning: import("vue").ComputedRef<boolean>;
65
+ };
66
+ //#endregion
67
+ export { useRunOrToast, useToast };
@@ -0,0 +1,65 @@
1
+ import { n as useToast } from "./use-toast-CACuYhK_.js";
2
+ import { computed, ref } from "vue";
3
+ //#region src/composables/dialogs/use-run-or-toast..ts
4
+ const { toast } = useToast();
5
+ /**
6
+ * Generate `run` method and a computed property `isRunning`.
7
+ * Calling `run` will reactively update `isRunning`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const { run, isRunning } = useRunOrToast();
12
+ *
13
+ * // Logs: false
14
+ * console.log(isRunning.value);
15
+ *
16
+ * const promise = run(() => someFunction(param1, param2));
17
+ *
18
+ * // Logs: true
19
+ * console.log(isRunning.value);
20
+ *
21
+ * await promise;
22
+ * // Logs: false
23
+ * console.log(isRunning.value);
24
+ * ```
25
+ */
26
+ function useRunOrToast() {
27
+ const running = ref(false);
28
+ const isRunning = computed(() => running.value);
29
+ /**
30
+ * Receives a function and its parameters, runs it, and if it throws an error, shows a toast with the error message.
31
+ * @returns the function returned value or `false` on error
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const { run, isRunning } = useRunOrToast();
36
+ *
37
+ * // Call like this:
38
+ * const someFunctionResult1 = await run(() => someFunction(param1, param2));
39
+ *
40
+ * // Or this:
41
+ * const someFunctionResult2 = await run(someFunction, param1, param2);
42
+ * ```
43
+ */
44
+ async function run(toRun, ...params) {
45
+ try {
46
+ running.value = true;
47
+ return await toRun(...params);
48
+ } catch (error) {
49
+ if (error instanceof Error) toast({
50
+ message: error.message,
51
+ color: "danger"
52
+ });
53
+ else console.error(error);
54
+ return false;
55
+ } finally {
56
+ running.value = false;
57
+ }
58
+ }
59
+ return {
60
+ run,
61
+ isRunning
62
+ };
63
+ }
64
+ //#endregion
65
+ export { useRunOrToast, useToast };
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ import "vuetify/styles";
15
15
  import { Plugin } from "vue";
16
16
  //#region src/index.d.ts
17
17
  /**
18
- * Create an uimed-vue instance to be installed after [createApp](https://vuejs.org/guide/essentials/application.html#the-application-instance).
18
+ * Create an UIMed-Vue instance to be installed after [createApp](https://vuejs.org/guide/essentials/application.html#the-application-instance).
19
19
  * @returns an instance to be used with [app.use](https://vuejs.org/guide/essentials/application.html#the-application-instance)
20
20
  */
21
21
  declare function createUimed(): Plugin;
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import { createVuetify } from "vuetify";
16
16
  import "vue";
17
17
  //#region src/index.ts
18
18
  /**
19
- * Create an uimed-vue instance to be installed after [createApp](https://vuejs.org/guide/essentials/application.html#the-application-instance).
19
+ * Create an UIMed-Vue instance to be installed after [createApp](https://vuejs.org/guide/essentials/application.html#the-application-instance).
20
20
  * @returns an instance to be used with [app.use](https://vuejs.org/guide/essentials/application.html#the-application-instance)
21
21
  */
22
22
  function createUimed() {
@@ -0,0 +1,5 @@
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 };
@@ -0,0 +1,53 @@
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdom/uimed-vue",
3
- "version": "1.1.0",
3
+ "version": "1.2.0-beta.2",
4
4
  "description": "A ui framework for NEXDOM applications.",
5
5
  "keywords": [
6
6
  "lib",
@@ -21,12 +21,14 @@
21
21
  "home": "https://github.com/nexdom-healthtech/uimed-vue#readme"
22
22
  },
23
23
  "files": [
24
- "dist"
24
+ "dist",
25
+ "AGENTS.md"
25
26
  ],
26
27
  "type": "module",
27
28
  "exports": {
28
29
  ".": "./dist/index.js",
29
30
  "./components": "./dist/components.js",
31
+ "./composables": "./dist/composables.js",
30
32
  "./plugins": "./dist/plugins.js",
31
33
  "./unit-test": "./dist/unit-test.js",
32
34
  "./package.json": "./package.json"
@@ -78,8 +80,9 @@
78
80
  "vitepress": "2.0.0-alpha.19",
79
81
  "vitepress-plugin-group-icons": "^1.7.6",
80
82
  "vue": "^3.5.40",
83
+ "vue-router": "^5.3.1",
81
84
  "vue-tsc": "^3.3.9",
82
- "vuetify": "^4.1.7"
85
+ "vuetify": "^4.2.0"
83
86
  },
84
87
  "peerDependencies": {
85
88
  "@fontsource/roboto": "^5.3.0",
@@ -89,7 +92,8 @@
89
92
  "vite-plugin-vuetify": "^2.1.3",
90
93
  "vite-plus": "^0.2.7",
91
94
  "vue": "^3.5.40",
92
- "vuetify": "^4.1.7"
95
+ "vue-router": "^5.3.1",
96
+ "vuetify": "^4.2.0"
93
97
  },
94
98
  "commitlint": {
95
99
  "extends": [