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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -93,7 +93,7 @@ vp install # install dependencies
93
93
  ## Commands
94
94
 
95
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)
96
+ - `vp test --coverage` — unit tests with coverage (Vitest, jsdom, 100% coverage threshold enforced)
97
97
  - `vpr test:mutations` — mutation tests (Stryker; thresholds: high 100, low 95, break 95)
98
98
  - `vpr test:e2e` — E2E tests (Playwright, runs against the built docs preview site)
99
99
  - `vpr depcruise` — architecture/dependency rules (dependency-cruiser)
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/README.md CHANGED
@@ -4,6 +4,19 @@
4
4
  [![CD](https://github.com/nexdom-healthtech/uimed-vue/actions/workflows/cd.yml/badge.svg)](https://github.com/nexdom-healthtech/uimed-vue/actions/workflows/cd.yml)
5
5
  [![Dependabot](https://github.com/nexdom-healthtech/uimed-vue/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/nexdom-healthtech/uimed-vue/actions/workflows/dependabot/dependabot-updates)
6
6
 
7
+ [![Quality Gate Status](https://sonar-qube-public.services-dev.nexdom.tec.br/api/project_badges/measure?project=UIMed-Vue&metric=alert_status&token=sqb_12cf3033ebcf463511977eb64f95e614562e6773)](https://sonar-qube-public.services-dev.nexdom.tec.br/dashboard?id=UIMed-Vue)
8
+ [![Maintainability Rating](https://sonar-qube-public.services-dev.nexdom.tec.br/api/project_badges/measure?project=UIMed-Vue&metric=software_quality_maintainability_rating&token=sqb_12cf3033ebcf463511977eb64f95e614562e6773)](https://sonar-qube-public.services-dev.nexdom.tec.br/dashboard?id=UIMed-Vue)
9
+ [![Reliability Rating](https://sonar-qube-public.services-dev.nexdom.tec.br/api/project_badges/measure?project=UIMed-Vue&metric=software_quality_reliability_rating&token=sqb_12cf3033ebcf463511977eb64f95e614562e6773)](https://sonar-qube-public.services-dev.nexdom.tec.br/dashboard?id=UIMed-Vue)
10
+ [![Security Rating](https://sonar-qube-public.services-dev.nexdom.tec.br/api/project_badges/measure?project=UIMed-Vue&metric=software_quality_security_rating&token=sqb_12cf3033ebcf463511977eb64f95e614562e6773)](https://sonar-qube-public.services-dev.nexdom.tec.br/dashboard?id=UIMed-Vue)
11
+ [![Lines of Code](https://sonar-qube-public.services-dev.nexdom.tec.br/api/project_badges/measure?project=UIMed-Vue&metric=ncloc&token=sqb_12cf3033ebcf463511977eb64f95e614562e6773)](https://sonar-qube-public.services-dev.nexdom.tec.br/dashboard?id=UIMed-Vue)
12
+ [![Coverage](https://sonar-qube-public.services-dev.nexdom.tec.br/api/project_badges/measure?project=UIMed-Vue&metric=coverage&token=sqb_12cf3033ebcf463511977eb64f95e614562e6773)](https://sonar-qube-public.services-dev.nexdom.tec.br/dashboard?id=UIMed-Vue)
13
+ [![Duplicated Lines (%)](https://sonar-qube-public.services-dev.nexdom.tec.br/api/project_badges/measure?project=UIMed-Vue&metric=duplicated_lines_density&token=sqb_12cf3033ebcf463511977eb64f95e614562e6773)](https://sonar-qube-public.services-dev.nexdom.tec.br/dashboard?id=UIMed-Vue)
14
+ [![Technical Debt](https://sonar-qube-public.services-dev.nexdom.tec.br/api/project_badges/measure?project=UIMed-Vue&metric=software_quality_maintainability_remediation_effort&token=sqb_12cf3033ebcf463511977eb64f95e614562e6773)](https://sonar-qube-public.services-dev.nexdom.tec.br/dashboard?id=UIMed-Vue)
15
+
16
+ [![Docs Website](https://img.shields.io/website?url=https://nexdom-healthtech.github.io/uimed-vue/&label=Docs%20Website&logo=github)](https://nexdom-healthtech.github.io/uimed-vue)
17
+
18
+ [![semantic-release: conventionalcommits](https://img.shields.io/badge/semantic--release-conventionalcommits-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
19
+
7
20
  A ui framework for NEXDOM applications, based on [Material Design](https://m3.material.io/).
8
21
 
9
22
  For more examples and information, check the [docs page](https://nexdom-healthtech.github.io/uimed-vue/).
@@ -92,6 +92,47 @@ interface Option {
92
92
  action?: () => void;
93
93
  }
94
94
  //#endregion
95
+ //#region src/components/navigation-menu/types.d.ts
96
+ /**
97
+ * Props exposed by the {@link NavigationMenu} component.
98
+ */
99
+ type NavigationMenuProps = {
100
+ /**
101
+ * Component id to use on automated tests.
102
+ */
103
+ dataTestid?: string;
104
+ /**
105
+ * List of items to show in the {@link NavigationMenu}.
106
+ * Can be plain items, grouped or both.
107
+ */
108
+ items?: Array<NavigationMenuParentItemOrItem>;
109
+ };
110
+ type NavigationMenuParentItemOrItem = NavigationMenuParentItem | NavigationMenuItem;
111
+ type NavigationMenuParentItem = {
112
+ /**
113
+ * Text to present for the group.
114
+ */
115
+ description: string;
116
+ /**
117
+ * Child {@link NavigationMenuItem} list.
118
+ */
119
+ items: Array<NavigationMenuItem>;
120
+ };
121
+ interface NavigationMenuItem {
122
+ /**
123
+ * Text to present for the item.
124
+ */
125
+ description: string;
126
+ /**
127
+ * Route to navigate when item is clicked.
128
+ */
129
+ route?: RouteLocationRaw;
130
+ /**
131
+ * Callback to be triggered when the item is clicked.
132
+ */
133
+ action?: () => void;
134
+ }
135
+ //#endregion
95
136
  //#region src/components/root/types.d.ts
96
137
  /**
97
138
  * Props exposed by the {@link Root} component.
@@ -105,6 +146,10 @@ type RootProps = {
105
146
  * App bar properties.
106
147
  */
107
148
  appBar?: AppBarProps;
149
+ /**
150
+ * Navigation menu properties.
151
+ */
152
+ navigationMenu?: NavigationMenuProps;
108
153
  };
109
154
  //#endregion
110
155
  //#region src/components/root/root.vue.d.ts
@@ -293,7 +338,11 @@ declare const __VLS_export$4: import("vue", { with: { "resolution-mode": "import
293
338
  modelValue?: string;
294
339
  }> & Readonly<{
295
340
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
296
- }>, {}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>;
341
+ }>, {
342
+ type: TextFieldType;
343
+ variant: TextFieldVariant;
344
+ clearable: boolean;
345
+ }, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>;
297
346
  //#endregion
298
347
  //#region src/components/grid/container/types.d.ts
299
348
  /**
@@ -457,4 +506,4 @@ type __VLS_WithSlots<T, S> = T & {
457
506
  };
458
507
  };
459
508
  //#endregion
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 };
509
+ export { type AppBarProps as AppBarConfig, type AppBarUserProps as AppBarUserConfig, _default as Btn, _default$1 as Column, _default$2 as Container, _default$3 as Frm, type NavigationMenuProps as NavigationMenuConfig, _default$4 as Root, _default$5 as Row, _default$6 as TextField };
@@ -1,6 +1,6 @@
1
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";
2
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, isRef, mergeModels, mergeProps, openBlock, ref, renderList, renderSlot, toDisplayString, toValue, unref, useModel, withCtx, withModifiers } from "vue";
3
+ import { VApp, VAppBar, VAppBarNavIcon, VAvatar, VBadge, VBtn, VCard, VCardText, VCol, VContainer, VDivider, VForm, VIcon, VIconBtn, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VMain, VMenu, VNavigationDrawer, VRow, VSnackbarQueue, 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) {
@@ -15,7 +15,7 @@ function isExternalRoute(route) {
15
15
  }
16
16
  //#endregion
17
17
  //#region src/components/app-bar/app-bar.vue?vue&type=script&setup=true&lang.ts
18
- const _hoisted_1 = { class: "opacity-60" };
18
+ const _hoisted_1$1 = { class: "opacity-60" };
19
19
  //#endregion
20
20
  //#region src/components/app-bar/app-bar.vue
21
21
  var app_bar_default = /* @__PURE__ */ defineComponent({
@@ -41,20 +41,28 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
41
41
  notifications: {
42
42
  type: Array,
43
43
  required: false
44
+ },
45
+ navigation: {
46
+ type: Boolean,
47
+ required: true
44
48
  }
45
49
  }, {
46
50
  "notificationsOpen": {
47
51
  type: Boolean,
48
52
  default: false
49
53
  },
50
- "notificationsOpenModifiers": {}
54
+ "notificationsOpenModifiers": {},
55
+ "navigationOpen": { type: Boolean },
56
+ "navigationOpenModifiers": {}
51
57
  }),
52
- emits: ["update:notificationsOpen"],
58
+ emits: ["update:notificationsOpen", "update:navigationOpen"],
53
59
  setup(__props) {
54
60
  const props = __props;
55
61
  const notificationsOpen = useModel(__props, "notificationsOpen");
62
+ const navigationOpen = useModel(__props, "navigationOpen");
56
63
  const helpBtnDataTestid = computed(() => `${props.dataTestid}-help`);
57
64
  const notificationsBtnDataTestid = computed(() => `${props.dataTestid}-notifications`);
65
+ const navigationBtnDataTestid = computed(() => `${props.dataTestid}-navigation`);
58
66
  const notificationsMenuDataTestid = computed(() => `${props.dataTestid}-notifications-menu`);
59
67
  const userBtnDataTestid = computed(() => `${props.dataTestid}-user`);
60
68
  const userMenuDataTestid = computed(() => `${props.dataTestid}-user-menu`);
@@ -90,6 +98,11 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
90
98
  title: props.title,
91
99
  "data-testid": props.dataTestid
92
100
  }, {
101
+ prepend: withCtx(() => [__props.navigation ? (openBlock(), createBlock(unref(VAppBarNavIcon), {
102
+ key: 0,
103
+ "data-testid": navigationBtnDataTestid.value,
104
+ onClick: _cache[0] || (_cache[0] = ($event) => navigationOpen.value = !navigationOpen.value)
105
+ }, null, 8, ["data-testid"])) : createCommentVNode("v-if", true)]),
93
106
  append: withCtx(() => [
94
107
  showHelp.value ? (openBlock(), createBlock(unref(VBtn), mergeProps({ key: 0 }, unref(routeProps), {
95
108
  "data-testid": helpBtnDataTestid.value,
@@ -98,7 +111,7 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
98
111
  showNotifications.value ? (openBlock(), createBlock(unref(VMenu), {
99
112
  key: 1,
100
113
  modelValue: notificationsOpen.value,
101
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => notificationsOpen.value = $event),
114
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => notificationsOpen.value = $event),
102
115
  "close-on-content-click": false
103
116
  }, {
104
117
  activator: withCtx(({ props }) => [createVNode(unref(VBtn), mergeProps(props, {
@@ -118,7 +131,7 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
118
131
  default: withCtx(() => [createVNode(unref(VCard), { "data-testid": notificationsMenuDataTestid.value }, {
119
132
  default: withCtx(() => [props.notifications?.length ? (openBlock(), createBlock(unref(VList), { key: 0 }, {
120
133
  default: withCtx(() => [createVNode(unref(VListSubheader), null, {
121
- default: withCtx(() => [..._cache[1] || (_cache[1] = [createTextVNode("Notificações", -1)])]),
134
+ default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode("Notificações", -1)])]),
122
135
  _: 1
123
136
  }), (openBlock(true), createElementBlock(Fragment, null, renderList(props.notifications, (notification, index) => {
124
137
  return openBlock(), createBlock(unref(VListItem), {
@@ -130,7 +143,7 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
130
143
  }, createSlots({ _: 2 }, [notification.date ? {
131
144
  name: "append",
132
145
  fn: withCtx(() => [createVNode(unref(VListItemAction), { end: "" }, {
133
- default: withCtx(() => [createElementVNode("small", _hoisted_1, toDisplayString(formatNotificationDate(notification.date)), 1)]),
146
+ default: withCtx(() => [createElementVNode("small", _hoisted_1$1, toDisplayString(formatNotificationDate(notification.date)), 1)]),
134
147
  _: 2
135
148
  }, 1024)]),
136
149
  key: "0"
@@ -142,7 +155,7 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
142
155
  }), 128))]),
143
156
  _: 1
144
157
  })) : (openBlock(), createBlock(unref(VCardText), { key: 1 }, {
145
- default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode("Nenhuma notificação", -1)])]),
158
+ default: withCtx(() => [..._cache[3] || (_cache[3] = [createTextVNode("Nenhuma notificação", -1)])]),
146
159
  _: 1
147
160
  }))]),
148
161
  _: 1
@@ -202,6 +215,269 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
202
215
  }
203
216
  });
204
217
  //#endregion
218
+ //#region src/composables/inputs/rules.ts
219
+ function required(value) {
220
+ return !isEmpty(value) || "Campo obrigatório";
221
+ }
222
+ function phone(value) {
223
+ return isEmpty(value) || isPhone(value) || "Número inválido";
224
+ }
225
+ function email(value) {
226
+ return isEmpty(value) || isEmail(value) || "E-mail inválido";
227
+ }
228
+ function url(value) {
229
+ return isEmpty(value) || isUrl(value) || "URL inválida";
230
+ }
231
+ //#endregion
232
+ //#region src/composables/inputs/use-rules.ts
233
+ function useRules(options) {
234
+ return computed(() => {
235
+ const rules = [];
236
+ if (options.required) rules.push(required);
237
+ return rules;
238
+ });
239
+ }
240
+ //#endregion
241
+ //#region src/composables/inputs/text-field.ts
242
+ const textFieldVariantToVuetifyVariant = {
243
+ primary: "underlined",
244
+ secondary: "outlined"
245
+ };
246
+ const textFieldTypeToVuetifyType = {
247
+ text: "text",
248
+ phone: "tel",
249
+ email: "email",
250
+ url: "url",
251
+ password: "password",
252
+ search: "text"
253
+ };
254
+ const textFieldTypeToVuetifyRule = {
255
+ phone,
256
+ email,
257
+ url
258
+ };
259
+ function useTextFieldVariant(variant) {
260
+ return computed(() => {
261
+ const variantValue = toValue(variant);
262
+ return textFieldVariantToVuetifyVariant[variantValue];
263
+ });
264
+ }
265
+ function useTextFieldType(type) {
266
+ return computed(() => {
267
+ const typeValue = toValue(type);
268
+ return textFieldTypeToVuetifyType[typeValue];
269
+ });
270
+ }
271
+ function useTextFieldRules(props) {
272
+ const defaultRules = useRules(props);
273
+ return computed(() => {
274
+ const rules = [...defaultRules.value];
275
+ const typeRule = associatedRuleType(props.type) ? textFieldTypeToVuetifyRule[props.type] : void 0;
276
+ if (typeRule) rules.push(typeRule);
277
+ return rules;
278
+ });
279
+ }
280
+ function associatedRuleType(type) {
281
+ return type in textFieldTypeToVuetifyRule;
282
+ }
283
+ //#endregion
284
+ //#region src/components/inputs/text-field/text-field.vue
285
+ var text_field_default = /* @__PURE__ */ defineComponent({
286
+ inheritAttrs: false,
287
+ __name: "text-field",
288
+ props: /*@__PURE__*/ mergeModels({
289
+ variant: {
290
+ type: String,
291
+ required: false,
292
+ default: "primary"
293
+ },
294
+ type: {
295
+ type: String,
296
+ required: false,
297
+ default: "text"
298
+ },
299
+ disabled: {
300
+ type: Boolean,
301
+ required: false
302
+ },
303
+ loading: {
304
+ type: Boolean,
305
+ required: false
306
+ },
307
+ clearable: {
308
+ type: Boolean,
309
+ required: false,
310
+ default: void 0
311
+ },
312
+ readonly: {
313
+ type: Boolean,
314
+ required: false
315
+ },
316
+ required: {
317
+ type: Boolean,
318
+ required: false
319
+ },
320
+ label: {
321
+ type: String,
322
+ required: false
323
+ },
324
+ placeholder: {
325
+ type: String,
326
+ required: false
327
+ },
328
+ hint: {
329
+ type: String,
330
+ required: false
331
+ },
332
+ dataTestid: {
333
+ type: String,
334
+ required: false
335
+ }
336
+ }, {
337
+ "modelValue": {
338
+ type: String,
339
+ default: ""
340
+ },
341
+ "modelModifiers": {}
342
+ }),
343
+ emits: ["update:modelValue"],
344
+ setup(__props) {
345
+ const modelValue = useModel(__props, "modelValue");
346
+ const props = __props;
347
+ const vuetifyVariant = useTextFieldVariant(() => props.variant);
348
+ const vuetifyType = useTextFieldType(() => props.type);
349
+ const rules = useTextFieldRules(props);
350
+ const isSearchField = computed(() => props.type === "search");
351
+ const computedClearable = computed(() => props.clearable ?? isSearchField.value);
352
+ const prependIcon = computed(() => isSearchField.value ? "mdi-magnify" : void 0);
353
+ function clear() {
354
+ modelValue.value = "";
355
+ }
356
+ return (_ctx, _cache) => {
357
+ return openBlock(), createBlock(unref(VTextField), {
358
+ modelValue: modelValue.value,
359
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => modelValue.value = $event),
360
+ variant: unref(vuetifyVariant),
361
+ type: unref(vuetifyType),
362
+ "prepend-inner-icon": prependIcon.value,
363
+ rules: unref(rules),
364
+ "data-testid": props.dataTestid,
365
+ disabled: props.disabled,
366
+ loading: props.loading,
367
+ readonly: props.readonly,
368
+ label: props.label,
369
+ placeholder: props.placeholder,
370
+ hint: props.hint,
371
+ clearable: computedClearable.value,
372
+ "onClick:clear": clear
373
+ }, null, 8, [
374
+ "modelValue",
375
+ "variant",
376
+ "type",
377
+ "prepend-inner-icon",
378
+ "rules",
379
+ "data-testid",
380
+ "disabled",
381
+ "loading",
382
+ "readonly",
383
+ "label",
384
+ "placeholder",
385
+ "hint",
386
+ "clearable"
387
+ ]);
388
+ };
389
+ }
390
+ });
391
+ //#endregion
392
+ //#region src/components/navigation-menu/navigation-menu.vue?vue&type=script&setup=true&lang.ts
393
+ const _hoisted_1 = { class: "px-4 pt-2" };
394
+ //#endregion
395
+ //#region src/components/navigation-menu/navigation-menu.vue
396
+ var navigation_menu_default = /* @__PURE__ */ defineComponent({
397
+ inheritAttrs: false,
398
+ __name: "navigation-menu",
399
+ props: /*@__PURE__*/ mergeModels({
400
+ dataTestid: {
401
+ type: String,
402
+ required: false
403
+ },
404
+ items: {
405
+ type: Array,
406
+ required: false
407
+ }
408
+ }, {
409
+ "modelValue": {
410
+ type: Boolean,
411
+ default: false
412
+ },
413
+ "modelModifiers": {}
414
+ }),
415
+ emits: ["update:modelValue"],
416
+ setup(__props) {
417
+ const props = __props;
418
+ const open = useModel(__props, "modelValue");
419
+ const search = ref("");
420
+ const filteredItems = computed(() => filterItems());
421
+ const searchDataTestid = computed(() => `${props.dataTestid}-search`);
422
+ const query = computed(() => search.value.toLowerCase());
423
+ function filterItems() {
424
+ return (props.items ?? []).reduce((filtered, item) => {
425
+ if (matches(item)) filtered.push(item);
426
+ else if (isParentItem(item)) {
427
+ const matchingChildren = item.items.filter((child) => matches(child));
428
+ if (matchingChildren.length) filtered.push({
429
+ ...item,
430
+ items: matchingChildren
431
+ });
432
+ }
433
+ return filtered;
434
+ }, []);
435
+ }
436
+ function matches(item) {
437
+ return item.description.toLowerCase().includes(query.value);
438
+ }
439
+ function isParentItem(item) {
440
+ return "items" in item;
441
+ }
442
+ return (_ctx, _cache) => {
443
+ return openBlock(), createBlock(unref(VNavigationDrawer), {
444
+ modelValue: open.value,
445
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => open.value = $event),
446
+ "data-testid": props.dataTestid,
447
+ absolute: "",
448
+ temporary: ""
449
+ }, {
450
+ prepend: withCtx(() => [createElementVNode("div", _hoisted_1, [createVNode(text_field_default, {
451
+ modelValue: search.value,
452
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event),
453
+ type: "search",
454
+ label: "Buscar",
455
+ "data-testid": searchDataTestid.value
456
+ }, null, 8, ["modelValue", "data-testid"])])]),
457
+ default: withCtx(() => [createVNode(unref(VDivider)), createVNode(unref(VList), { nav: "" }, {
458
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(filteredItems.value, (item, index) => {
459
+ return openBlock(), createElementBlock(Fragment, { key: index }, [isParentItem(item) ? (openBlock(), createBlock(unref(VListGroup), { key: 0 }, {
460
+ activator: withCtx(({ props: activatorProps }) => [createVNode(unref(VListItem), mergeProps({ ref_for: true }, activatorProps, { title: item.description }), null, 16, ["title"])]),
461
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(item.items, (childItem, childIndex) => {
462
+ return openBlock(), createBlock(unref(VListItem), mergeProps({ key: childIndex }, { ref_for: true }, unref(routeToProps)(childItem.route), {
463
+ title: childItem.description,
464
+ onClick: childItem.action
465
+ }), null, 16, ["title", "onClick"]);
466
+ }), 128))]),
467
+ _: 2
468
+ }, 1024)) : (openBlock(), createBlock(unref(VListItem), mergeProps({
469
+ key: 1,
470
+ title: item.description
471
+ }, { ref_for: true }, unref(routeToProps)(item.route), { onClick: item.action }), null, 16, ["title", "onClick"]))], 64);
472
+ }), 128))]),
473
+ _: 1
474
+ })]),
475
+ _: 1
476
+ }, 8, ["modelValue", "data-testid"]);
477
+ };
478
+ }
479
+ });
480
+ //#endregion
205
481
  //#region src/components/grid/container/container.vue
206
482
  var container_default = /* @__PURE__ */ defineComponent({
207
483
  inheritAttrs: false,
@@ -264,6 +540,10 @@ var root_default = /* @__PURE__ */ defineComponent({
264
540
  appBar: {
265
541
  type: Object,
266
542
  required: false
543
+ },
544
+ navigationMenu: {
545
+ type: Object,
546
+ required: false
267
547
  }
268
548
  }, {
269
549
  "notificationsOpen": {
@@ -275,19 +555,33 @@ var root_default = /* @__PURE__ */ defineComponent({
275
555
  emits: ["update:notificationsOpen"],
276
556
  setup(__props) {
277
557
  const props = __props;
558
+ const appBarProps = computed(() => props.appBar ? {
559
+ ...props.appBar,
560
+ navigation: props.navigationMenu !== void 0
561
+ } : void 0);
278
562
  const notificationsOpen = useModel(__props, "notificationsOpen");
563
+ const navigationMenuOpen = ref(false);
279
564
  return (_ctx, _cache) => {
280
565
  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)]),
566
+ default: withCtx(() => [
567
+ appBarProps.value ? (openBlock(), createBlock(app_bar_default, mergeProps({ key: 0 }, appBarProps.value, {
568
+ "notifications-open": notificationsOpen.value,
569
+ "onUpdate:notificationsOpen": _cache[0] || (_cache[0] = ($event) => notificationsOpen.value = $event),
570
+ "navigation-open": navigationMenuOpen.value,
571
+ "onUpdate:navigationOpen": _cache[1] || (_cache[1] = ($event) => navigationMenuOpen.value = $event)
572
+ }), null, 16, ["notifications-open", "navigation-open"])) : createCommentVNode("v-if", true),
573
+ props.navigationMenu ? (openBlock(), createBlock(navigation_menu_default, mergeProps({ key: 1 }, props.navigationMenu, {
574
+ modelValue: navigationMenuOpen.value,
575
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => navigationMenuOpen.value = $event)
576
+ }), null, 16, ["modelValue"])) : createCommentVNode("v-if", true),
577
+ createVNode(unref(VMain), null, {
578
+ default: withCtx(() => [createVNode(container_default, null, {
579
+ default: withCtx(() => [renderSlot(_ctx.$slots, "default"), createVNode(toast_default)]),
580
+ _: 3
581
+ })]),
287
582
  _: 3
288
- })]),
289
- _: 3
290
- })]),
583
+ })
584
+ ]),
291
585
  _: 3
292
586
  }, 8, ["data-testid"]);
293
587
  };
@@ -386,170 +680,6 @@ var btn_default = /* @__PURE__ */ defineComponent({
386
680
  }
387
681
  });
388
682
  //#endregion
389
- //#region src/composables/inputs/rules.ts
390
- function required(value) {
391
- return !isEmpty(value) || "Campo obrigatório";
392
- }
393
- function phone(value) {
394
- return isEmpty(value) || isPhone(value) || "Número inválido";
395
- }
396
- function email(value) {
397
- return isEmpty(value) || isEmail(value) || "E-mail inválido";
398
- }
399
- function url(value) {
400
- return isEmpty(value) || isUrl(value) || "URL inválida";
401
- }
402
- //#endregion
403
- //#region src/composables/inputs/use-rules.ts
404
- function useRules(options) {
405
- return computed(() => {
406
- const rules = [];
407
- if (options.required) rules.push(required);
408
- return rules;
409
- });
410
- }
411
- //#endregion
412
- //#region src/composables/inputs/text-field.ts
413
- const textFieldVariantToVuetifyVariant = {
414
- primary: "underlined",
415
- secondary: "outlined"
416
- };
417
- const textFieldTypeToVuetifyType = {
418
- text: "text",
419
- phone: "tel",
420
- email: "email",
421
- url: "url",
422
- password: "password",
423
- search: "text"
424
- };
425
- const textFieldTypeToVuetifyRule = {
426
- phone,
427
- email,
428
- url
429
- };
430
- function useTextFieldVariant(variant) {
431
- return computed(() => {
432
- const variantValue = toValue(variant);
433
- return variantValue ? textFieldVariantToVuetifyVariant[variantValue] : textFieldVariantToVuetifyVariant.primary;
434
- });
435
- }
436
- function useTextFieldType(type) {
437
- return computed(() => {
438
- const typeValue = toValue(type);
439
- return typeValue ? textFieldTypeToVuetifyType[typeValue] : textFieldTypeToVuetifyType.text;
440
- });
441
- }
442
- function useTextFieldRules(props) {
443
- const defaultRules = useRules(props);
444
- return computed(() => {
445
- const rules = [...defaultRules.value];
446
- const typeRule = associatedRuleType(props.type) ? textFieldTypeToVuetifyRule[props.type] : void 0;
447
- if (typeRule) rules.push(typeRule);
448
- return rules;
449
- });
450
- }
451
- function associatedRuleType(type) {
452
- return type in textFieldTypeToVuetifyRule;
453
- }
454
- //#endregion
455
- //#region src/components/inputs/text-field/text-field.vue
456
- var text_field_default = /* @__PURE__ */ defineComponent({
457
- inheritAttrs: false,
458
- __name: "text-field",
459
- props: /*@__PURE__*/ mergeModels({
460
- variant: {
461
- type: String,
462
- required: false
463
- },
464
- type: {
465
- type: String,
466
- required: false
467
- },
468
- disabled: {
469
- type: Boolean,
470
- required: false
471
- },
472
- loading: {
473
- type: Boolean,
474
- required: false
475
- },
476
- clearable: {
477
- type: Boolean,
478
- required: false,
479
- default: () => void 0
480
- },
481
- readonly: {
482
- type: Boolean,
483
- required: false
484
- },
485
- required: {
486
- type: Boolean,
487
- required: false
488
- },
489
- label: {
490
- type: String,
491
- required: false
492
- },
493
- placeholder: {
494
- type: String,
495
- required: false
496
- },
497
- hint: {
498
- type: String,
499
- required: false
500
- },
501
- dataTestid: {
502
- type: String,
503
- required: false
504
- }
505
- }, {
506
- "modelValue": {
507
- type: String,
508
- default: ""
509
- },
510
- "modelModifiers": {}
511
- }),
512
- emits: ["update:modelValue"],
513
- setup(__props) {
514
- const modelValue = useModel(__props, "modelValue");
515
- const props = createPropsRestProxy(__props, ["clearable"]);
516
- const vuetifyVariant = useTextFieldVariant(() => props.variant);
517
- const vuetifyType = useTextFieldType(() => props.type);
518
- const rules = useTextFieldRules(props);
519
- const computedClearable = computed(() => __props.clearable ?? props.type === "search");
520
- return (_ctx, _cache) => {
521
- return openBlock(), createBlock(unref(VTextField), {
522
- modelValue: modelValue.value,
523
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => modelValue.value = $event),
524
- variant: unref(vuetifyVariant),
525
- type: unref(vuetifyType),
526
- rules: unref(rules),
527
- "data-testid": props.dataTestid,
528
- disabled: props.disabled,
529
- loading: props.loading,
530
- readonly: props.readonly,
531
- label: props.label,
532
- placeholder: props.placeholder,
533
- hint: props.hint,
534
- clearable: computedClearable.value
535
- }, null, 8, [
536
- "modelValue",
537
- "variant",
538
- "type",
539
- "rules",
540
- "data-testid",
541
- "disabled",
542
- "loading",
543
- "readonly",
544
- "label",
545
- "placeholder",
546
- "hint",
547
- "clearable"
548
- ]);
549
- };
550
- }
551
- });
552
- //#endregion
553
683
  //#region src/components/grid/row/row.vue
554
684
  var row_default = /* @__PURE__ */ defineComponent({
555
685
  inheritAttrs: false,
package/dist/unit-test.js CHANGED
@@ -16,7 +16,7 @@ const vuetify = createVuetify({
16
16
  * Vue Test Utils plugin for Uimed.
17
17
  */
18
18
  function vueTestUtilsPluginUimed() {
19
- global.ResizeObserver = __require("resize-observer-polyfill");
19
+ globalThis.ResizeObserver = __require("resize-observer-polyfill");
20
20
  return vuetify;
21
21
  }
22
22
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdom/uimed-vue",
3
- "version": "1.2.0-beta.2",
3
+ "version": "1.2.0",
4
4
  "description": "A ui framework for NEXDOM applications.",
5
5
  "keywords": [
6
6
  "lib",
@@ -22,7 +22,8 @@
22
22
  },
23
23
  "files": [
24
24
  "dist",
25
- "AGENTS.md"
25
+ "AGENTS.md",
26
+ "CLAUDE.md"
26
27
  ],
27
28
  "type": "module",
28
29
  "exports": {
@@ -58,6 +59,7 @@
58
59
  "@mdi/font": "^7.4.47",
59
60
  "@nexdom/shared": "^2.1.1",
60
61
  "@playwright/test": "^1.62.1",
62
+ "@sonar/scan": "^5.0.0",
61
63
  "@stryker-mutator/core": "^10.0.0",
62
64
  "@stryker-mutator/vitest-runner": "^10.0.0",
63
65
  "@types/node": "^26.1.2",