@nexdom/uimed-vue 2.0.0-beta.5 → 2.0.0-beta.50

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