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

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