@nexdom/uimed-vue 2.0.0-beta.4 → 2.0.0-beta.41

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.
@@ -1,7 +1,11 @@
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, openBlock, ref, renderList, renderSlot, toDisplayString, toValue, unref, useModel, withCtx, withModifiers } from "vue";
3
- import { VApp, VAppBar, VAppBarNavIcon, VAppBarTitle, VAvatar, VBadge, VBtn, VCard, VCardActions, VCardText, VCheckboxBtn, VCol, VContainer, VDivider, VForm, VIcon, VIconBtn, VImg, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VMain, VMenu, VNavigationDrawer, VRow, VSkeletonLoader, VSnackbarQueue, VTable, VTextField } from "vuetify/components";
4
- import { formatDateTime, isEmail, isEmpty, isPhone, isUrl, toPeriodInterval } from "@nexdom/shared/utils";
1
+ import { a as messages, n as owners$1, r as requests, s as useVuetifyColor } from "./use-dialog-Ck2_ZW7T.js";
2
+ import { useLocale, useMask } from "vuetify";
3
+ import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, inject, isRef, mergeModels, mergeProps, nextTick, normalizeClass, normalizeProps, onMounted, onScopeDispose, onUnmounted, openBlock, provide, readonly, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toValue, unref, useId, useModel, useSlots, useTemplateRef, watch, withCtx, withKeys, withModifiers } from "vue";
4
+ import { pt } from "vuetify/locale";
5
+ import { VApp, VAppBar, VAppBarNavIcon, VAppBarTitle, VAutocomplete, VAvatar, VBadge, VBtn, VCard, VCardActions, VCardText, VCheckboxBtn, VCol, VCombobox, VContainer, VDataIterator, VDatePicker, VDialog, VDivider, VEmptyState, VExpansionPanel, VExpansionPanelText, VExpansionPanelTitle, VExpansionPanels, VFooter, VForm, VIcon, VIconBtn, VImg, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VLocaleProvider, VMain, VMenu, VNavigationDrawer, VPagination, VProgressCircular, VProgressLinear, VRow, VSkeletonLoader, VSnackbarQueue, VTable, VTextField, VTimePicker } from "vuetify/components";
6
+ import { formatDateTime, isEmail, isEmpty, isPhone, isUrl } from "@nexdom/shared/utils";
7
+ import { VHighlight } from "vuetify/labs/VHighlight";
8
+ import { RouterLink } from "vue-router";
5
9
  //#region src/composables/navigation/use-route-props.ts
6
10
  function useRouteProps(route) {
7
11
  return computed(() => routeToProps(route));
@@ -14,8 +18,175 @@ function isExternalRoute(route) {
14
18
  return typeof route === "string" && route.startsWith("http");
15
19
  }
16
20
  //#endregion
21
+ //#region src/composables/navigation/use-navigation-search-shortcut.ts
22
+ /**
23
+ * Mounted navigation menus, in mounting order. Only the first one answers the shortcut, so pages
24
+ * with more than one `UMain` (like the docs) don't open every menu at once.
25
+ */
26
+ const owners = ref([]);
27
+ /**
28
+ * Focus targets where the shortcut is left to the page: overlays (dialogs, confirmations and
29
+ * menus) and other modals, the activators of open menus (which keep the focus when opened by a
30
+ * click, and would pull it back from the navigation menu) and rich text editors, which usually
31
+ * take Ctrl+K to insert links.
32
+ */
33
+ const IGNORED_TARGETS = [
34
+ ".v-overlay",
35
+ "dialog",
36
+ "[aria-modal='true']",
37
+ "[aria-haspopup][aria-expanded='true']",
38
+ "[contenteditable]:not([contenteditable='false'])"
39
+ ].join(", ");
40
+ /** Accessible name of the search field, whose placeholder also shows the shortcut. */
41
+ const SEARCH_NAME = "Buscar";
42
+ /**
43
+ * Shortcut keys for the current platform. Starts with the non-macOS keys and switches on mount,
44
+ * so server-rendered pages (the docs) hydrate without mismatches.
45
+ */
46
+ function useNavigationShortcutKeys() {
47
+ const isMac = ref(false);
48
+ onMounted(() => isMac.value = isMacOS());
49
+ return {
50
+ label: computed(() => isMac.value ? "⌘K" : "Ctrl+K"),
51
+ ariaKeyshortcuts: computed(() => isMac.value ? "Meta+K" : "Control+K")
52
+ };
53
+ }
54
+ /**
55
+ * Listens to Ctrl+K (⌘K on macOS) on `window` while mounted: opens the menu, waits for it to
56
+ * render (the drawer is `inert` while closed) and focuses the search field (a disabled field, as
57
+ * while loading, can't take the focus). When the menu opened by the shortcut closes with the focus
58
+ * inside it, the focus goes back to where it was.
59
+ */
60
+ function useNavigationSearchShortcut(options) {
61
+ const { open, field } = options;
62
+ const owner = Symbol("navigation-search-shortcut");
63
+ /** Element focused before the shortcut opened the menu, which gets the focus back on close. */
64
+ let origin = null;
65
+ onMounted(() => {
66
+ owners.value.push(owner);
67
+ globalThis.addEventListener("keydown", onKeydown, { capture: true });
68
+ findInput().setAttribute("aria-label", SEARCH_NAME);
69
+ });
70
+ onUnmounted(() => {
71
+ owners.value = owners.value.filter((item) => item !== owner);
72
+ globalThis.removeEventListener("keydown", onKeydown, { capture: true });
73
+ });
74
+ watch(open, (isOpen) => {
75
+ if (isOpen) return;
76
+ const target = origin;
77
+ origin = null;
78
+ if (!findInput().closest(".v-navigation-drawer").contains(document.activeElement)) return;
79
+ if (target instanceof HTMLElement || target instanceof SVGElement) target.focus();
80
+ });
81
+ function onKeydown(event) {
82
+ if (owners.value[0] !== owner || isIgnoredTarget(event)) return;
83
+ if (isShortcut(event)) openAndFocus(event);
84
+ else if (event.key === "Escape" && origin) close();
85
+ }
86
+ async function openAndFocus(event) {
87
+ event.preventDefault();
88
+ if (!open.value) origin = document.activeElement;
89
+ open.value = true;
90
+ await nextTick();
91
+ const input = findInput();
92
+ input.focus({ preventScroll: true });
93
+ input.select();
94
+ }
95
+ function findInput() {
96
+ return field.value.$el.querySelector("input");
97
+ }
98
+ function close() {
99
+ open.value = false;
100
+ }
101
+ return {
102
+ ...useNavigationShortcutKeys(),
103
+ close
104
+ };
105
+ }
106
+ function isMacOS() {
107
+ return navigator.userAgent.includes("Macintosh");
108
+ }
109
+ function isShortcut(event) {
110
+ return (isMacOS() ? event.metaKey : event.ctrlKey) && !event.shiftKey && !event.altKey && !event.isComposing && event.key.toLowerCase() === "k";
111
+ }
112
+ function isIgnoredTarget(event) {
113
+ return event.target instanceof Element && event.target.closest(IGNORED_TARGETS) !== null;
114
+ }
115
+ //#endregion
116
+ //#region src/composables/dimensions/dimensions.ts
117
+ /**
118
+ * Vuetify's utility class that stretches a component to the whole width of its parent, for
119
+ * components with a `fullWidth` prop.
120
+ */
121
+ function useFullWidth(fullWidth) {
122
+ return computed(() => toValue(fullWidth) ? "w-100" : void 0);
123
+ }
124
+ /**
125
+ * Vuetify's utility class that stretches a component to the whole height of its parent, for
126
+ * components with a `fullHeight` prop. The parent must have a height of its own.
127
+ */
128
+ function useFullHeight(fullHeight) {
129
+ return computed(() => toValue(fullHeight) ? "h-100" : void 0);
130
+ }
131
+ //#endregion
132
+ //#region src/components/skeleton-loader/skeleton-loader.vue
133
+ var skeleton_loader_default = /* @__PURE__ */ defineComponent({
134
+ inheritAttrs: false,
135
+ __name: "skeleton-loader",
136
+ props: {
137
+ loading: {
138
+ type: Boolean,
139
+ required: false
140
+ },
141
+ type: {
142
+ type: null,
143
+ required: true
144
+ },
145
+ fullWidth: {
146
+ type: Boolean,
147
+ required: false
148
+ },
149
+ fullHeight: {
150
+ type: Boolean,
151
+ required: false
152
+ },
153
+ color: {
154
+ type: String,
155
+ required: false
156
+ }
157
+ },
158
+ setup(__props) {
159
+ const props = __props;
160
+ const { t } = useLocale();
161
+ const widthClass = useFullWidth(() => props.fullWidth);
162
+ const heightClass = useFullHeight(() => props.fullHeight);
163
+ const loadingAttrs = computed(() => ({
164
+ ariaLabel: void 0,
165
+ ariaLive: void 0,
166
+ "aria-label": t("$vuetify.loading"),
167
+ "aria-live": "polite"
168
+ }));
169
+ return (_ctx, _cache) => {
170
+ return openBlock(), createBlock(unref(VSkeletonLoader), mergeProps({
171
+ loading: props.loading,
172
+ type: props.type,
173
+ color: props.color,
174
+ class: [unref(widthClass), unref(heightClass)]
175
+ }, loadingAttrs.value), {
176
+ default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
177
+ _: 3
178
+ }, 16, [
179
+ "loading",
180
+ "type",
181
+ "color",
182
+ "class"
183
+ ]);
184
+ };
185
+ }
186
+ });
187
+ //#endregion
17
188
  //#region src/components/app-bar/app-bar.vue?vue&type=script&setup=true&lang.ts
18
- const _hoisted_1$2 = { class: "opacity-60" };
189
+ const _hoisted_1$3 = { class: "opacity-60" };
19
190
  //#endregion
20
191
  //#region src/components/app-bar/app-bar.vue
21
192
  var app_bar_default = /* @__PURE__ */ defineComponent({
@@ -49,6 +220,10 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
49
220
  logo: {
50
221
  type: String,
51
222
  required: false
223
+ },
224
+ loading: {
225
+ type: Boolean,
226
+ required: false
52
227
  }
53
228
  }, {
54
229
  "notificationsOpen": {
@@ -64,6 +239,7 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
64
239
  const props = __props;
65
240
  const notificationsOpen = useModel(__props, "notificationsOpen");
66
241
  const navigationOpen = useModel(__props, "navigationOpen");
242
+ const { ariaKeyshortcuts: navigationKeyshortcuts } = useNavigationShortcutKeys();
67
243
  const helpBtnDataTestid = computed(() => `${props.dataTestid}-help`);
68
244
  const notificationsBtnDataTestid = computed(() => `${props.dataTestid}-notifications`);
69
245
  const navigationBtnDataTestid = computed(() => `${props.dataTestid}-navigation`);
@@ -75,157 +251,174 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
75
251
  const showNotifications = computed(() => !!props.notifications);
76
252
  const unreadCount = computed(() => props.notifications.filter((notification) => !notification.read).length);
77
253
  const hasUnreadNotifications = computed(() => unreadCount.value > 0);
254
+ const unreadLabel = computed(() => unreadCount.value === 1 ? "1 não lida" : `${unreadCount.value} não lidas`);
255
+ const notificationsLabel = computed(() => hasUnreadNotifications.value ? `Notificações (${unreadLabel.value})` : "Notificações");
256
+ watch(() => props.loading, (loading) => {
257
+ if (loading) notificationsOpen.value = false;
258
+ });
78
259
  const showUser = computed(() => !!props.user);
79
260
  const userAvatarProps = computed(() => props.user.img ? { image: props.user.img } : { icon: "mdi-account-outline" });
80
261
  function isParentOption(option) {
81
262
  return "items" in option;
82
263
  }
83
- function formatNotificationDate(date) {
84
- const interval = toPeriodInterval(date);
85
- if (wasThisMonth(interval)) {
86
- if (wasToday(interval)) return formatDateTime(date, "HH:mm");
87
- else if (wasYesterday(interval)) return "Ontem";
88
- }
89
- return formatDateTime(date, "DD/MM/YYYY");
90
- }
91
- function wasThisMonth(interval) {
92
- return interval.months === 0 && interval.years === 0;
93
- }
94
- function wasToday(interval) {
95
- return interval.days === 0;
96
- }
97
- function wasYesterday(interval) {
98
- return interval.days === 1;
99
- }
100
264
  return (_ctx, _cache) => {
101
- return openBlock(), createBlock(unref(VAppBar), { "data-testid": props.dataTestid }, {
102
- append: withCtx(() => [
103
- showHelp.value ? (openBlock(), createBlock(unref(VBtn), mergeProps({ key: 0 }, unref(routeProps), {
104
- "data-testid": helpBtnDataTestid.value,
105
- icon: "mdi-help-circle-outline"
106
- }), null, 16, ["data-testid"])) : createCommentVNode("v-if", true),
107
- showNotifications.value ? (openBlock(), createBlock(unref(VMenu), {
108
- key: 1,
109
- modelValue: notificationsOpen.value,
110
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => notificationsOpen.value = $event),
111
- "close-on-content-click": false
112
- }, {
113
- activator: withCtx(({ props }) => [createVNode(unref(VBtn), mergeProps(props, {
114
- "data-testid": notificationsBtnDataTestid.value,
115
- icon: ""
116
- }), {
117
- default: withCtx(() => [createVNode(unref(VBadge), {
118
- "model-value": hasUnreadNotifications.value,
119
- content: unreadCount.value,
120
- color: "error"
121
- }, {
122
- default: withCtx(() => [createVNode(unref(VIcon), { icon: "mdi-bell-outline" })]),
123
- _: 1
124
- }, 8, ["model-value", "content"])]),
125
- _: 1
126
- }, 16, ["data-testid"])]),
127
- default: withCtx(() => [createVNode(unref(VCard), { "data-testid": notificationsMenuDataTestid.value }, {
128
- default: withCtx(() => [props.notifications?.length ? (openBlock(), createBlock(unref(VList), { key: 0 }, {
129
- default: withCtx(() => [createVNode(unref(VListSubheader), null, {
130
- default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode("Notificações", -1)])]),
265
+ return openBlock(), createBlock(unref(VAppBar), {
266
+ "data-testid": props.dataTestid,
267
+ "aria-busy": props.loading || void 0
268
+ }, {
269
+ append: withCtx(() => [createVNode(skeleton_loader_default, {
270
+ loading: props.loading,
271
+ type: "avatar"
272
+ }, {
273
+ default: withCtx(() => [
274
+ showHelp.value ? (openBlock(), createBlock(unref(VBtn), mergeProps({ key: 0 }, unref(routeProps), {
275
+ "data-testid": helpBtnDataTestid.value,
276
+ "aria-label": "Ajuda",
277
+ icon: "mdi-help-circle-outline"
278
+ }), null, 16, ["data-testid"])) : createCommentVNode("v-if", true),
279
+ showNotifications.value ? (openBlock(), createBlock(unref(VMenu), {
280
+ key: 1,
281
+ modelValue: notificationsOpen.value,
282
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => notificationsOpen.value = $event),
283
+ "close-on-content-click": false
284
+ }, {
285
+ activator: withCtx(({ props }) => [createVNode(unref(VBtn), mergeProps(props, {
286
+ "data-testid": notificationsBtnDataTestid.value,
287
+ "aria-label": notificationsLabel.value,
288
+ icon: ""
289
+ }), {
290
+ default: withCtx(() => [createVNode(unref(VBadge), {
291
+ "model-value": hasUnreadNotifications.value,
292
+ content: unreadCount.value,
293
+ label: unreadLabel.value,
294
+ color: "error"
295
+ }, {
296
+ default: withCtx(() => [createVNode(unref(VIcon), { icon: "mdi-bell-outline" })]),
131
297
  _: 1
132
- }), (openBlock(true), createElementBlock(Fragment, null, renderList(props.notifications, (notification, index) => {
133
- return openBlock(), createBlock(unref(VListItem), {
134
- key: index,
135
- active: !notification.read,
136
- title: notification.title,
137
- subtitle: notification.subtitle,
138
- "active-class": "text-primary"
139
- }, createSlots({ _: 2 }, [notification.date ? {
140
- name: "append",
141
- fn: withCtx(() => [createVNode(unref(VListItemAction), { end: "" }, {
142
- default: withCtx(() => [createElementVNode("small", _hoisted_1$2, toDisplayString(formatNotificationDate(notification.date)), 1)]),
143
- _: 2
144
- }, 1024)]),
145
- key: "0"
146
- } : void 0]), 1032, [
147
- "active",
148
- "title",
149
- "subtitle"
150
- ]);
151
- }), 128))]),
298
+ }, 8, [
299
+ "model-value",
300
+ "content",
301
+ "label"
302
+ ])]),
152
303
  _: 1
153
- })) : (openBlock(), createBlock(unref(VCardText), { key: 1 }, {
154
- default: withCtx(() => [..._cache[3] || (_cache[3] = [createTextVNode("Nenhuma notificação", -1)])]),
304
+ }, 16, ["data-testid", "aria-label"])]),
305
+ default: withCtx(() => [createVNode(unref(VCard), { "data-testid": notificationsMenuDataTestid.value }, {
306
+ default: withCtx(() => [props.notifications?.length ? (openBlock(), createBlock(unref(VList), { key: 0 }, {
307
+ default: withCtx(() => [createVNode(unref(VListSubheader), null, {
308
+ default: withCtx(() => [..._cache[2] || (_cache[2] = [createTextVNode("Notificações", -1)])]),
309
+ _: 1
310
+ }), (openBlock(true), createElementBlock(Fragment, null, renderList(props.notifications, (notification, index) => {
311
+ return openBlock(), createBlock(unref(VListItem), {
312
+ key: index,
313
+ active: !notification.read,
314
+ title: notification.title,
315
+ subtitle: notification.subtitle,
316
+ "active-class": "text-primary"
317
+ }, createSlots({ _: 2 }, [notification.when ? {
318
+ name: "append",
319
+ fn: withCtx(() => [createVNode(unref(VListItemAction), { end: "" }, {
320
+ default: withCtx(() => [createElementVNode("small", _hoisted_1$3, toDisplayString(notification.when), 1)]),
321
+ _: 2
322
+ }, 1024)]),
323
+ key: "0"
324
+ } : void 0]), 1032, [
325
+ "active",
326
+ "title",
327
+ "subtitle"
328
+ ]);
329
+ }), 128))]),
330
+ _: 1
331
+ })) : (openBlock(), createBlock(unref(VCardText), { key: 1 }, {
332
+ default: withCtx(() => [..._cache[3] || (_cache[3] = [createTextVNode("Nenhuma notificação", -1)])]),
333
+ _: 1
334
+ }))]),
155
335
  _: 1
156
- }))]),
157
- _: 1
158
- }, 8, ["data-testid"])]),
159
- _: 1
160
- }, 8, ["modelValue"])) : createCommentVNode("v-if", true),
161
- showUser.value ? (openBlock(), createBlock(unref(VMenu), {
162
- key: 2,
163
- "close-on-content-click": false
164
- }, {
165
- activator: withCtx(({ props }) => [createVNode(unref(VBtn), mergeProps(props, {
166
- "data-testid": userBtnDataTestid.value,
167
- icon: ""
168
- }), {
169
- default: withCtx(() => [createVNode(unref(VAvatar), mergeProps(userAvatarProps.value, { color: "primary" }), null, 16)]),
336
+ }, 8, ["data-testid"])]),
170
337
  _: 1
171
- }, 16, ["data-testid"])]),
172
- default: withCtx(() => [createVNode(unref(VCard), {
173
- title: __props.user?.title,
174
- subtitle: __props.user?.subtitle,
175
- "data-testid": userMenuDataTestid.value
338
+ }, 8, ["modelValue"])) : createCommentVNode("v-if", true),
339
+ showUser.value ? (openBlock(), createBlock(unref(VMenu), {
340
+ key: 2,
341
+ "close-on-content-click": false
176
342
  }, {
177
- prepend: withCtx(() => [createVNode(unref(VAvatar), mergeProps(userAvatarProps.value, { color: "primary" }), null, 16)]),
178
- default: withCtx(() => [createVNode(unref(VDivider)), createVNode(unref(VList), { nav: "" }, {
179
- default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(props.user?.options, (option, index) => {
180
- return openBlock(), createElementBlock(Fragment, { key: index }, [index > 0 ? (openBlock(), createBlock(unref(VDivider), {
181
- key: 0,
182
- class: "mt-1"
183
- })) : createCommentVNode("v-if", true), isParentOption(option) ? (openBlock(), createBlock(unref(VListGroup), { key: 1 }, {
184
- activator: withCtx(({ props }) => [createVNode(unref(VListItem), mergeProps({ ref_for: true }, props, { title: option.description }), null, 16, ["title"])]),
185
- default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(option.items, (childOption, index) => {
186
- return openBlock(), createBlock(unref(VListItem), mergeProps({ key: index }, { ref_for: true }, unref(routeToProps)(childOption.route), {
187
- title: childOption.description,
188
- onClick: childOption.action
189
- }), null, 16, ["title", "onClick"]);
190
- }), 128))]),
191
- _: 2
192
- }, 1024)) : (openBlock(), createBlock(unref(VListItem), mergeProps({
193
- key: 2,
194
- title: option.description
195
- }, { ref_for: true }, unref(routeToProps)(option.route), { onClick: option.action }), null, 16, ["title", "onClick"]))], 64);
196
- }), 128))]),
343
+ activator: withCtx(({ props }) => [createVNode(unref(VBtn), mergeProps(props, {
344
+ "data-testid": userBtnDataTestid.value,
345
+ "aria-label": "Menu do usuário",
346
+ icon: ""
347
+ }), {
348
+ default: withCtx(() => [createVNode(unref(VAvatar), mergeProps(userAvatarProps.value, { color: "primary" }), null, 16)]),
197
349
  _: 1
198
- })]),
350
+ }, 16, ["data-testid"])]),
351
+ default: withCtx(() => [createVNode(unref(VCard), {
352
+ title: __props.user?.title,
353
+ subtitle: __props.user?.subtitle,
354
+ "data-testid": userMenuDataTestid.value
355
+ }, {
356
+ prepend: withCtx(() => [createVNode(unref(VAvatar), mergeProps(userAvatarProps.value, { color: "primary" }), null, 16)]),
357
+ default: withCtx(() => [createVNode(unref(VDivider)), createVNode(unref(VList), { nav: "" }, {
358
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(props.user?.options, (option, index) => {
359
+ return openBlock(), createElementBlock(Fragment, { key: index }, [index > 0 ? (openBlock(), createBlock(unref(VDivider), {
360
+ key: 0,
361
+ class: "mt-1"
362
+ })) : createCommentVNode("v-if", true), isParentOption(option) ? (openBlock(), createBlock(unref(VListGroup), { key: 1 }, {
363
+ activator: withCtx(({ props }) => [createVNode(unref(VListItem), mergeProps({ ref_for: true }, props, { title: option.description }), null, 16, ["title"])]),
364
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(option.items, (childOption, index) => {
365
+ return openBlock(), createBlock(unref(VListItem), mergeProps({ key: index }, { ref_for: true }, unref(routeToProps)(childOption.route), {
366
+ title: childOption.description,
367
+ onClick: childOption.action
368
+ }), null, 16, ["title", "onClick"]);
369
+ }), 128))]),
370
+ _: 2
371
+ }, 1024)) : (openBlock(), createBlock(unref(VListItem), mergeProps({
372
+ key: 2,
373
+ title: option.description
374
+ }, { ref_for: true }, unref(routeToProps)(option.route), { onClick: option.action }), null, 16, ["title", "onClick"]))], 64);
375
+ }), 128))]),
376
+ _: 1
377
+ })]),
378
+ _: 1
379
+ }, 8, [
380
+ "title",
381
+ "subtitle",
382
+ "data-testid"
383
+ ])]),
199
384
  _: 1
200
- }, 8, [
201
- "title",
202
- "subtitle",
203
- "data-testid"
204
- ])]),
205
- _: 1
206
- })) : createCommentVNode("v-if", true)
207
- ]),
385
+ })) : createCommentVNode("v-if", true)
386
+ ]),
387
+ _: 1
388
+ }, 8, ["loading"])]),
208
389
  default: withCtx(() => [
209
390
  __props.navigation ? (openBlock(), createBlock(unref(VAppBarNavIcon), {
210
391
  key: 0,
211
392
  "data-testid": navigationBtnDataTestid.value,
393
+ "aria-expanded": navigationOpen.value,
394
+ "aria-label": "Menu de navegação",
395
+ "aria-keyshortcuts": unref(navigationKeyshortcuts),
212
396
  onClick: _cache[0] || (_cache[0] = ($event) => navigationOpen.value = !navigationOpen.value)
213
- }, null, 8, ["data-testid"])) : createCommentVNode("v-if", true),
397
+ }, null, 8, [
398
+ "data-testid",
399
+ "aria-expanded",
400
+ "aria-keyshortcuts"
401
+ ])) : createCommentVNode("v-if", true),
214
402
  __props.logo ? (openBlock(), createBlock(unref(VImg), {
215
403
  key: 1,
216
404
  src: props.logo,
405
+ alt: "",
217
406
  height: "100%",
218
407
  width: "fit-content",
219
408
  class: "py-2 ml-5 mr-n3",
220
409
  cover: ""
221
410
  }, null, 8, ["src"])) : createCommentVNode("v-if", true),
222
- props.title ? (openBlock(), createBlock(unref(VAppBarTitle), { key: 2 }, {
411
+ props.title ? (openBlock(), createBlock(unref(VAppBarTitle), {
412
+ key: 2,
413
+ role: "heading",
414
+ "aria-level": "1"
415
+ }, {
223
416
  default: withCtx(() => [createTextVNode(toDisplayString(props.title), 1)]),
224
417
  _: 1
225
418
  })) : createCommentVNode("v-if", true)
226
419
  ]),
227
420
  _: 1
228
- }, 8, ["data-testid"]);
421
+ }, 8, ["data-testid", "aria-busy"]);
229
422
  };
230
423
  }
231
424
  });
@@ -244,7 +437,23 @@ function url(value) {
244
437
  return isEmpty(value) || isUrl(value) || "URL inválida";
245
438
  }
246
439
  //#endregion
247
- //#region src/composables/inputs/use-rules.ts
440
+ //#region src/composables/inputs/fields.ts
441
+ const textFieldVariantToVuetifyVariant = {
442
+ primary: "underlined",
443
+ secondary: "outlined"
444
+ };
445
+ function useTextFieldVariant(variant) {
446
+ return computed(() => {
447
+ const variantValue = toValue(variant);
448
+ return textFieldVariantToVuetifyVariant[variantValue];
449
+ });
450
+ }
451
+ function createRequiredRule(required, isPresent) {
452
+ return (value) => {
453
+ if (!required) return true;
454
+ return isPresent(value) || "Campo obrigatório";
455
+ };
456
+ }
248
457
  function useRules(options) {
249
458
  return computed(() => {
250
459
  const rules = [];
@@ -254,10 +463,6 @@ function useRules(options) {
254
463
  }
255
464
  //#endregion
256
465
  //#region src/composables/inputs/text-field.ts
257
- const textFieldVariantToVuetifyVariant = {
258
- primary: "underlined",
259
- secondary: "outlined"
260
- };
261
466
  const textFieldTypeToVuetifyType = {
262
467
  text: "text",
263
468
  phone: "tel",
@@ -271,18 +476,39 @@ const textFieldTypeToVuetifyRule = {
271
476
  email,
272
477
  url
273
478
  };
274
- function useTextFieldVariant(variant) {
275
- return computed(() => {
276
- const variantValue = toValue(variant);
277
- return textFieldVariantToVuetifyVariant[variantValue];
278
- });
279
- }
280
- function useTextFieldType(type) {
479
+ /**
480
+ * Maps the field type to the input type, showing as plain text a password
481
+ * the user chose to make visible.
482
+ */
483
+ function useTextFieldType(type, isPasswordVisible) {
281
484
  return computed(() => {
282
485
  const typeValue = toValue(type);
486
+ if (typeValue === "password" && toValue(isPasswordVisible)) return "text";
283
487
  return textFieldTypeToVuetifyType[typeValue];
284
488
  });
285
489
  }
490
+ /**
491
+ * Controls the button that shows or hides a password field's value. The
492
+ * password starts hidden and hides again whenever the type stops being
493
+ * `password`.
494
+ */
495
+ function useTextFieldPasswordToggle(type) {
496
+ const isPasswordField = computed(() => toValue(type) === "password");
497
+ const isPasswordVisible = ref(false);
498
+ const icon = computed(() => isPasswordVisible.value ? "mdi-eye-off" : "mdi-eye");
499
+ watch(isPasswordField, () => {
500
+ isPasswordVisible.value = false;
501
+ });
502
+ function toggle() {
503
+ isPasswordVisible.value = !isPasswordVisible.value;
504
+ }
505
+ return {
506
+ isPasswordField,
507
+ isPasswordVisible: readonly(isPasswordVisible),
508
+ icon,
509
+ toggle
510
+ };
511
+ }
286
512
  function useTextFieldRules(props) {
287
513
  const defaultRules = useRules(props);
288
514
  return computed(() => {
@@ -296,6 +522,34 @@ function associatedRuleType(type) {
296
522
  return type in textFieldTypeToVuetifyRule;
297
523
  }
298
524
  //#endregion
525
+ //#region src/components/inputs/field-loader.vue
526
+ var field_loader_default = /* @__PURE__ */ defineComponent({
527
+ inheritAttrs: false,
528
+ __name: "field-loader",
529
+ props: {
530
+ color: {
531
+ type: String,
532
+ required: false
533
+ },
534
+ isActive: {
535
+ type: Boolean,
536
+ required: true
537
+ }
538
+ },
539
+ setup(__props) {
540
+ const props = __props;
541
+ return (_ctx, _cache) => {
542
+ return openBlock(), createBlock(unref(VProgressLinear), {
543
+ active: props.isActive,
544
+ color: props.color,
545
+ height: "2",
546
+ indeterminate: "",
547
+ "aria-hidden": "true"
548
+ }, null, 8, ["active", "color"]);
549
+ };
550
+ }
551
+ });
552
+ //#endregion
299
553
  //#region src/components/inputs/text-field/text-field.vue
300
554
  var text_field_default = /* @__PURE__ */ defineComponent({
301
555
  inheritAttrs: false,
@@ -360,7 +614,8 @@ var text_field_default = /* @__PURE__ */ defineComponent({
360
614
  const modelValue = useModel(__props, "modelValue");
361
615
  const props = __props;
362
616
  const vuetifyVariant = useTextFieldVariant(() => props.variant);
363
- const vuetifyType = useTextFieldType(() => props.type);
617
+ const { isPasswordField, isPasswordVisible, icon: passwordToggleIcon, toggle: togglePassword } = useTextFieldPasswordToggle(() => props.type);
618
+ const vuetifyType = useTextFieldType(() => props.type, isPasswordVisible);
364
619
  const rules = useTextFieldRules(props);
365
620
  const isSearchField = computed(() => props.type === "search");
366
621
  const computedClearable = computed(() => props.clearable ?? isSearchField.value);
@@ -379,13 +634,34 @@ var text_field_default = /* @__PURE__ */ defineComponent({
379
634
  "data-testid": props.dataTestid,
380
635
  disabled: props.disabled,
381
636
  loading: props.loading,
637
+ "aria-busy": props.loading || void 0,
382
638
  readonly: props.readonly,
383
639
  label: props.label,
384
640
  placeholder: props.placeholder,
385
641
  hint: props.hint,
386
642
  clearable: computedClearable.value,
387
643
  "onClick:clear": clear
388
- }, null, 8, [
644
+ }, createSlots({
645
+ loader: withCtx((loader) => [createVNode(field_loader_default, normalizeProps(guardReactiveProps(loader)), null, 16)]),
646
+ _: 2
647
+ }, [unref(isPasswordField) ? {
648
+ name: "append-inner",
649
+ fn: withCtx(() => [createVNode(unref(VBtn), {
650
+ icon: unref(passwordToggleIcon),
651
+ "aria-label": "Mostrar senha",
652
+ "aria-pressed": unref(isPasswordVisible),
653
+ disabled: props.disabled,
654
+ size: "small",
655
+ variant: "text",
656
+ onClick: unref(togglePassword)
657
+ }, null, 8, [
658
+ "icon",
659
+ "aria-pressed",
660
+ "disabled",
661
+ "onClick"
662
+ ])]),
663
+ key: "0"
664
+ } : void 0]), 1032, [
389
665
  "modelValue",
390
666
  "variant",
391
667
  "type",
@@ -394,6 +670,7 @@ var text_field_default = /* @__PURE__ */ defineComponent({
394
670
  "data-testid",
395
671
  "disabled",
396
672
  "loading",
673
+ "aria-busy",
397
674
  "readonly",
398
675
  "label",
399
676
  "placeholder",
@@ -405,7 +682,7 @@ var text_field_default = /* @__PURE__ */ defineComponent({
405
682
  });
406
683
  //#endregion
407
684
  //#region src/components/navigation-menu/navigation-menu.vue?vue&type=script&setup=true&lang.ts
408
- const _hoisted_1$1 = { class: "px-4 pt-2" };
685
+ const _hoisted_1$2 = { class: "px-4 pt-2" };
409
686
  //#endregion
410
687
  //#region src/components/navigation-menu/navigation-menu.vue
411
688
  var navigation_menu_default = /* @__PURE__ */ defineComponent({
@@ -419,6 +696,10 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
419
696
  items: {
420
697
  type: Array,
421
698
  required: false
699
+ },
700
+ loading: {
701
+ type: Boolean,
702
+ required: false
422
703
  }
423
704
  }, {
424
705
  "modelValue": {
@@ -432,6 +713,10 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
432
713
  const props = __props;
433
714
  const open = useModel(__props, "modelValue");
434
715
  const search = ref("");
716
+ const { label: shortcutLabel, close } = useNavigationSearchShortcut({
717
+ open,
718
+ field: useTemplateRef("searchField")
719
+ });
435
720
  const filteredItems = computed(() => filterItems());
436
721
  const searchDataTestid = computed(() => `${props.dataTestid}-search`);
437
722
  const query = computed(() => search.value.toLowerCase());
@@ -458,37 +743,88 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
458
743
  return openBlock(), createBlock(unref(VNavigationDrawer), {
459
744
  modelValue: open.value,
460
745
  "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => open.value = $event),
746
+ color: "primary",
461
747
  "data-testid": props.dataTestid,
748
+ "aria-busy": props.loading || void 0,
462
749
  absolute: "",
463
- temporary: ""
750
+ temporary: "",
751
+ onKeydown: withKeys(unref(close), ["esc"])
464
752
  }, {
465
- prepend: withCtx(() => [createElementVNode("div", _hoisted_1$1, [createVNode(text_field_default, {
753
+ prepend: withCtx(() => [createElementVNode("div", _hoisted_1$2, [createVNode(text_field_default, {
754
+ ref: "searchField",
466
755
  modelValue: search.value,
467
756
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event),
468
757
  type: "search",
469
- label: "Buscar",
470
- "data-testid": searchDataTestid.value
471
- }, null, 8, ["modelValue", "data-testid"])])]),
472
- default: withCtx(() => [createVNode(unref(VDivider)), createVNode(unref(VList), { nav: "" }, {
473
- default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(filteredItems.value, (item, index) => {
474
- return openBlock(), createElementBlock(Fragment, { key: index }, [isParentItem(item) ? (openBlock(), createBlock(unref(VListGroup), { key: 0 }, {
475
- activator: withCtx(({ props: activatorProps }) => [createVNode(unref(VListItem), mergeProps({ ref_for: true }, activatorProps, { title: item.description }), null, 16, ["title"])]),
476
- default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(item.items, (childItem, childIndex) => {
477
- return openBlock(), createBlock(unref(VListItem), mergeProps({ key: childIndex }, { ref_for: true }, unref(routeToProps)(childItem.route), {
478
- title: childItem.description,
479
- onClick: childItem.action
480
- }), null, 16, ["title", "onClick"]);
481
- }), 128))]),
482
- _: 2
483
- }, 1024)) : (openBlock(), createBlock(unref(VListItem), mergeProps({
484
- key: 1,
485
- title: item.description
486
- }, { ref_for: true }, unref(routeToProps)(item.route), { onClick: item.action }), null, 16, ["title", "onClick"]))], 64);
487
- }), 128))]),
758
+ placeholder: `Buscar (${unref(shortcutLabel)})`,
759
+ "data-testid": searchDataTestid.value,
760
+ disabled: props.loading
761
+ }, null, 8, [
762
+ "modelValue",
763
+ "placeholder",
764
+ "data-testid",
765
+ "disabled"
766
+ ])])]),
767
+ default: withCtx(() => [createVNode(unref(VDivider)), createVNode(skeleton_loader_default, {
768
+ loading: props.loading,
769
+ type: "list-item@6",
770
+ color: "primary"
771
+ }, {
772
+ default: withCtx(() => [createVNode(unref(VList), { nav: "" }, {
773
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(filteredItems.value, (item, index) => {
774
+ return openBlock(), createElementBlock(Fragment, { key: index }, [isParentItem(item) ? (openBlock(), createBlock(unref(VListGroup), { key: 0 }, {
775
+ activator: withCtx(({ props: activatorProps }) => [createVNode(unref(VListItem), mergeProps({ ref_for: true }, activatorProps, { title: item.description }), null, 16, ["title"])]),
776
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(item.items, (childItem, childIndex) => {
777
+ return openBlock(), createBlock(unref(VListItem), mergeProps({ key: childIndex }, { ref_for: true }, unref(routeToProps)(childItem.route), {
778
+ title: childItem.description,
779
+ onClick: childItem.action
780
+ }), null, 16, ["title", "onClick"]);
781
+ }), 128))]),
782
+ _: 2
783
+ }, 1024)) : (openBlock(), createBlock(unref(VListItem), mergeProps({
784
+ key: 1,
785
+ title: item.description
786
+ }, { ref_for: true }, unref(routeToProps)(item.route), { onClick: item.action }), null, 16, ["title", "onClick"]))], 64);
787
+ }), 128))]),
788
+ _: 1
789
+ })]),
488
790
  _: 1
489
- })]),
791
+ }, 8, ["loading"])]),
792
+ _: 1
793
+ }, 8, [
794
+ "modelValue",
795
+ "data-testid",
796
+ "aria-busy",
797
+ "onKeydown"
798
+ ]);
799
+ };
800
+ }
801
+ });
802
+ //#endregion
803
+ //#region src/components/footer/footer.vue
804
+ var footer_default = /* @__PURE__ */ defineComponent({
805
+ inheritAttrs: false,
806
+ __name: "footer",
807
+ props: {
808
+ description: {
809
+ type: String,
810
+ required: false
811
+ },
812
+ dataTestid: {
813
+ type: String,
814
+ required: false
815
+ }
816
+ },
817
+ setup(__props) {
818
+ const props = __props;
819
+ return (_ctx, _cache) => {
820
+ return openBlock(), createBlock(unref(VFooter), {
821
+ "data-testid": props.dataTestid,
822
+ class: "justify-center text-center",
823
+ app: ""
824
+ }, {
825
+ default: withCtx(() => [createTextVNode(toDisplayString(props.description), 1)]),
490
826
  _: 1
491
- }, 8, ["modelValue", "data-testid"]);
827
+ }, 8, ["data-testid"]);
492
828
  };
493
829
  }
494
830
  });
@@ -497,33 +833,67 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
497
833
  var container_default = /* @__PURE__ */ defineComponent({
498
834
  inheritAttrs: false,
499
835
  __name: "container",
500
- props: { dataTestid: {
501
- type: String,
502
- required: false
503
- } },
836
+ props: {
837
+ fullHeight: {
838
+ type: Boolean,
839
+ required: false
840
+ },
841
+ dataTestid: {
842
+ type: String,
843
+ required: false
844
+ }
845
+ },
504
846
  setup(__props) {
505
847
  const props = __props;
848
+ const heightClass = useFullHeight(() => props.fullHeight);
506
849
  return (_ctx, _cache) => {
507
850
  return openBlock(), createBlock(unref(VContainer), {
851
+ class: normalizeClass(unref(heightClass)),
508
852
  "data-testid": props.dataTestid,
509
853
  fluid: ""
510
854
  }, {
511
855
  default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
512
856
  _: 3
513
- }, 8, ["data-testid"]);
857
+ }, 8, ["class", "data-testid"]);
514
858
  };
515
859
  }
516
860
  });
517
861
  //#endregion
862
+ //#region src/composables/dialogs/use-decorative-timer.ts
863
+ /**
864
+ * Content props that hide a snackbar's timer bar from assistive technologies,
865
+ * keeping it only as a visual countdown, since the message itself is already
866
+ * announced by the snackbar's live region.
867
+ *
868
+ * Workaround until Vuetify offers an option for it: the timer bar is a
869
+ * `VProgressLinear` that always renders `role="progressbar"` with
870
+ * `aria-hidden="false"` and no accessible name, and neither `VSnackbar` nor
871
+ * Vuetify's defaults can set attributes on it. So these vnode hooks mark the
872
+ * bar after every render of the snackbar content, which also covers the bar
873
+ * recreated when the pointer leaves the snackbar. Vue doesn't patch the
874
+ * attribute back, since Vuetify keeps rendering the same `"false"` value.
875
+ */
876
+ function useDecorativeTimer() {
877
+ return {
878
+ onVnodeMounted: hideTimer,
879
+ onVnodeUpdated: hideTimer
880
+ };
881
+ }
882
+ function hideTimer({ el }) {
883
+ el?.querySelector(".v-snackbar__timer [role='progressbar']")?.setAttribute("aria-hidden", "true");
884
+ }
885
+ //#endregion
518
886
  //#region src/components/dialogs/toast.vue
519
887
  var toast_default = /* @__PURE__ */ defineComponent({
520
888
  inheritAttrs: false,
521
889
  __name: "toast",
522
890
  setup(__props) {
891
+ const decorativeTimer = useDecorativeTimer();
523
892
  return (_ctx, _cache) => {
524
893
  return openBlock(), createBlock(unref(VSnackbarQueue), {
525
894
  modelValue: unref(messages),
526
895
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => isRef(messages) ? messages.value = $event : null),
896
+ "content-props": unref(decorativeTimer),
527
897
  "display-strategy": "overflow",
528
898
  location: "top end",
529
899
  "total-visible": "3",
@@ -538,72 +908,7 @@ var toast_default = /* @__PURE__ */ defineComponent({
538
908
  variant: "text"
539
909
  }), null, 16)]),
540
910
  _: 1
541
- }, 8, ["modelValue"]);
542
- };
543
- }
544
- });
545
- //#endregion
546
- //#region src/components/main/main.vue
547
- var main_default = /* @__PURE__ */ defineComponent({
548
- inheritAttrs: false,
549
- __name: "main",
550
- props: /*@__PURE__*/ mergeModels({
551
- dataTestid: {
552
- type: String,
553
- required: false
554
- },
555
- logo: {
556
- type: String,
557
- required: false
558
- },
559
- appBar: {
560
- type: Object,
561
- required: false
562
- },
563
- navigationMenu: {
564
- type: Object,
565
- required: false
566
- }
567
- }, {
568
- "notificationsOpen": {
569
- type: Boolean,
570
- default: false
571
- },
572
- "notificationsOpenModifiers": {}
573
- }),
574
- emits: ["update:notificationsOpen"],
575
- setup(__props) {
576
- const props = __props;
577
- const appBarProps = computed(() => props.appBar ? {
578
- ...props.appBar,
579
- logo: props.logo,
580
- navigation: props.navigationMenu !== void 0
581
- } : void 0);
582
- const notificationsOpen = useModel(__props, "notificationsOpen");
583
- const navigationMenuOpen = ref(false);
584
- return (_ctx, _cache) => {
585
- return openBlock(), createBlock(unref(VApp), { "data-testid": props.dataTestid }, {
586
- default: withCtx(() => [
587
- appBarProps.value ? (openBlock(), createBlock(app_bar_default, mergeProps({ key: 0 }, appBarProps.value, {
588
- "notifications-open": notificationsOpen.value,
589
- "onUpdate:notificationsOpen": _cache[0] || (_cache[0] = ($event) => notificationsOpen.value = $event),
590
- "navigation-open": navigationMenuOpen.value,
591
- "onUpdate:navigationOpen": _cache[1] || (_cache[1] = ($event) => navigationMenuOpen.value = $event)
592
- }), null, 16, ["notifications-open", "navigation-open"])) : createCommentVNode("v-if", true),
593
- props.navigationMenu ? (openBlock(), createBlock(navigation_menu_default, mergeProps({ key: 1 }, props.navigationMenu, {
594
- modelValue: navigationMenuOpen.value,
595
- "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => navigationMenuOpen.value = $event)
596
- }), null, 16, ["modelValue"])) : createCommentVNode("v-if", true),
597
- createVNode(unref(VMain), null, {
598
- default: withCtx(() => [createVNode(container_default, null, {
599
- default: withCtx(() => [renderSlot(_ctx.$slots, "default"), createVNode(toast_default)]),
600
- _: 3
601
- })]),
602
- _: 3
603
- })
604
- ]),
605
- _: 3
606
- }, 8, ["data-testid"]);
911
+ }, 8, ["modelValue", "content-props"]);
607
912
  };
608
913
  }
609
914
  });
@@ -612,7 +917,7 @@ var main_default = /* @__PURE__ */ defineComponent({
612
917
  const buttonVariantToVuetifyVariant = {
613
918
  primary: "elevated",
614
919
  secondary: "flat",
615
- ghost: "outlined"
920
+ ghost: "text"
616
921
  };
617
922
  function useButtonVariant(variant) {
618
923
  return computed(() => {
@@ -654,6 +959,10 @@ var button_default = /* @__PURE__ */ defineComponent({
654
959
  type: Boolean,
655
960
  required: false
656
961
  },
962
+ fullWidth: {
963
+ type: Boolean,
964
+ required: false
965
+ },
657
966
  form: {
658
967
  type: String,
659
968
  required: false
@@ -671,20 +980,28 @@ var button_default = /* @__PURE__ */ defineComponent({
671
980
  const color = useVuetifyColor(() => props.color);
672
981
  const type = useButtonType(() => props.type);
673
982
  const form = useButtonForm(() => props.form);
983
+ const widthClass = useFullWidth(() => props.fullWidth);
984
+ const isDisabled = computed(() => props.disabled || props.loading);
674
985
  function onClick(event) {
675
986
  emit("click", event);
676
987
  }
677
988
  return (_ctx, _cache) => {
678
989
  return openBlock(), createBlock(unref(VBtn), {
679
- disabled: props.disabled,
990
+ disabled: isDisabled.value,
680
991
  loading: props.loading,
681
992
  "data-testid": props.dataTestid,
682
993
  variant: unref(variant),
683
994
  color: unref(color),
684
995
  type: unref(type),
685
996
  form: unref(form),
997
+ class: normalizeClass(unref(widthClass)),
686
998
  onClick
687
999
  }, {
1000
+ loader: withCtx(() => [createVNode(unref(VProgressCircular), {
1001
+ indeterminate: "",
1002
+ width: "2",
1003
+ "aria-hidden": "true"
1004
+ })]),
688
1005
  default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
689
1006
  _: 3
690
1007
  }, 8, [
@@ -694,16 +1011,542 @@ var button_default = /* @__PURE__ */ defineComponent({
694
1011
  "variant",
695
1012
  "color",
696
1013
  "type",
697
- "form"
1014
+ "form",
1015
+ "class"
698
1016
  ]);
699
1017
  };
700
1018
  }
701
1019
  });
702
1020
  //#endregion
703
- //#region src/components/inputs/checkbox/checkbox.vue
704
- var checkbox_default = /* @__PURE__ */ defineComponent({
705
- inheritAttrs: false,
706
- __name: "checkbox",
1021
+ //#region src/components/dialogs/dialog/dialog-host-key.ts
1022
+ /**
1023
+ * Injection key of {@link DialogHostContext}, provided by the internal `DialogHost`. Besides the
1024
+ * role, the blocking while an action runs and the end of the leave transitions, it makes the
1025
+ * dialog's content describe the dialog (`aria-describedby`), since `DialogHost` only displays text
1026
+ * messages. Not part of the public API.
1027
+ */
1028
+ const dialogHostKey = Symbol("dialog-host");
1029
+ //#endregion
1030
+ //#region src/composables/dialogs/use-dialog-focus.ts
1031
+ /** Focus state of the closed dialogs, by their card, for the dialogs opened from them. */
1032
+ const closedDialogs = /* @__PURE__ */ new WeakMap();
1033
+ /** Elements that can get the focus when the dialog opens. The first one in the dialog gets it. */
1034
+ const focusable = [
1035
+ "button:not([disabled])",
1036
+ "[href]",
1037
+ "input:not([disabled]):not([type=\"hidden\"])",
1038
+ "select:not([disabled])",
1039
+ "textarea:not([disabled])",
1040
+ "[tabindex]:not([tabindex=\"-1\"])"
1041
+ ].join(", ");
1042
+ /**
1043
+ * Moves the focus into a dialog once it opens and back to where it was when it closes, including
1044
+ * when dialogs are opened from others. Internal to the dialog components.
1045
+ * @param isActive - Whether the dialog is displayed. It turns `false` as soon as the dialog starts
1046
+ * leaving, so the focus returns right away.
1047
+ * @param card - The dialog's card, marked with `data-u-dialog`, or nothing while it isn't rendered.
1048
+ * @returns `focusFirstElement`, to call once the dialog finishes entering.
1049
+ */
1050
+ function useDialogFocus(isActive, card) {
1051
+ /** Focus state of this dialog, shared with the dialogs opened from it. */
1052
+ const focusState = {
1053
+ isOpen: isActive,
1054
+ origin: null
1055
+ };
1056
+ onMounted(() => onActiveChange(isActive()));
1057
+ watch(isActive, onActiveChange);
1058
+ function onActiveChange(active) {
1059
+ if (active) {
1060
+ focusState.origin = document.activeElement;
1061
+ return;
1062
+ }
1063
+ const element = card();
1064
+ if (element) closedDialogs.set(element, focusState);
1065
+ const target = findFocusTarget(focusState.origin);
1066
+ if (hasFocus(element) && (target instanceof HTMLElement || target instanceof SVGElement)) target.focus();
1067
+ }
1068
+ function focusFirstElement() {
1069
+ card()?.querySelector(focusable)?.focus();
1070
+ }
1071
+ return { focusFirstElement };
1072
+ }
1073
+ /**
1074
+ * Element that gets the focus back when a dialog closes: its origin or, when the origin is inside
1075
+ * another dialog that closed meanwhile (and can't get the focus anymore), that dialog's.
1076
+ */
1077
+ function findFocusTarget(origin) {
1078
+ const card = origin?.closest("[data-u-dialog]");
1079
+ const owner = card && closedDialogs.get(card);
1080
+ return owner && !owner.isOpen() ? findFocusTarget(owner.origin) : origin;
1081
+ }
1082
+ /**
1083
+ * Whether the focus is on the dialog or lost (on nothing or on the page's body), so it can move.
1084
+ * Otherwise it moved elsewhere, e.g. to another dialog opened over this one.
1085
+ */
1086
+ function hasFocus(card) {
1087
+ const active = document.activeElement;
1088
+ if (!active) return true;
1089
+ return !!card && (card.contains(active) || active.contains(card));
1090
+ }
1091
+ //#endregion
1092
+ //#region src/components/dialogs/dialog/dialog.vue?vue&type=script&setup=true&lang.ts
1093
+ const _hoisted_1$1 = ["id"];
1094
+ //#endregion
1095
+ //#region src/components/dialogs/dialog/dialog.vue
1096
+ var dialog_default = /* @__PURE__ */ defineComponent({
1097
+ inheritAttrs: false,
1098
+ __name: "dialog",
1099
+ props: /*@__PURE__*/ mergeModels({
1100
+ title: {
1101
+ type: String,
1102
+ required: false
1103
+ },
1104
+ actions: {
1105
+ type: Array,
1106
+ required: false
1107
+ },
1108
+ size: {
1109
+ type: String,
1110
+ required: false
1111
+ },
1112
+ dataTestid: {
1113
+ type: String,
1114
+ required: false
1115
+ }
1116
+ }, {
1117
+ "modelValue": {
1118
+ type: Boolean,
1119
+ default: false
1120
+ },
1121
+ "modelModifiers": {}
1122
+ }),
1123
+ emits: ["update:modelValue"],
1124
+ setup(__props) {
1125
+ const props = __props;
1126
+ const model = useModel(__props, "modelValue");
1127
+ const maxWidths = {
1128
+ small: 400,
1129
+ medium: 560,
1130
+ large: 800
1131
+ };
1132
+ const host = inject(dialogHostKey, null);
1133
+ const contentId = useId();
1134
+ const card = useTemplateRef("card");
1135
+ const title = computed(() => props.title || void 0);
1136
+ const maxWidth = computed(() => maxWidths[props.size ?? "medium"]);
1137
+ const role = computed(() => host?.role() ?? "dialog");
1138
+ /**
1139
+ * Whether the dialog is displayed. It follows `v-model`, but turns `false` as soon as the user
1140
+ * closes the dialog, while `v-model` only does once the leave transition ends.
1141
+ */
1142
+ const isActive = ref(model.value);
1143
+ watch(model, (value) => isActive.value = value);
1144
+ const { focusFirstElement } = useDialogFocus(() => isActive.value, () => card.value?.$el);
1145
+ function onAfterLeave() {
1146
+ model.value = false;
1147
+ host?.afterLeave();
1148
+ }
1149
+ return (_ctx, _cache) => {
1150
+ return openBlock(), createBlock(unref(VDialog), {
1151
+ modelValue: isActive.value,
1152
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => isActive.value = $event),
1153
+ persistent: unref(host)?.persistent(),
1154
+ "max-width": maxWidth.value,
1155
+ role: role.value,
1156
+ "aria-label": title.value,
1157
+ "aria-labelledby": title.value ? void 0 : unref(contentId),
1158
+ "aria-describedby": unref(host) ? unref(contentId) : void 0,
1159
+ "data-testid": props.dataTestid,
1160
+ scrollable: "",
1161
+ onAfterEnter: unref(focusFirstElement),
1162
+ onAfterLeave
1163
+ }, {
1164
+ default: withCtx(() => [createCommentVNode(" `data-u-dialog` identifies the card to the dialogs opened from this one "), createVNode(unref(VCard), {
1165
+ ref_key: "card",
1166
+ ref: card,
1167
+ title: title.value,
1168
+ "data-u-dialog": ""
1169
+ }, createSlots({
1170
+ text: withCtx(() => [createElementVNode("div", { id: unref(contentId) }, [renderSlot(_ctx.$slots, "default")], 8, _hoisted_1$1)]),
1171
+ _: 2
1172
+ }, [props.actions?.length ? {
1173
+ name: "actions",
1174
+ fn: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(props.actions, ({ label, onClick, ...action }, index) => {
1175
+ return openBlock(), createBlock(button_default, mergeProps({ key: index }, { ref_for: true }, action, { onClick }), {
1176
+ default: withCtx(() => [createTextVNode(toDisplayString(label), 1)]),
1177
+ _: 2
1178
+ }, 1040, ["onClick"]);
1179
+ }), 128))]),
1180
+ key: "0"
1181
+ } : void 0]), 1032, ["title"])]),
1182
+ _: 3
1183
+ }, 8, [
1184
+ "modelValue",
1185
+ "persistent",
1186
+ "max-width",
1187
+ "role",
1188
+ "aria-label",
1189
+ "aria-labelledby",
1190
+ "aria-describedby",
1191
+ "data-testid",
1192
+ "onAfterEnter"
1193
+ ]);
1194
+ };
1195
+ }
1196
+ });
1197
+ //#endregion
1198
+ //#region src/components/dialogs/dialog-host.vue
1199
+ var dialog_host_default = /* @__PURE__ */ defineComponent({
1200
+ inheritAttrs: false,
1201
+ __name: "dialog-host",
1202
+ setup(__props) {
1203
+ const owner = Symbol("dialog");
1204
+ const isOwner = computed(() => owners$1.value[0] === owner);
1205
+ const next = computed(() => isOwner.value ? requests.value[0] : void 0);
1206
+ /** Dialog on display, kept until its leave transition ends. */
1207
+ const request = shallowRef();
1208
+ /**
1209
+ * Changes for every dialog on display, so each one gets its own `Dialog`, open from the start.
1210
+ * The next dialog can then open right after a dismissed one, whose `v-model` stays `true`.
1211
+ */
1212
+ const key = shallowRef();
1213
+ const isOpen = ref(false);
1214
+ const selected = shallowRef();
1215
+ const isRunning = computed(() => selected.value !== void 0);
1216
+ const actions = computed(() => {
1217
+ const current = request.value;
1218
+ if (!current) return void 0;
1219
+ return current.actions.map((action) => ({
1220
+ label: action.text,
1221
+ variant: action.variant,
1222
+ color: action.color,
1223
+ loading: selected.value === action,
1224
+ disabled: isRunning.value && selected.value !== action,
1225
+ onClick: () => void select(current, action)
1226
+ }));
1227
+ });
1228
+ provide(dialogHostKey, {
1229
+ role: () => request.value?.role,
1230
+ persistent: () => isRunning.value,
1231
+ afterLeave: showNext
1232
+ });
1233
+ onMounted(() => owners$1.value.push(owner));
1234
+ onUnmounted(() => owners$1.value = owners$1.value.filter((item) => item !== owner));
1235
+ watch(next, show);
1236
+ function show() {
1237
+ if (request.value) return;
1238
+ request.value = next.value;
1239
+ key.value = Symbol();
1240
+ isOpen.value = next.value !== void 0;
1241
+ }
1242
+ function showNext() {
1243
+ request.value = void 0;
1244
+ show();
1245
+ }
1246
+ async function select(current, action) {
1247
+ requests.value.shift();
1248
+ selected.value = action;
1249
+ await current.select(action?.value);
1250
+ selected.value = void 0;
1251
+ if (action) isOpen.value = false;
1252
+ }
1253
+ /** Called once a dialog closed by the user leaves, right before the next one is displayed. */
1254
+ function dismiss() {
1255
+ if (request.value) select(request.value);
1256
+ }
1257
+ return (_ctx, _cache) => {
1258
+ return openBlock(), createElementBlock(Fragment, null, [createCommentVNode(" PascalCase, since `<dialog>` in the template would render the native HTML element instead "), (openBlock(), createBlock(dialog_default, {
1259
+ key: key.value,
1260
+ "model-value": isOpen.value,
1261
+ title: request.value?.title,
1262
+ actions: actions.value,
1263
+ "onUpdate:modelValue": dismiss
1264
+ }, {
1265
+ default: withCtx(() => [createTextVNode(toDisplayString(request.value?.message), 1)]),
1266
+ _: 1
1267
+ }, 8, [
1268
+ "model-value",
1269
+ "title",
1270
+ "actions"
1271
+ ]))], 2112);
1272
+ };
1273
+ }
1274
+ });
1275
+ //#endregion
1276
+ //#region src/components/main/main.vue
1277
+ var main_default = /* @__PURE__ */ defineComponent({
1278
+ inheritAttrs: false,
1279
+ __name: "main",
1280
+ props: /*@__PURE__*/ mergeModels({
1281
+ dataTestid: {
1282
+ type: String,
1283
+ required: false
1284
+ },
1285
+ logo: {
1286
+ type: String,
1287
+ required: false
1288
+ },
1289
+ appBar: {
1290
+ type: Object,
1291
+ required: false
1292
+ },
1293
+ navigationMenu: {
1294
+ type: Object,
1295
+ required: false
1296
+ },
1297
+ footer: {
1298
+ type: Object,
1299
+ required: false
1300
+ },
1301
+ loading: {
1302
+ type: Boolean,
1303
+ required: false
1304
+ }
1305
+ }, {
1306
+ "notificationsOpen": {
1307
+ type: Boolean,
1308
+ default: false
1309
+ },
1310
+ "notificationsOpenModifiers": {}
1311
+ }),
1312
+ emits: ["update:notificationsOpen"],
1313
+ setup(__props) {
1314
+ const props = __props;
1315
+ const appBarProps = computed(() => props.appBar ? {
1316
+ ...props.appBar,
1317
+ logo: props.logo,
1318
+ navigation: props.navigationMenu !== void 0,
1319
+ loading: props.loading
1320
+ } : void 0);
1321
+ const notificationsOpen = useModel(__props, "notificationsOpen");
1322
+ const navigationMenuOpen = ref(false);
1323
+ return (_ctx, _cache) => {
1324
+ return openBlock(), createBlock(unref(VApp), { "data-testid": props.dataTestid }, {
1325
+ default: withCtx(() => [
1326
+ appBarProps.value ? (openBlock(), createBlock(app_bar_default, mergeProps({ key: 0 }, appBarProps.value, {
1327
+ "notifications-open": notificationsOpen.value,
1328
+ "onUpdate:notificationsOpen": _cache[0] || (_cache[0] = ($event) => notificationsOpen.value = $event),
1329
+ "navigation-open": navigationMenuOpen.value,
1330
+ "onUpdate:navigationOpen": _cache[1] || (_cache[1] = ($event) => navigationMenuOpen.value = $event)
1331
+ }), null, 16, ["notifications-open", "navigation-open"])) : createCommentVNode("v-if", true),
1332
+ props.navigationMenu ? (openBlock(), createBlock(navigation_menu_default, mergeProps({ key: 1 }, props.navigationMenu, {
1333
+ modelValue: navigationMenuOpen.value,
1334
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => navigationMenuOpen.value = $event),
1335
+ loading: props.loading
1336
+ }), null, 16, ["modelValue", "loading"])) : createCommentVNode("v-if", true),
1337
+ createVNode(unref(VMain), null, {
1338
+ default: withCtx(() => [createCommentVNode(" Full height, so the page's rows and containers with `fullHeight` have a height to fill "), createVNode(container_default, { "full-height": "" }, {
1339
+ default: withCtx(() => [
1340
+ renderSlot(_ctx.$slots, "default"),
1341
+ createVNode(toast_default),
1342
+ createVNode(dialog_host_default)
1343
+ ]),
1344
+ _: 3
1345
+ })]),
1346
+ _: 3
1347
+ }),
1348
+ props.footer?.description ? (openBlock(), createBlock(footer_default, normalizeProps(mergeProps({ key: 2 }, props.footer)), null, 16)) : createCommentVNode("v-if", true)
1349
+ ]),
1350
+ _: 3
1351
+ }, 8, ["data-testid"]);
1352
+ };
1353
+ }
1354
+ });
1355
+ //#endregion
1356
+ //#region src/composables/inputs/autocomplete-field.ts
1357
+ function createAutocompleteRequiredRule(options) {
1358
+ return createRequiredRule(options.required, (value) => {
1359
+ if (options.multiple) return (Array.isArray(value) ? value : []).length > 0;
1360
+ return !isEmpty(String(value ?? ""));
1361
+ });
1362
+ }
1363
+ function useAutocompleteRules(options) {
1364
+ return computed(() => {
1365
+ const opts = toValue(options);
1366
+ const rules = [];
1367
+ if (opts.required) rules.push(createAutocompleteRequiredRule(opts));
1368
+ return rules;
1369
+ });
1370
+ }
1371
+ /**
1372
+ * Menu props that keep the combobox semantics only on the field's `<input>`.
1373
+ *
1374
+ * The field's container gets `role="combobox"` from the field, which passes its
1375
+ * role to both the container and the input, and the menu's ARIA attributes,
1376
+ * since it's the menu's activator. Without these props, the container is
1377
+ * exposed as an unnamed combobox wrapping the input. The menu removes from its
1378
+ * activator the attributes set to `undefined`, role included, and the field
1379
+ * never sets that role again, since its value doesn't change. The input keeps
1380
+ * its role, label, `aria-expanded` and `aria-controls`. This only happens in
1381
+ * the browser: server-rendered HTML still has the role on the container.
1382
+ */
1383
+ const autocompleteFieldMenuProps = { activatorProps: {
1384
+ role: void 0,
1385
+ "aria-haspopup": void 0,
1386
+ "aria-expanded": void 0,
1387
+ "aria-controls": void 0,
1388
+ "aria-owns": void 0
1389
+ } };
1390
+ //#endregion
1391
+ //#region src/components/inputs/autocomplete-field/autocomplete-field.vue
1392
+ var autocomplete_field_default = /* @__PURE__ */ defineComponent({
1393
+ inheritAttrs: false,
1394
+ __name: "autocomplete-field",
1395
+ props: /*@__PURE__*/ mergeModels({
1396
+ variant: {
1397
+ type: String,
1398
+ required: false,
1399
+ default: "primary"
1400
+ },
1401
+ disabled: {
1402
+ type: Boolean,
1403
+ required: false
1404
+ },
1405
+ loading: {
1406
+ type: Boolean,
1407
+ required: false
1408
+ },
1409
+ clearable: {
1410
+ type: Boolean,
1411
+ required: false
1412
+ },
1413
+ readonly: {
1414
+ type: Boolean,
1415
+ required: false
1416
+ },
1417
+ required: {
1418
+ type: Boolean,
1419
+ required: false
1420
+ },
1421
+ label: {
1422
+ type: String,
1423
+ required: false
1424
+ },
1425
+ placeholder: {
1426
+ type: String,
1427
+ required: false
1428
+ },
1429
+ hint: {
1430
+ type: String,
1431
+ required: false
1432
+ },
1433
+ dataTestid: {
1434
+ type: String,
1435
+ required: false
1436
+ },
1437
+ strict: {
1438
+ type: Boolean,
1439
+ required: false
1440
+ },
1441
+ multiple: {
1442
+ type: null,
1443
+ required: false
1444
+ },
1445
+ items: {
1446
+ type: Array,
1447
+ required: false,
1448
+ default: () => []
1449
+ }
1450
+ }, {
1451
+ "modelValue": { type: null },
1452
+ "modelModifiers": {}
1453
+ }),
1454
+ emits: ["update:modelValue"],
1455
+ setup(__props) {
1456
+ const modelValue = useModel(__props, "modelValue");
1457
+ const props = __props;
1458
+ const vuetifyVariant = useTextFieldVariant(() => props.variant);
1459
+ const isMultiple = computed(() => props.multiple ?? false);
1460
+ const rules = useAutocompleteRules(() => ({
1461
+ required: props.required,
1462
+ multiple: isMultiple.value
1463
+ }));
1464
+ const vuetifyComponent = computed(() => props.strict ? VAutocomplete : VCombobox);
1465
+ const search = ref("");
1466
+ const maskClass = computed(() => props.strict ? "v-autocomplete__mask" : "v-combobox__mask");
1467
+ const normalizedItems = computed(() => {
1468
+ return props.items.map((item) => {
1469
+ if (typeof item === "string") return {
1470
+ label: item,
1471
+ value: item
1472
+ };
1473
+ return item;
1474
+ });
1475
+ });
1476
+ return (_ctx, _cache) => {
1477
+ return openBlock(), createBlock(resolveDynamicComponent(vuetifyComponent.value), {
1478
+ modelValue: modelValue.value,
1479
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => modelValue.value = $event),
1480
+ search: search.value,
1481
+ "onUpdate:search": _cache[1] || (_cache[1] = ($event) => search.value = $event),
1482
+ variant: unref(vuetifyVariant),
1483
+ items: normalizedItems.value,
1484
+ "data-testid": props.dataTestid,
1485
+ disabled: props.disabled,
1486
+ loading: props.loading,
1487
+ "aria-busy": props.loading || void 0,
1488
+ readonly: props.readonly,
1489
+ label: props.label,
1490
+ placeholder: props.placeholder,
1491
+ hint: props.hint,
1492
+ clearable: props.clearable,
1493
+ multiple: isMultiple.value,
1494
+ "item-title": "label",
1495
+ rules: unref(rules),
1496
+ "menu-props": unref(autocompleteFieldMenuProps),
1497
+ chips: ""
1498
+ }, createSlots({
1499
+ loader: withCtx((loader) => [createVNode(field_loader_default, normalizeProps(guardReactiveProps(loader)), null, 16)]),
1500
+ _: 2
1501
+ }, [isMultiple.value ? {
1502
+ name: "item",
1503
+ fn: withCtx(({ props: itemProps }) => [createVNode(unref(VListItem), normalizeProps(guardReactiveProps(itemProps)), {
1504
+ prepend: withCtx(({ isSelected }) => [createVNode(unref(VCheckboxBtn), {
1505
+ "model-value": isSelected,
1506
+ ripple: false,
1507
+ inert: "",
1508
+ "aria-hidden": "true"
1509
+ }, null, 8, ["model-value"])]),
1510
+ title: withCtx(() => [createVNode(unref(VHighlight), {
1511
+ text: itemProps.title,
1512
+ query: search.value,
1513
+ "match-all": "",
1514
+ "ignore-case": "",
1515
+ "mark-class": maskClass.value
1516
+ }, null, 8, [
1517
+ "text",
1518
+ "query",
1519
+ "mark-class"
1520
+ ])]),
1521
+ _: 2
1522
+ }, 1040)]),
1523
+ key: "0"
1524
+ } : void 0]), 1064, [
1525
+ "modelValue",
1526
+ "search",
1527
+ "variant",
1528
+ "items",
1529
+ "data-testid",
1530
+ "disabled",
1531
+ "loading",
1532
+ "aria-busy",
1533
+ "readonly",
1534
+ "label",
1535
+ "placeholder",
1536
+ "hint",
1537
+ "clearable",
1538
+ "multiple",
1539
+ "rules",
1540
+ "menu-props"
1541
+ ]);
1542
+ };
1543
+ }
1544
+ });
1545
+ //#endregion
1546
+ //#region src/components/inputs/checkbox/checkbox.vue
1547
+ var checkbox_default = /* @__PURE__ */ defineComponent({
1548
+ inheritAttrs: false,
1549
+ __name: "checkbox",
707
1550
  props: /*@__PURE__*/ mergeModels({
708
1551
  label: {
709
1552
  type: String,
@@ -763,27 +1606,630 @@ var checkbox_default = /* @__PURE__ */ defineComponent({
763
1606
  "data-testid",
764
1607
  "disabled",
765
1608
  "readonly",
766
- "label"
1609
+ "label"
1610
+ ]);
1611
+ };
1612
+ }
1613
+ });
1614
+ //#endregion
1615
+ //#region src/composables/inputs/date-time-field.ts
1616
+ /**
1617
+ * The menu's content is a dialog with the pickers. `aria-owns` is removed since the input can't
1618
+ * own the content, and the menu still adds `aria-expanded` and `aria-controls`.
1619
+ */
1620
+ const activatorProps = {
1621
+ role: "combobox",
1622
+ "aria-haspopup": "dialog",
1623
+ "aria-owns": void 0
1624
+ };
1625
+ /**
1626
+ * Extracts the date part (`YYYY-MM-DD`) of a value, or an empty string when
1627
+ * the type has no date.
1628
+ */
1629
+ function getDatePart(value, type) {
1630
+ return type === "time" ? "" : value.split("T")[0];
1631
+ }
1632
+ /**
1633
+ * Extracts the time part (`HH:mm`) of a value, or an empty string when the
1634
+ * value has no time.
1635
+ */
1636
+ function getTimePart(value, type) {
1637
+ if (type === "time") return value;
1638
+ const [, time = ""] = value.split("T");
1639
+ return time;
1640
+ }
1641
+ /**
1642
+ * Builds a value for the given type, or an empty string while any required
1643
+ * part is missing.
1644
+ */
1645
+ function joinDateTime(type, date, time) {
1646
+ if (type === "date") return date;
1647
+ if (type === "time") return time;
1648
+ return date && time ? `${date}T${time}` : "";
1649
+ }
1650
+ /**
1651
+ * Formats a value to be displayed in pt-BR, like `24/09/2026`, `14:30` or
1652
+ * `24/09/2026 14:30`.
1653
+ */
1654
+ function formatDateTime$1(value, type) {
1655
+ return [getDatePart(value, type).split("-").reverse().join("/"), getTimePart(value, type)].filter(Boolean).join(" ");
1656
+ }
1657
+ function useDateTimeFieldRules(props) {
1658
+ const defaultRules = useRules(props);
1659
+ return computed(() => {
1660
+ const rules = [...defaultRules.value];
1661
+ if (props.min) rules.push(minRule(props.min, props.type));
1662
+ if (props.max) rules.push(maxRule(props.max, props.type));
1663
+ return rules;
1664
+ });
1665
+ }
1666
+ /**
1667
+ * Computes the limits of each picker. For `datetime`, time limits only apply
1668
+ * when the selected date is the limit's own date.
1669
+ */
1670
+ function useDateTimeFieldPickerBounds(props, selectedDate) {
1671
+ return computed(() => {
1672
+ const min = props.min ?? "";
1673
+ const max = props.max ?? "";
1674
+ const date = toValue(selectedDate);
1675
+ return {
1676
+ dateMin: getDatePart(min, props.type),
1677
+ dateMax: getDatePart(max, props.type),
1678
+ timeMin: getTimeBound(min, props.type, date),
1679
+ timeMax: getTimeBound(max, props.type, date)
1680
+ };
1681
+ });
1682
+ }
1683
+ /**
1684
+ * Makes the field's input the activator of the pickers' menu, so the menu's attributes are on an
1685
+ * element whose role accepts them.
1686
+ * @param isOpen - Whether the menu is open.
1687
+ * @param isDisabled - Whether the menu can't open, as when the field is disabled or readonly.
1688
+ * @param hasLabel - Whether the field has a label, which names the menu's dialog.
1689
+ */
1690
+ function useDateTimeFieldMenu(isOpen, isDisabled, hasLabel) {
1691
+ const inputId = useId();
1692
+ const activator = computed(() => toValue(isDisabled) ? void 0 : `#${inputId}`);
1693
+ const contentProps = computed(() => ({
1694
+ role: "dialog",
1695
+ "aria-labelledby": toValue(hasLabel) ? `${inputId}-label` : void 0
1696
+ }));
1697
+ function open() {
1698
+ if (!toValue(isDisabled)) isOpen.value = true;
1699
+ }
1700
+ return {
1701
+ inputId,
1702
+ activator,
1703
+ activatorProps,
1704
+ contentProps,
1705
+ open
1706
+ };
1707
+ }
1708
+ function getTimeBound(bound, type, selectedDate) {
1709
+ if (type === "datetime" && getDatePart(bound, type) !== selectedDate) return "";
1710
+ return getTimePart(bound, type);
1711
+ }
1712
+ function minRule(min, type) {
1713
+ const message = `Valor anterior ao mínimo permitido (${formatDateTime$1(min, type)})`;
1714
+ return (value) => !value || value >= min || message;
1715
+ }
1716
+ function maxRule(max, type) {
1717
+ const message = `Valor posterior ao máximo permitido (${formatDateTime$1(max, type)})`;
1718
+ return (value) => !value || value <= max || message;
1719
+ }
1720
+ //#endregion
1721
+ //#region src/components/inputs/date-time-field/date-time-field.vue
1722
+ var date_time_field_default = /* @__PURE__ */ defineComponent({
1723
+ inheritAttrs: false,
1724
+ __name: "date-time-field",
1725
+ props: /*@__PURE__*/ mergeModels({
1726
+ variant: {
1727
+ type: String,
1728
+ required: false,
1729
+ default: "primary"
1730
+ },
1731
+ type: {
1732
+ type: String,
1733
+ required: false,
1734
+ default: "datetime"
1735
+ },
1736
+ min: {
1737
+ type: String,
1738
+ required: false
1739
+ },
1740
+ max: {
1741
+ type: String,
1742
+ required: false
1743
+ },
1744
+ disabled: {
1745
+ type: Boolean,
1746
+ required: false
1747
+ },
1748
+ loading: {
1749
+ type: Boolean,
1750
+ required: false
1751
+ },
1752
+ clearable: {
1753
+ type: Boolean,
1754
+ required: false
1755
+ },
1756
+ readonly: {
1757
+ type: Boolean,
1758
+ required: false
1759
+ },
1760
+ required: {
1761
+ type: Boolean,
1762
+ required: false
1763
+ },
1764
+ label: {
1765
+ type: String,
1766
+ required: false
1767
+ },
1768
+ placeholder: {
1769
+ type: String,
1770
+ required: false
1771
+ },
1772
+ hint: {
1773
+ type: String,
1774
+ required: false
1775
+ },
1776
+ dataTestid: {
1777
+ type: String,
1778
+ required: false
1779
+ }
1780
+ }, {
1781
+ "modelValue": {
1782
+ type: String,
1783
+ default: ""
1784
+ },
1785
+ "modelModifiers": {}
1786
+ }),
1787
+ emits: ["update:modelValue"],
1788
+ setup(__props) {
1789
+ const modelValue = useModel(__props, "modelValue");
1790
+ const props = __props;
1791
+ const messages = { "pt-BR": pt };
1792
+ const vuetifyVariant = useTextFieldVariant(() => props.variant);
1793
+ const rules = useDateTimeFieldRules(props);
1794
+ const displayValue = computed(() => formatDateTime$1(modelValue.value, props.type));
1795
+ const isClearable = computed(() => props.clearable && !props.readonly);
1796
+ const isMenuDisabled = computed(() => props.disabled || props.readonly);
1797
+ const isMenuOpen = ref(false);
1798
+ const draft = ref({});
1799
+ const draftDate = computed(() => draft.value.date ?? getDatePart(modelValue.value, props.type));
1800
+ const draftTime = computed(() => draft.value.time ?? getTimePart(modelValue.value, props.type));
1801
+ const draftValue = computed(() => joinDateTime(props.type, draftDate.value, draftTime.value));
1802
+ const bounds = useDateTimeFieldPickerBounds(props, draftDate);
1803
+ const { inputId, activator, activatorProps, contentProps, open: openMenu } = useDateTimeFieldMenu(isMenuOpen, isMenuDisabled, () => !!props.label);
1804
+ watch(isMenuOpen, () => {
1805
+ draft.value = {};
1806
+ });
1807
+ function onDateUpdate(date) {
1808
+ draft.value.date = formatDateTime(date, "YYYY-MM-DD");
1809
+ commitDraft();
1810
+ closeIfComplete();
1811
+ }
1812
+ function onTimeUpdate(time) {
1813
+ draft.value.time = time ?? "";
1814
+ commitDraft();
1815
+ }
1816
+ function commitDraft() {
1817
+ if (draftValue.value) modelValue.value = draftValue.value;
1818
+ }
1819
+ function closeIfComplete() {
1820
+ if (draftValue.value) isMenuOpen.value = false;
1821
+ }
1822
+ function clear() {
1823
+ modelValue.value = "";
1824
+ }
1825
+ return (_ctx, _cache) => {
1826
+ return openBlock(), createBlock(unref(VTextField), {
1827
+ id: unref(inputId),
1828
+ "model-value": displayValue.value,
1829
+ "validation-value": modelValue.value,
1830
+ variant: unref(vuetifyVariant),
1831
+ rules: unref(rules),
1832
+ "data-testid": props.dataTestid,
1833
+ disabled: props.disabled,
1834
+ loading: props.loading,
1835
+ "aria-busy": props.loading || void 0,
1836
+ label: props.label,
1837
+ placeholder: props.placeholder,
1838
+ hint: props.hint,
1839
+ clearable: isClearable.value,
1840
+ readonly: "",
1841
+ "onClick:clear": clear,
1842
+ "onClick:control": unref(openMenu)
1843
+ }, {
1844
+ loader: withCtx((loader) => [createVNode(field_loader_default, normalizeProps(guardReactiveProps(loader)), null, 16)]),
1845
+ default: withCtx(() => [createVNode(unref(VMenu), {
1846
+ modelValue: isMenuOpen.value,
1847
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => isMenuOpen.value = $event),
1848
+ activator: unref(activator),
1849
+ target: "parent",
1850
+ "activator-props": unref(activatorProps),
1851
+ "content-props": unref(contentProps),
1852
+ location: "bottom start",
1853
+ "min-width": "0",
1854
+ disabled: isMenuDisabled.value,
1855
+ "close-on-content-click": false
1856
+ }, {
1857
+ default: withCtx(() => [createVNode(unref(VCard), null, {
1858
+ default: withCtx(() => [createVNode(unref(VLocaleProvider), {
1859
+ locale: "pt-BR",
1860
+ messages,
1861
+ class: "d-flex flex-wrap"
1862
+ }, {
1863
+ default: withCtx(() => [props.type !== "time" ? (openBlock(), createBlock(unref(VDatePicker), {
1864
+ key: 0,
1865
+ "model-value": draftDate.value || null,
1866
+ min: unref(bounds).dateMin,
1867
+ max: unref(bounds).dateMax,
1868
+ "onUpdate:modelValue": onDateUpdate
1869
+ }, null, 8, [
1870
+ "model-value",
1871
+ "min",
1872
+ "max"
1873
+ ])) : createCommentVNode("v-if", true), props.type !== "date" ? (openBlock(), createBlock(unref(VTimePicker), {
1874
+ key: 1,
1875
+ "model-value": draftTime.value,
1876
+ min: unref(bounds).timeMin,
1877
+ max: unref(bounds).timeMax,
1878
+ format: "24hr",
1879
+ "onUpdate:modelValue": onTimeUpdate,
1880
+ "onUpdate:minute": closeIfComplete
1881
+ }, null, 8, [
1882
+ "model-value",
1883
+ "min",
1884
+ "max"
1885
+ ])) : createCommentVNode("v-if", true)]),
1886
+ _: 1
1887
+ })]),
1888
+ _: 1
1889
+ })]),
1890
+ _: 1
1891
+ }, 8, [
1892
+ "modelValue",
1893
+ "activator",
1894
+ "activator-props",
1895
+ "content-props",
1896
+ "disabled"
1897
+ ])]),
1898
+ _: 1
1899
+ }, 8, [
1900
+ "id",
1901
+ "model-value",
1902
+ "validation-value",
1903
+ "variant",
1904
+ "rules",
1905
+ "data-testid",
1906
+ "disabled",
1907
+ "loading",
1908
+ "aria-busy",
1909
+ "label",
1910
+ "placeholder",
1911
+ "hint",
1912
+ "clearable",
1913
+ "onClick:control"
1914
+ ]);
1915
+ };
1916
+ }
1917
+ });
1918
+ //#endregion
1919
+ //#region src/composables/inputs/mask-field.ts
1920
+ /**
1921
+ * Masks of the presets. Our preset names always map to these masks, never to
1922
+ * Vuetify's own presets (e.g. its American `phone`).
1923
+ */
1924
+ const maskFieldPresets = {
1925
+ cpf: "###.###.###-##",
1926
+ cnpj: "##.###.###/####-##",
1927
+ cep: "#####-###",
1928
+ phone: "(##) #####-####",
1929
+ date: "##/##/####"
1930
+ };
1931
+ /**
1932
+ * Returns the mask of a preset, or the given mask itself when it's a custom
1933
+ * one.
1934
+ *
1935
+ * It takes any string, as the `mask` prop may get one through a cast or from
1936
+ * JavaScript: a string that isn't a preset is taken as the mask.
1937
+ */
1938
+ function resolveMask(mask) {
1939
+ return isMaskFieldPreset(mask) ? maskFieldPresets[mask] : mask;
1940
+ }
1941
+ /**
1942
+ * Returns a mask as Vuetify reads it. `#` is our only token, but Vuetify always
1943
+ * adds its own tokens (`A`, `a`, `N`, `n` and `X`) and its escape (`\`), so
1944
+ * these characters are escaped to stay literals.
1945
+ */
1946
+ function toVuetifyMask(mask) {
1947
+ return mask.replaceAll(/[AaNnX\\]/g, String.raw`\$&`);
1948
+ }
1949
+ /**
1950
+ * Returns the default placeholder of a mask: the mask with `0` in place of each
1951
+ * `#`, keeping its literals (e.g. `000.000.000-00` for `cpf`).
1952
+ */
1953
+ function getMaskPlaceholder(mask) {
1954
+ return mask.replaceAll("#", "0");
1955
+ }
1956
+ function isMaskFieldPreset(mask) {
1957
+ return Object.hasOwn(maskFieldPresets, mask);
1958
+ }
1959
+ /**
1960
+ * Keeps the input masked while the user edits it, emitting only the digits
1961
+ * (Vuetify's `unmask`).
1962
+ *
1963
+ * A `v-model` out of the mask's format (masked, or with characters or length
1964
+ * the mask doesn't accept) is displayed normalized, without being emitted, and
1965
+ * validated as it is, so a value that doesn't fit the mask fails validation.
1966
+ */
1967
+ function useMaskField(props, model) {
1968
+ const mask = computed(() => resolveMask(props.mask));
1969
+ const digitCount = computed(() => mask.value.split("#").length - 1);
1970
+ const vuetifyMask = useMask({ get mask() {
1971
+ return {
1972
+ mask: toVuetifyMask(mask.value),
1973
+ tokens: {}
1974
+ };
1975
+ } });
1976
+ const displayValue = computed(() => vuetifyMask.mask(unmask(model.value)));
1977
+ const placeholder = computed(() => props.placeholder ?? getMaskPlaceholder(mask.value));
1978
+ const defaultRules = useRules(props);
1979
+ const rules = computed(() => [
1980
+ ...defaultRules.value,
1981
+ (value) => !value || !vuetifyMask.isValid(value) || isComplete(value) || "Valor incompleto",
1982
+ (value) => !value || vuetifyMask.isValid(value) || "Valor inválido"
1983
+ ]);
1984
+ function unmask(text) {
1985
+ return vuetifyMask.unmask(text) ?? "";
1986
+ }
1987
+ function isComplete(value) {
1988
+ return unmask(vuetifyMask.mask(value)).length === digitCount.value;
1989
+ }
1990
+ /**
1991
+ * Returns the position in a masked text right after the given number of
1992
+ * digits, skipping the literals after it unless the user deleted backwards
1993
+ * (as Vuetify's mask input does).
1994
+ */
1995
+ function findCaret(masked, valueLength, deletedBackwards) {
1996
+ let caret = 0;
1997
+ let found = 0;
1998
+ while (found < valueLength) {
1999
+ if (!vuetifyMask.isDelimiter(masked, caret)) found++;
2000
+ caret++;
2001
+ }
2002
+ if (!deletedBackwards) while (vuetifyMask.isDelimiter(masked, caret)) caret++;
2003
+ return caret;
2004
+ }
2005
+ function update(input, deletedBackwards) {
2006
+ const caret = input.selectionStart ?? input.value.length;
2007
+ const masked = vuetifyMask.mask(unmask(input.value));
2008
+ const newCaret = findCaret(masked, unmask(vuetifyMask.mask(unmask(input.value.slice(0, caret)))).length, deletedBackwards);
2009
+ const value = unmask(masked);
2010
+ input.value = masked;
2011
+ input.setSelectionRange(newCaret, newCaret);
2012
+ if (value !== unmask(displayValue.value)) model.value = value;
2013
+ }
2014
+ function onInput(event) {
2015
+ if (!event.isComposing) update(event.target, event.inputType.endsWith("Backward"));
2016
+ }
2017
+ function onCompositionEnd(event) {
2018
+ update(event.target, false);
2019
+ }
2020
+ return {
2021
+ displayValue,
2022
+ placeholder,
2023
+ rules,
2024
+ onInput,
2025
+ onCompositionEnd
2026
+ };
2027
+ }
2028
+ //#endregion
2029
+ //#region src/components/inputs/mask-field/mask-field.vue
2030
+ var mask_field_default = /* @__PURE__ */ defineComponent({
2031
+ inheritAttrs: false,
2032
+ __name: "mask-field",
2033
+ props: /*@__PURE__*/ mergeModels({
2034
+ variant: {
2035
+ type: String,
2036
+ required: false,
2037
+ default: "primary"
2038
+ },
2039
+ mask: {
2040
+ type: null,
2041
+ required: true
2042
+ },
2043
+ disabled: {
2044
+ type: Boolean,
2045
+ required: false
2046
+ },
2047
+ loading: {
2048
+ type: Boolean,
2049
+ required: false
2050
+ },
2051
+ clearable: {
2052
+ type: Boolean,
2053
+ required: false
2054
+ },
2055
+ readonly: {
2056
+ type: Boolean,
2057
+ required: false
2058
+ },
2059
+ required: {
2060
+ type: Boolean,
2061
+ required: false
2062
+ },
2063
+ label: {
2064
+ type: String,
2065
+ required: false
2066
+ },
2067
+ placeholder: {
2068
+ type: String,
2069
+ required: false
2070
+ },
2071
+ hint: {
2072
+ type: String,
2073
+ required: false
2074
+ },
2075
+ dataTestid: {
2076
+ type: String,
2077
+ required: false
2078
+ }
2079
+ }, {
2080
+ "modelValue": {
2081
+ type: String,
2082
+ default: ""
2083
+ },
2084
+ "modelModifiers": {}
2085
+ }),
2086
+ emits: ["update:modelValue"],
2087
+ setup(__props) {
2088
+ const modelValue = useModel(__props, "modelValue");
2089
+ const props = __props;
2090
+ const vuetifyVariant = useTextFieldVariant(() => props.variant);
2091
+ const { displayValue, placeholder, rules, onInput, onCompositionEnd } = useMaskField(props, modelValue);
2092
+ function keepFieldControlled() {}
2093
+ function clear() {
2094
+ modelValue.value = "";
2095
+ }
2096
+ return (_ctx, _cache) => {
2097
+ return openBlock(), createBlock(unref(VTextField), {
2098
+ "model-value": unref(displayValue),
2099
+ "validation-value": modelValue.value,
2100
+ variant: unref(vuetifyVariant),
2101
+ rules: unref(rules),
2102
+ "data-testid": props.dataTestid,
2103
+ disabled: props.disabled,
2104
+ loading: props.loading,
2105
+ "aria-busy": props.loading || void 0,
2106
+ readonly: props.readonly,
2107
+ label: props.label,
2108
+ placeholder: unref(placeholder),
2109
+ hint: props.hint,
2110
+ clearable: props.clearable,
2111
+ type: "text",
2112
+ inputmode: "numeric",
2113
+ "onUpdate:modelValue": keepFieldControlled,
2114
+ onInput: unref(onInput),
2115
+ onCompositionend: unref(onCompositionEnd),
2116
+ "onClick:clear": clear
2117
+ }, {
2118
+ loader: withCtx((loader) => [createVNode(field_loader_default, normalizeProps(guardReactiveProps(loader)), null, 16)]),
2119
+ _: 1
2120
+ }, 8, [
2121
+ "model-value",
2122
+ "validation-value",
2123
+ "variant",
2124
+ "rules",
2125
+ "data-testid",
2126
+ "disabled",
2127
+ "loading",
2128
+ "aria-busy",
2129
+ "readonly",
2130
+ "label",
2131
+ "placeholder",
2132
+ "hint",
2133
+ "clearable",
2134
+ "onInput",
2135
+ "onCompositionend"
767
2136
  ]);
768
2137
  };
769
2138
  }
770
2139
  });
771
2140
  //#endregion
2141
+ //#region src/composables/alignment/alignment.ts
2142
+ const alignXToClass = {
2143
+ start: "justify-start",
2144
+ center: "justify-center",
2145
+ end: "justify-end"
2146
+ };
2147
+ const alignYToClasses = {
2148
+ start: {
2149
+ items: "align-start",
2150
+ content: "align-content-start"
2151
+ },
2152
+ center: {
2153
+ items: "align-center",
2154
+ content: "align-content-center"
2155
+ },
2156
+ end: {
2157
+ items: "align-end",
2158
+ content: "align-content-end"
2159
+ },
2160
+ stretch: {
2161
+ items: "align-stretch",
2162
+ content: "align-content-stretch"
2163
+ }
2164
+ };
2165
+ /**
2166
+ * Vuetify's `justify-*` utility class for components with an `alignX` prop, laid out with flexbox.
2167
+ * Falls back to `"start"`.
2168
+ */
2169
+ function useAlignX(alignX) {
2170
+ return computed(() => alignXToClass[toValue(alignX) ?? "start"]);
2171
+ }
2172
+ /**
2173
+ * Vuetify's `align-*` and `align-content-*` utility classes for components with an `alignY` prop,
2174
+ * laid out with flexbox. Falls back to `"start"`.
2175
+ */
2176
+ function useAlignY(alignY) {
2177
+ return computed(() => alignYToClasses[toValue(alignY) ?? "start"]);
2178
+ }
2179
+ //#endregion
772
2180
  //#region src/components/grid/row/row.vue
773
2181
  var row_default = /* @__PURE__ */ defineComponent({
774
2182
  inheritAttrs: false,
775
2183
  __name: "row",
776
- props: { dataTestid: {
777
- type: String,
778
- required: false
779
- } },
2184
+ props: {
2185
+ alignX: {
2186
+ type: String,
2187
+ required: false
2188
+ },
2189
+ alignY: {
2190
+ type: String,
2191
+ required: false
2192
+ },
2193
+ fullHeight: {
2194
+ type: Boolean,
2195
+ required: false
2196
+ },
2197
+ list: {
2198
+ type: Boolean,
2199
+ required: false
2200
+ },
2201
+ dataTestid: {
2202
+ type: String,
2203
+ required: false
2204
+ }
2205
+ },
780
2206
  setup(__props) {
781
2207
  const props = __props;
2208
+ const alignXClass = useAlignX(() => props.alignX);
2209
+ const alignYClasses = useAlignY(() => props.alignY);
2210
+ const heightClass = useFullHeight(() => props.fullHeight);
2211
+ const alignContentClass = computed(() => props.fullHeight ? alignYClasses.value.content : void 0);
2212
+ const tag = computed(() => props.list ? "ul" : void 0);
2213
+ const listClasses = computed(() => props.list ? "pa-0 ma-0" : void 0);
782
2214
  return (_ctx, _cache) => {
783
- return openBlock(), createBlock(unref(VRow), { "data-testid": props.dataTestid }, {
2215
+ return openBlock(), createBlock(unref(VRow), {
2216
+ tag: tag.value,
2217
+ class: normalizeClass([
2218
+ unref(alignXClass),
2219
+ unref(alignYClasses).items,
2220
+ unref(heightClass),
2221
+ alignContentClass.value,
2222
+ listClasses.value
2223
+ ]),
2224
+ "data-testid": props.dataTestid
2225
+ }, {
784
2226
  default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
785
2227
  _: 3
786
- }, 8, ["data-testid"]);
2228
+ }, 8, [
2229
+ "tag",
2230
+ "class",
2231
+ "data-testid"
2232
+ ]);
787
2233
  };
788
2234
  }
789
2235
  });
@@ -798,6 +2244,10 @@ var column_default = /* @__PURE__ */ defineComponent({
798
2244
  required: false,
799
2245
  default: "12"
800
2246
  },
2247
+ listItem: {
2248
+ type: Boolean,
2249
+ required: false
2250
+ },
801
2251
  dataTestid: {
802
2252
  type: String,
803
2253
  required: false
@@ -805,15 +2255,24 @@ var column_default = /* @__PURE__ */ defineComponent({
805
2255
  },
806
2256
  setup(__props) {
807
2257
  const props = __props;
2258
+ const tag = computed(() => props.listItem ? "li" : void 0);
2259
+ const listItemClasses = computed(() => props.listItem ? "d-block" : void 0);
808
2260
  return (_ctx, _cache) => {
809
2261
  return openBlock(), createBlock(unref(VCol), {
810
2262
  cols: "12",
811
2263
  sm: props.cols,
2264
+ tag: tag.value,
2265
+ class: normalizeClass(listItemClasses.value),
812
2266
  "data-testid": props.dataTestid
813
2267
  }, {
814
2268
  default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
815
2269
  _: 3
816
- }, 8, ["sm", "data-testid"]);
2270
+ }, 8, [
2271
+ "sm",
2272
+ "tag",
2273
+ "class",
2274
+ "data-testid"
2275
+ ]);
817
2276
  };
818
2277
  }
819
2278
  });
@@ -878,6 +2337,26 @@ function useSectionVariant(variant) {
878
2337
  return variantValue ? sectionVariantToVuetifyVariant[variantValue] : sectionVariantToVuetifyVariant.primary;
879
2338
  });
880
2339
  }
2340
+ const sectionTextAlignToClasses = {
2341
+ start: {
2342
+ content: "text-start",
2343
+ actions: "justify-end"
2344
+ },
2345
+ center: {
2346
+ content: "text-center",
2347
+ actions: "justify-center"
2348
+ },
2349
+ end: {
2350
+ content: "text-end",
2351
+ actions: "justify-end"
2352
+ }
2353
+ };
2354
+ function useSectionTextAlign(textAlign) {
2355
+ return computed(() => {
2356
+ const textAlignValue = toValue(textAlign);
2357
+ return textAlignValue ? sectionTextAlignToClasses[textAlignValue] : sectionTextAlignToClasses.start;
2358
+ });
2359
+ }
881
2360
  //#endregion
882
2361
  //#region src/components/sections/section/section.vue
883
2362
  var section_default = /* @__PURE__ */ defineComponent({
@@ -896,6 +2375,10 @@ var section_default = /* @__PURE__ */ defineComponent({
896
2375
  type: String,
897
2376
  required: false
898
2377
  },
2378
+ textAlign: {
2379
+ type: String,
2380
+ required: false
2381
+ },
899
2382
  actions: {
900
2383
  type: Array,
901
2384
  required: false
@@ -920,31 +2403,35 @@ var section_default = /* @__PURE__ */ defineComponent({
920
2403
  setup(__props) {
921
2404
  const props = __props;
922
2405
  const variant = useSectionVariant(() => props.variant);
2406
+ const textAlignClasses = useSectionTextAlign(() => props.textAlign);
923
2407
  const title = computed(() => props.title || void 0);
924
2408
  const subtitle = computed(() => props.subtitle || void 0);
925
- const width = computed(() => props.fullWidth ? "100%" : void 0);
926
- const height = computed(() => props.fullHeight ? "100%" : void 0);
2409
+ const widthClass = useFullWidth(() => props.fullWidth);
2410
+ const heightClass = useFullHeight(() => props.fullHeight);
927
2411
  const showActions = computed(() => !!props.actions?.length);
928
2412
  const skeletonType = computed(() => showActions.value ? "image, actions" : "image");
929
2413
  return (_ctx, _cache) => {
930
- return openBlock(), createBlock(unref(VSkeletonLoader), {
2414
+ return openBlock(), createBlock(skeleton_loader_default, {
931
2415
  loading: props.loading,
932
2416
  type: skeletonType.value,
933
- width: width.value,
934
- height: height.value
2417
+ "full-width": props.fullWidth,
2418
+ "full-height": props.fullHeight
935
2419
  }, {
936
- default: withCtx(() => [createVNode(unref(VCard), {
2420
+ default: withCtx(() => [createCommentVNode(" The card and its actions have no alignment props, so `textAlign` uses utility classes "), createVNode(unref(VCard), {
937
2421
  title: title.value,
938
2422
  subtitle: subtitle.value,
939
2423
  variant: unref(variant),
940
- width: width.value,
941
- height: height.value,
942
2424
  "data-testid": props.dataTestid,
943
- class: "d-flex flex-column"
2425
+ class: normalizeClass([
2426
+ "d-flex flex-column",
2427
+ unref(widthClass),
2428
+ unref(heightClass),
2429
+ unref(textAlignClasses).content
2430
+ ])
944
2431
  }, {
945
2432
  default: withCtx(() => [renderSlot(_ctx.$slots, "default"), showActions.value ? (openBlock(), createBlock(unref(VCardActions), {
946
2433
  key: 0,
947
- class: "mt-auto"
2434
+ class: normalizeClass(["mt-auto", unref(textAlignClasses).actions])
948
2435
  }, {
949
2436
  default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(props.actions, ({ label, onClick, ...action }, index) => {
950
2437
  return openBlock(), createBlock(button_default, mergeProps({ key: index }, { ref_for: true }, action, { onClick }), {
@@ -953,22 +2440,21 @@ var section_default = /* @__PURE__ */ defineComponent({
953
2440
  }, 1040, ["onClick"]);
954
2441
  }), 128))]),
955
2442
  _: 1
956
- })) : createCommentVNode("v-if", true)]),
2443
+ }, 8, ["class"])) : createCommentVNode("v-if", true)]),
957
2444
  _: 3
958
2445
  }, 8, [
959
2446
  "title",
960
2447
  "subtitle",
961
2448
  "variant",
962
- "width",
963
- "height",
964
- "data-testid"
2449
+ "data-testid",
2450
+ "class"
965
2451
  ])]),
966
2452
  _: 3
967
2453
  }, 8, [
968
2454
  "loading",
969
2455
  "type",
970
- "width",
971
- "height"
2456
+ "full-width",
2457
+ "full-height"
972
2458
  ]);
973
2459
  };
974
2460
  }
@@ -1018,7 +2504,7 @@ var table_default = /* @__PURE__ */ defineComponent({
1018
2504
  return `${props.dataTestid}-${index}`;
1019
2505
  }
1020
2506
  return (_ctx, _cache) => {
1021
- return openBlock(), createBlock(unref(VSkeletonLoader), {
2507
+ return openBlock(), createBlock(skeleton_loader_default, {
1022
2508
  loading: props.loading,
1023
2509
  type: type.value
1024
2510
  }, {
@@ -1056,4 +2542,448 @@ var table_default = /* @__PURE__ */ defineComponent({
1056
2542
  }
1057
2543
  });
1058
2544
  //#endregion
1059
- export { button_default as UButton, checkbox_default as UCheckbox, column_default as UColumn, container_default as UContainer, form_default as UForm, main_default as UMain, row_default as URow, section_default as USection, section_content_default as USectionContent, table_default as UTable, text_field_default as UTextField };
2545
+ //#region src/components/details/details.vue
2546
+ var details_default = /* @__PURE__ */ defineComponent({
2547
+ inheritAttrs: false,
2548
+ __name: "details",
2549
+ props: {
2550
+ variant: {
2551
+ type: String,
2552
+ required: false
2553
+ },
2554
+ title: {
2555
+ type: String,
2556
+ required: false
2557
+ },
2558
+ loading: {
2559
+ type: Boolean,
2560
+ required: false
2561
+ },
2562
+ dataTestid: {
2563
+ type: String,
2564
+ required: false
2565
+ }
2566
+ },
2567
+ setup(__props) {
2568
+ const props = __props;
2569
+ const outlined = computed(() => props.variant === "secondary");
2570
+ return (_ctx, _cache) => {
2571
+ return openBlock(), createBlock(skeleton_loader_default, {
2572
+ loading: props.loading,
2573
+ type: "heading",
2574
+ "full-width": ""
2575
+ }, {
2576
+ default: withCtx(() => [createVNode(unref(VExpansionPanels), {
2577
+ flat: outlined.value,
2578
+ "data-testid": props.dataTestid
2579
+ }, {
2580
+ default: withCtx(() => [createVNode(unref(VExpansionPanel), { class: normalizeClass({ border: outlined.value }) }, {
2581
+ default: withCtx(() => [createVNode(unref(VExpansionPanelTitle), null, {
2582
+ default: withCtx(() => [createTextVNode(toDisplayString(props.title), 1)]),
2583
+ _: 1
2584
+ }), createVNode(unref(VExpansionPanelText), null, {
2585
+ default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
2586
+ _: 3
2587
+ })]),
2588
+ _: 3
2589
+ }, 8, ["class"])]),
2590
+ _: 3
2591
+ }, 8, ["flat", "data-testid"])]),
2592
+ _: 3
2593
+ }, 8, ["loading"]);
2594
+ };
2595
+ }
2596
+ });
2597
+ //#endregion
2598
+ //#region src/components/img/img.vue
2599
+ var img_default = /* @__PURE__ */ defineComponent({
2600
+ inheritAttrs: false,
2601
+ __name: "img",
2602
+ props: {
2603
+ src: {
2604
+ type: String,
2605
+ required: true
2606
+ },
2607
+ width: {
2608
+ type: [Number, String],
2609
+ required: false
2610
+ },
2611
+ height: {
2612
+ type: [Number, String],
2613
+ required: false
2614
+ },
2615
+ aspectRatio: {
2616
+ type: Number,
2617
+ required: false
2618
+ },
2619
+ cover: {
2620
+ type: Boolean,
2621
+ required: false
2622
+ },
2623
+ eager: {
2624
+ type: Boolean,
2625
+ required: false
2626
+ },
2627
+ dataTestid: {
2628
+ type: String,
2629
+ required: false
2630
+ }
2631
+ },
2632
+ setup(__props) {
2633
+ const props = __props;
2634
+ return (_ctx, _cache) => {
2635
+ return openBlock(), createBlock(unref(VImg), {
2636
+ src: props.src,
2637
+ alt: "",
2638
+ width: props.width,
2639
+ height: props.height,
2640
+ "aspect-ratio": props.aspectRatio,
2641
+ cover: props.cover,
2642
+ eager: props.eager,
2643
+ "data-testid": props.dataTestid
2644
+ }, null, 8, [
2645
+ "src",
2646
+ "width",
2647
+ "height",
2648
+ "aspect-ratio",
2649
+ "cover",
2650
+ "eager",
2651
+ "data-testid"
2652
+ ]);
2653
+ };
2654
+ }
2655
+ });
2656
+ //#endregion
2657
+ //#region src/components/link/link.vue
2658
+ var link_default = /* @__PURE__ */ defineComponent({
2659
+ inheritAttrs: false,
2660
+ __name: "link",
2661
+ props: {
2662
+ route: {
2663
+ type: null,
2664
+ required: true
2665
+ },
2666
+ dataTestid: {
2667
+ type: String,
2668
+ required: false
2669
+ }
2670
+ },
2671
+ setup(__props) {
2672
+ const props = __props;
2673
+ const routeProps = useRouteProps(() => props.route);
2674
+ return (_ctx, _cache) => {
2675
+ return openBlock(), createElementBlock(Fragment, null, [createCommentVNode(" No Vuetify component renders an inline text link, so the class removes the browser's underline "), (openBlock(), createBlock(resolveDynamicComponent(unref(isExternalRoute)(props.route) ? "a" : unref(RouterLink)), mergeProps(unref(routeProps), {
2676
+ class: "text-decoration-none",
2677
+ "data-testid": props.dataTestid
2678
+ }), {
2679
+ default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
2680
+ _: 3
2681
+ }, 16, ["data-testid"]))], 2112);
2682
+ };
2683
+ }
2684
+ });
2685
+ //#endregion
2686
+ //#region src/composables/data-set/data-set.ts
2687
+ /**
2688
+ * Width, out of the grid's 12 columns, taken by each record so that
2689
+ * `columns` records fit side by side on a row.
2690
+ */
2691
+ const dataSetColumnsToColumnCols = {
2692
+ 1: "12",
2693
+ 2: "6",
2694
+ 3: "4",
2695
+ 4: "3",
2696
+ 6: "2"
2697
+ };
2698
+ /**
2699
+ * Space, in pixels, between two cards of the same row: the default gutter of
2700
+ * `URow`/`UColumn` (Vuetify's `v-row` with its default density puts a 24px
2701
+ * gap between its columns). `n` cards side by side take `n` card widths plus
2702
+ * `n - 1` gaps, so they fit in `width` pixels when
2703
+ * `n * (minDataSetItemWidth + gap) <= width + gap`.
2704
+ */
2705
+ const dataSetItemGap = 24;
2706
+ /**
2707
+ * Supported numbers of records per row, from the largest to the smallest.
2708
+ */
2709
+ const dataSetColumnsOptions = [
2710
+ 6,
2711
+ 4,
2712
+ 3,
2713
+ 2,
2714
+ 1
2715
+ ];
2716
+ /**
2717
+ * Number of records to place side by side: the largest supported amount, up
2718
+ * to `columns`, that keeps each card at least {@link minDataSetItemWidth}
2719
+ * pixels wide, besides the {@link dataSetItemGap} between cards, in a data set
2720
+ * `width` pixels wide, but never less than one.
2721
+ * While the width is unknown (`0`, e.g. before the first measure or during
2722
+ * server-side rendering), `columns` is used as is.
2723
+ */
2724
+ function getDataSetFittingColumns(columns, width) {
2725
+ if (!width) return columns;
2726
+ const fittingColumns = Math.min(columns, Math.floor((width + dataSetItemGap) / 264));
2727
+ return dataSetColumnsOptions.find((option) => option <= fittingColumns) ?? 1;
2728
+ }
2729
+ /**
2730
+ * Reactive version of {@link getDataSetFittingColumns}.
2731
+ */
2732
+ function useDataSetFittingColumns(columns, width) {
2733
+ return computed(() => getDataSetFittingColumns(toValue(columns), toValue(width)));
2734
+ }
2735
+ /**
2736
+ * Width, in pixels, of the given element, kept up to date with a
2737
+ * `ResizeObserver`. It's `0` while there's no element, e.g. before it's
2738
+ * mounted, and the observer is disconnected when the element changes or the
2739
+ * calling scope is disposed.
2740
+ */
2741
+ function useDataSetWidth(element) {
2742
+ const width = ref(0);
2743
+ let observer;
2744
+ function disconnect() {
2745
+ observer?.disconnect();
2746
+ observer = void 0;
2747
+ }
2748
+ watch(() => toValue(element), (currentElement) => {
2749
+ disconnect();
2750
+ width.value = 0;
2751
+ if (!currentElement) return;
2752
+ observer = new ResizeObserver(([entry]) => {
2753
+ width.value = entry.contentRect.width;
2754
+ });
2755
+ observer.observe(currentElement);
2756
+ }, { immediate: true });
2757
+ onScopeDispose(disconnect);
2758
+ return width;
2759
+ }
2760
+ /**
2761
+ * Maps the number of records per row to the width of each record's grid
2762
+ * column.
2763
+ */
2764
+ function useDataSetColumnCols(columns) {
2765
+ return computed(() => dataSetColumnsToColumnCols[toValue(columns)]);
2766
+ }
2767
+ /**
2768
+ * Maps the record properties considered by the search to Vuetify's filter
2769
+ * keys. An empty list means every property, just like an undefined one.
2770
+ */
2771
+ function useDataSetFilterKeys(searchKeys) {
2772
+ return computed(() => {
2773
+ const keys = toValue(searchKeys);
2774
+ return keys?.length ? keys : void 0;
2775
+ });
2776
+ }
2777
+ /**
2778
+ * Up to this many pages, every page button is shown. Vuetify's automatic
2779
+ * sizing collapses 3 pages into `1 … 2 … 3` in containers narrower than
2780
+ * ~336px, although showing every page never takes more room than its
2781
+ * collapsed form (`1 … x … 5`, 5 buttons) up to this amount.
2782
+ */
2783
+ const maxAlwaysVisiblePages = 5;
2784
+ /**
2785
+ * Number of page buttons to show for `pageCount` pages: every page up to
2786
+ * {@link maxAlwaysVisiblePages}, otherwise `undefined` to let Vuetify fit them
2787
+ * to the available width.
2788
+ */
2789
+ function getDataSetTotalVisible(pageCount) {
2790
+ return pageCount <= maxAlwaysVisiblePages ? pageCount : void 0;
2791
+ }
2792
+ /**
2793
+ * Goes back to the first page whenever the search changes. It must run before
2794
+ * the new search reaches Vuetify's iterator, which would otherwise clamp the
2795
+ * outdated page to the last one of the filtered records.
2796
+ */
2797
+ function useDataSetSearchPageReset(search, page) {
2798
+ watch(() => toValue(search), () => {
2799
+ page.value = 1;
2800
+ });
2801
+ }
2802
+ //#endregion
2803
+ //#region src/components/data-sets/data-set/data-set.vue
2804
+ var data_set_default = /* @__PURE__ */ defineComponent({
2805
+ inheritAttrs: false,
2806
+ __name: "data-set",
2807
+ props: /*@__PURE__*/ mergeModels({
2808
+ items: {
2809
+ type: Array,
2810
+ required: false
2811
+ },
2812
+ columns: {
2813
+ type: Number,
2814
+ required: false,
2815
+ default: 3
2816
+ },
2817
+ itemsPerPage: {
2818
+ type: Number,
2819
+ required: false,
2820
+ default: 10
2821
+ },
2822
+ searchable: {
2823
+ type: Boolean,
2824
+ required: false
2825
+ },
2826
+ searchKeys: {
2827
+ type: Array,
2828
+ required: false
2829
+ },
2830
+ noDataText: {
2831
+ type: String,
2832
+ required: false,
2833
+ default: "Nenhum registro encontrado."
2834
+ },
2835
+ loading: {
2836
+ type: Boolean,
2837
+ required: false
2838
+ },
2839
+ dataTestid: {
2840
+ type: String,
2841
+ required: false
2842
+ }
2843
+ }, {
2844
+ "page": {
2845
+ type: Number,
2846
+ default: 1
2847
+ },
2848
+ "pageModifiers": {},
2849
+ "search": {
2850
+ type: String,
2851
+ default: ""
2852
+ },
2853
+ "searchModifiers": {}
2854
+ }),
2855
+ emits: ["update:page", "update:search"],
2856
+ setup(__props) {
2857
+ const props = __props;
2858
+ useSlots();
2859
+ const page = useModel(__props, "page");
2860
+ const search = useModel(__props, "search");
2861
+ const iterator = useTemplateRef("iterator");
2862
+ const columnCols = useDataSetColumnCols(useDataSetFittingColumns(() => props.columns, useDataSetWidth(() => iterator.value?.$el)));
2863
+ const filterKeys = useDataSetFilterKeys(() => props.searchKeys);
2864
+ useDataSetSearchPageReset(search, page);
2865
+ return (_ctx, _cache) => {
2866
+ return openBlock(), createBlock(skeleton_loader_default, {
2867
+ loading: props.loading,
2868
+ type: "card",
2869
+ "full-width": ""
2870
+ }, {
2871
+ default: withCtx(() => [createVNode(unref(VDataIterator), {
2872
+ ref_key: "iterator",
2873
+ ref: iterator,
2874
+ page: page.value,
2875
+ "onUpdate:page": _cache[2] || (_cache[2] = ($event) => page.value = $event),
2876
+ items: props.items,
2877
+ "items-per-page": props.itemsPerPage,
2878
+ search: search.value,
2879
+ "filter-keys": unref(filterKeys),
2880
+ "data-testid": props.dataTestid
2881
+ }, {
2882
+ header: withCtx(() => [props.searchable ? (openBlock(), createBlock(text_field_default, {
2883
+ key: 0,
2884
+ modelValue: search.value,
2885
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event),
2886
+ type: "search",
2887
+ label: "Pesquisar"
2888
+ }, null, 8, ["modelValue"])) : createCommentVNode("v-if", true)]),
2889
+ default: withCtx(({ items: pageItems }) => [createCommentVNode(" Stretched, so the items of the same line (e.g. full-height cards) share its height "), createVNode(row_default, {
2890
+ list: "",
2891
+ "align-y": "stretch"
2892
+ }, {
2893
+ default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(pageItems, (pageItem, index) => {
2894
+ return openBlock(), createBlock(column_default, {
2895
+ key: index,
2896
+ cols: unref(columnCols),
2897
+ "list-item": ""
2898
+ }, {
2899
+ default: withCtx(() => [renderSlot(_ctx.$slots, "default", {
2900
+ item: pageItem.raw,
2901
+ index
2902
+ })]),
2903
+ _: 2
2904
+ }, 1032, ["cols"]);
2905
+ }), 128))]),
2906
+ _: 2
2907
+ }, 1024)]),
2908
+ "no-data": withCtx(() => [createVNode(unref(VEmptyState), { text: props.noDataText }, null, 8, ["text"])]),
2909
+ footer: withCtx(({ pageCount }) => [pageCount > 1 ? (openBlock(), createBlock(unref(VPagination), {
2910
+ key: 0,
2911
+ modelValue: page.value,
2912
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => page.value = $event),
2913
+ length: pageCount,
2914
+ "total-visible": unref(getDataSetTotalVisible)(pageCount)
2915
+ }, null, 8, [
2916
+ "modelValue",
2917
+ "length",
2918
+ "total-visible"
2919
+ ])) : createCommentVNode("v-if", true)]),
2920
+ _: 3
2921
+ }, 8, [
2922
+ "page",
2923
+ "items",
2924
+ "items-per-page",
2925
+ "search",
2926
+ "filter-keys",
2927
+ "data-testid"
2928
+ ])]),
2929
+ _: 3
2930
+ }, 8, ["loading"]);
2931
+ };
2932
+ }
2933
+ });
2934
+ //#endregion
2935
+ //#region src/components/data-sets/data-set-item/data-set-item.vue
2936
+ var data_set_item_default = /* @__PURE__ */ defineComponent({
2937
+ inheritAttrs: false,
2938
+ __name: "data-set-item",
2939
+ props: {
2940
+ title: {
2941
+ type: String,
2942
+ required: false
2943
+ },
2944
+ subtitle: {
2945
+ type: String,
2946
+ required: false
2947
+ },
2948
+ variant: {
2949
+ type: String,
2950
+ required: false
2951
+ },
2952
+ actions: {
2953
+ type: Array,
2954
+ required: false
2955
+ },
2956
+ dataTestid: {
2957
+ type: String,
2958
+ required: false
2959
+ }
2960
+ },
2961
+ setup(__props) {
2962
+ const props = __props;
2963
+ const slots = useSlots();
2964
+ return (_ctx, _cache) => {
2965
+ return openBlock(), createBlock(section_default, {
2966
+ title: props.title,
2967
+ subtitle: props.subtitle,
2968
+ variant: props.variant,
2969
+ actions: props.actions,
2970
+ "data-testid": props.dataTestid,
2971
+ "full-height": ""
2972
+ }, {
2973
+ default: withCtx(() => [slots.default ? (openBlock(), createBlock(section_content_default, { key: 0 }, {
2974
+ default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
2975
+ _: 3
2976
+ })) : createCommentVNode("v-if", true)]),
2977
+ _: 3
2978
+ }, 8, [
2979
+ "title",
2980
+ "subtitle",
2981
+ "variant",
2982
+ "actions",
2983
+ "data-testid"
2984
+ ]);
2985
+ };
2986
+ }
2987
+ });
2988
+ //#endregion
2989
+ export { autocomplete_field_default as UAutocompleteField, button_default as UButton, checkbox_default as UCheckbox, column_default as UColumn, container_default as UContainer, data_set_default as UDataSet, data_set_item_default as UDataSetItem, date_time_field_default as UDateTimeField, details_default as UDetails, dialog_default as UDialog, form_default as UForm, img_default as UImg, link_default as ULink, main_default as UMain, mask_field_default as UMaskField, row_default as URow, section_default as USection, section_content_default as USectionContent, table_default as UTable, text_field_default as UTextField };