ads-nuxt 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +567 -546
- package/app/app.config.ts +25 -25
- package/app/assets/ads/checkbox-check.svg +5 -5
- package/app/assets/ads/checkbox-minus.svg +5 -5
- package/app/assets/brand/farmarcas.svg +6 -6
- package/app/assets/brand/radar.svg +9 -9
- package/app/assets/css/component-tokens.css +403 -403
- package/app/assets/css/main.css +617 -617
- package/app/components/AdsBadge.vue +48 -48
- package/app/components/AdsBroadcast.vue +45 -45
- package/app/components/AdsButton.vue +569 -569
- package/app/components/AdsButtonGroup.vue +39 -39
- package/app/components/AdsButtonMenu.vue +48 -48
- package/app/components/AdsCard.vue +117 -117
- package/app/components/AdsCharttip.vue +78 -78
- package/app/components/AdsCheckbox.vue +77 -77
- package/app/components/AdsDatePicker.vue +473 -473
- package/app/components/AdsDropdownMenu.vue +19 -19
- package/app/components/AdsEmptyState.vue +58 -58
- package/app/components/AdsFarmarcasPill.vue +74 -74
- package/app/components/AdsFeatureCard.vue +217 -217
- package/app/components/AdsFileCard.vue +101 -101
- package/app/components/AdsFilterDropdown.vue +174 -174
- package/app/components/AdsFilterOption.vue +72 -72
- package/app/components/AdsFilterSelect.vue +202 -202
- package/app/components/AdsFooter.vue +25 -25
- package/app/components/AdsInput.vue +143 -143
- package/app/components/AdsListDropdown.vue +44 -44
- package/app/components/AdsLoading.vue +109 -109
- package/app/components/AdsMenuBar.vue +103 -103
- package/app/components/AdsMetricCard.vue +119 -119
- package/app/components/AdsMiniPill.vue +40 -40
- package/app/components/AdsModal.vue +157 -157
- package/app/components/AdsMonthPicker.vue +80 -80
- package/app/components/AdsNavbarApp.vue +67 -67
- package/app/components/AdsNavbarDesk.vue +62 -62
- package/app/components/AdsNotabilityPill.vue +27 -27
- package/app/components/AdsNumberInput.vue +166 -166
- package/app/components/AdsPagination.vue +91 -91
- package/app/components/AdsProductTag.vue +61 -61
- package/app/components/AdsRadio.vue +70 -70
- package/app/components/AdsSearchInput.vue +113 -113
- package/app/components/AdsSegmentedTabs.vue +28 -28
- package/app/components/AdsSideMenu.vue +101 -101
- package/app/components/AdsSmalltip.vue +44 -44
- package/app/components/AdsSteptip.vue +101 -101
- package/app/components/AdsSwitch.vue +59 -59
- package/app/components/AdsTable.vue +100 -100
- package/app/components/AdsTableToolbar.vue +44 -44
- package/app/components/AdsTabs.vue +51 -51
- package/app/components/AdsTextarea.vue +109 -109
- package/app/components/AdsToast.vue +181 -181
- package/app/components/AdsTooltip.vue +66 -66
- package/app/components/InvertedTooltip.vue +40 -40
- package/app/components/OverlayLoading.vue +173 -173
- package/app/components/ProgressBarLoading.vue +256 -256
- package/app/composables/useOverlayLoading.ts +66 -66
- package/app/locale/pt-br.ts +6 -6
- package/app/plugins/ads-toast.client.ts +24 -24
- package/app/theme/ads-theme.ts +370 -370
- package/app/theme/component-tokens.json +402 -402
- package/app/theme/component-tokens.ts +815 -815
- package/app/types/overlay-loading.types.ts +26 -26
- package/app/utils/ads-filter-select.ts +172 -172
- package/app/utils/ads-table.ts +204 -204
- package/app/utils/calendar-date.ts +128 -128
- package/app/utils/category-role.ts +45 -45
- package/app/utils/money.ts +77 -77
- package/app/utils/notability.ts +8 -8
- package/app/utils/parser.ts +134 -134
- package/app/utils/percentage.ts +54 -54
- package/nuxt.config.ts +31 -32
- package/package.json +48 -46
|
@@ -1,256 +1,256 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div :class="rootClass">
|
|
3
|
-
<div v-if="hasHeader" class="flex items-start gap-3">
|
|
4
|
-
<span
|
|
5
|
-
v-if="showIcon"
|
|
6
|
-
class="inline-flex size-10 shrink-0 items-center justify-center rounded-lg border border-primary-200 bg-primary-50"
|
|
7
|
-
>
|
|
8
|
-
<UIcon
|
|
9
|
-
:name="icon"
|
|
10
|
-
class="size-6 text-primary"
|
|
11
|
-
:class="{ 'animate-spin': spinIcon }"
|
|
12
|
-
aria-hidden="true"
|
|
13
|
-
/>
|
|
14
|
-
</span>
|
|
15
|
-
<div class="min-w-0 flex-1">
|
|
16
|
-
<p v-if="title" class="font-semibold text-highlighted">{{ title }}</p>
|
|
17
|
-
<p
|
|
18
|
-
v-if="description"
|
|
19
|
-
class="text-sm text-muted"
|
|
20
|
-
:class="title ? 'mt-0.5' : ''"
|
|
21
|
-
>
|
|
22
|
-
{{ description }}
|
|
23
|
-
</p>
|
|
24
|
-
</div>
|
|
25
|
-
<span
|
|
26
|
-
v-if="reservePercentSlot"
|
|
27
|
-
class="shrink-0 text-sm font-medium tabular-nums text-highlighted"
|
|
28
|
-
:class="{ invisible: !isPercentVisible }"
|
|
29
|
-
:aria-hidden="!isPercentVisible"
|
|
30
|
-
>
|
|
31
|
-
{{ percentClamped }}%
|
|
32
|
-
</span>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<div
|
|
36
|
-
class="h-[var(--ads-progress-height)] w-full overflow-hidden rounded-[var(--ads-progress-radius)] bg-[var(--ads-progress-track)]"
|
|
37
|
-
role="progressbar"
|
|
38
|
-
:aria-valuenow="indeterminate ? undefined : percentClamped"
|
|
39
|
-
aria-valuemin="0"
|
|
40
|
-
aria-valuemax="100"
|
|
41
|
-
:aria-busy="indeterminate ? true : undefined"
|
|
42
|
-
:aria-label="effectiveAriaLabel"
|
|
43
|
-
>
|
|
44
|
-
<div
|
|
45
|
-
v-if="indeterminate"
|
|
46
|
-
class="progress-bar-loading__indeterminate h-full rounded-full"
|
|
47
|
-
:class="fillClass"
|
|
48
|
-
/>
|
|
49
|
-
<div
|
|
50
|
-
v-else
|
|
51
|
-
class="h-full rounded-full transition-[width] duration-300 ease-out"
|
|
52
|
-
:class="fillClass"
|
|
53
|
-
:style="{ width: `${percentClamped}%` }"
|
|
54
|
-
/>
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
<div
|
|
58
|
-
v-if="reserveElapsedTimeSlot"
|
|
59
|
-
class="flex justify-end"
|
|
60
|
-
>
|
|
61
|
-
<span
|
|
62
|
-
class="inline-flex items-center gap-1 font-mono text-xs tabular-nums text-muted"
|
|
63
|
-
:class="{ invisible: !showElapsedTime }"
|
|
64
|
-
role="timer"
|
|
65
|
-
aria-live="off"
|
|
66
|
-
:aria-hidden="!showElapsedTime"
|
|
67
|
-
:aria-label="`Tempo decorrido: ${elapsedDisplay}`"
|
|
68
|
-
>
|
|
69
|
-
<UIcon
|
|
70
|
-
name="i-lucide-timer"
|
|
71
|
-
class="size-3.5 text-muted"
|
|
72
|
-
aria-hidden="true"
|
|
73
|
-
/>
|
|
74
|
-
{{ elapsedDisplay }}
|
|
75
|
-
</span>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</template>
|
|
79
|
-
|
|
80
|
-
<script setup lang="ts">
|
|
81
|
-
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
82
|
-
|
|
83
|
-
type ProgressBarLoadingVariant = "primary" | "success" | "error";
|
|
84
|
-
|
|
85
|
-
interface Props {
|
|
86
|
-
title?: string;
|
|
87
|
-
description?: string;
|
|
88
|
-
icon?: string;
|
|
89
|
-
showIcon?: boolean;
|
|
90
|
-
spinIcon?: boolean;
|
|
91
|
-
indeterminate?: boolean;
|
|
92
|
-
percent?: number;
|
|
93
|
-
showPercent?: boolean;
|
|
94
|
-
showElapsedTime?: boolean;
|
|
95
|
-
ariaLabel?: string;
|
|
96
|
-
variant?: ProgressBarLoadingVariant;
|
|
97
|
-
rootClass?: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
101
|
-
title: "",
|
|
102
|
-
description: "",
|
|
103
|
-
icon: "i-lucide-loader-circle",
|
|
104
|
-
showIcon: true,
|
|
105
|
-
spinIcon: true,
|
|
106
|
-
indeterminate: false,
|
|
107
|
-
percent: 0,
|
|
108
|
-
showPercent: true,
|
|
109
|
-
showElapsedTime: true,
|
|
110
|
-
ariaLabel: "",
|
|
111
|
-
variant: "primary",
|
|
112
|
-
rootClass: "flex flex-col gap-3",
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const percentClamped = computed(() =>
|
|
116
|
-
Math.max(0, Math.min(100, Math.round(props.percent ?? 0))),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const fillClass = computed(() => {
|
|
120
|
-
if (props.variant === "success") return "bg-success-500";
|
|
121
|
-
if (props.variant === "error") return "bg-error-500";
|
|
122
|
-
return "bg-[var(--ads-progress-fill)]";
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const effectiveAriaLabel = computed(
|
|
126
|
-
() => props.ariaLabel || props.title || "Carregando",
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const isPercentVisible = computed(
|
|
130
|
-
() => Boolean(props.showPercent) && !props.indeterminate,
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Slots reservados de forma "sticky-true": uma vez que o slot é necessário (header,
|
|
135
|
-
* timer ou percent), mantemos o nó renderizado para sempre. Isso evita que mudanças
|
|
136
|
-
* de status (ex.: pipeline `uploading` → `success`, `indeterminate` → determinado,
|
|
137
|
-
* `showElapsedTime` true → false) movam outros elementos no eixo Y.
|
|
138
|
-
*
|
|
139
|
-
* A alternância visual é feita apenas via `invisible` (mantém o espaço reservado).
|
|
140
|
-
*/
|
|
141
|
-
const computedHasHeader = () =>
|
|
142
|
-
Boolean(props.showIcon) ||
|
|
143
|
-
Boolean(props.title) ||
|
|
144
|
-
Boolean(props.description) ||
|
|
145
|
-
(!props.indeterminate && Boolean(props.showPercent));
|
|
146
|
-
|
|
147
|
-
const hasHeader = ref(computedHasHeader());
|
|
148
|
-
const reserveElapsedTimeSlot = ref(Boolean(props.showElapsedTime));
|
|
149
|
-
const reservePercentSlot = ref(Boolean(props.showPercent));
|
|
150
|
-
|
|
151
|
-
watch(computedHasHeader, (val) => {
|
|
152
|
-
if (val) hasHeader.value = true;
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
watch(
|
|
156
|
-
() => Boolean(props.showElapsedTime),
|
|
157
|
-
(val) => {
|
|
158
|
-
if (val) reserveElapsedTimeSlot.value = true;
|
|
159
|
-
},
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
watch(
|
|
163
|
-
() => Boolean(props.showPercent),
|
|
164
|
-
(val) => {
|
|
165
|
-
if (val) reservePercentSlot.value = true;
|
|
166
|
-
},
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const startedAt = ref<number>(0);
|
|
170
|
-
const now = ref<number>(0);
|
|
171
|
-
let elapsedTimerId: ReturnType<typeof setInterval> | null = null;
|
|
172
|
-
|
|
173
|
-
const stopElapsedTimer = () => {
|
|
174
|
-
if (elapsedTimerId != null) {
|
|
175
|
-
clearInterval(elapsedTimerId);
|
|
176
|
-
elapsedTimerId = null;
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const startElapsedTimer = () => {
|
|
181
|
-
stopElapsedTimer();
|
|
182
|
-
startedAt.value = Date.now();
|
|
183
|
-
now.value = startedAt.value;
|
|
184
|
-
if (typeof window === "undefined") {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
elapsedTimerId = setInterval(() => {
|
|
188
|
-
now.value = Date.now();
|
|
189
|
-
}, 1000);
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const elapsedSeconds = computed(() =>
|
|
193
|
-
Math.max(0, Math.floor((now.value - startedAt.value) / 1000)),
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const elapsedDisplay = computed(() => {
|
|
197
|
-
const total = elapsedSeconds.value;
|
|
198
|
-
const hours = Math.floor(total / 3600);
|
|
199
|
-
const minutes = Math.floor((total % 3600) / 60);
|
|
200
|
-
const seconds = total % 60;
|
|
201
|
-
const pad = (n: number) => String(n).padStart(2, "0");
|
|
202
|
-
if (hours > 0) {
|
|
203
|
-
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
204
|
-
}
|
|
205
|
-
return `${pad(minutes)}:${pad(seconds)}`;
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
watch(
|
|
209
|
-
() => props.showElapsedTime,
|
|
210
|
-
(enabled) => {
|
|
211
|
-
if (enabled) {
|
|
212
|
-
startElapsedTimer();
|
|
213
|
-
} else {
|
|
214
|
-
stopElapsedTimer();
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
onMounted(() => {
|
|
220
|
-
if (props.showElapsedTime) {
|
|
221
|
-
startElapsedTimer();
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
onBeforeUnmount(() => {
|
|
226
|
-
stopElapsedTimer();
|
|
227
|
-
});
|
|
228
|
-
</script>
|
|
229
|
-
|
|
230
|
-
<style scoped>
|
|
231
|
-
.progress-bar-loading__indeterminate {
|
|
232
|
-
width: 35%;
|
|
233
|
-
animation: progress-bar-loading-indeterminate 1.4s ease-in-out infinite;
|
|
234
|
-
transform-origin: left;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
@keyframes progress-bar-loading-indeterminate {
|
|
238
|
-
0% {
|
|
239
|
-
margin-inline-start: -35%;
|
|
240
|
-
}
|
|
241
|
-
60% {
|
|
242
|
-
margin-inline-start: 100%;
|
|
243
|
-
}
|
|
244
|
-
100% {
|
|
245
|
-
margin-inline-start: 100%;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
@media (prefers-reduced-motion: reduce) {
|
|
250
|
-
.progress-bar-loading__indeterminate {
|
|
251
|
-
width: 100%;
|
|
252
|
-
animation: none;
|
|
253
|
-
opacity: 0.6;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="rootClass">
|
|
3
|
+
<div v-if="hasHeader" class="flex items-start gap-3">
|
|
4
|
+
<span
|
|
5
|
+
v-if="showIcon"
|
|
6
|
+
class="inline-flex size-10 shrink-0 items-center justify-center rounded-lg border border-primary-200 bg-primary-50"
|
|
7
|
+
>
|
|
8
|
+
<UIcon
|
|
9
|
+
:name="icon"
|
|
10
|
+
class="size-6 text-primary"
|
|
11
|
+
:class="{ 'animate-spin': spinIcon }"
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
/>
|
|
14
|
+
</span>
|
|
15
|
+
<div class="min-w-0 flex-1">
|
|
16
|
+
<p v-if="title" class="font-semibold text-highlighted">{{ title }}</p>
|
|
17
|
+
<p
|
|
18
|
+
v-if="description"
|
|
19
|
+
class="text-sm text-muted"
|
|
20
|
+
:class="title ? 'mt-0.5' : ''"
|
|
21
|
+
>
|
|
22
|
+
{{ description }}
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
<span
|
|
26
|
+
v-if="reservePercentSlot"
|
|
27
|
+
class="shrink-0 text-sm font-medium tabular-nums text-highlighted"
|
|
28
|
+
:class="{ invisible: !isPercentVisible }"
|
|
29
|
+
:aria-hidden="!isPercentVisible"
|
|
30
|
+
>
|
|
31
|
+
{{ percentClamped }}%
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div
|
|
36
|
+
class="h-[var(--ads-progress-height)] w-full overflow-hidden rounded-[var(--ads-progress-radius)] bg-[var(--ads-progress-track)]"
|
|
37
|
+
role="progressbar"
|
|
38
|
+
:aria-valuenow="indeterminate ? undefined : percentClamped"
|
|
39
|
+
aria-valuemin="0"
|
|
40
|
+
aria-valuemax="100"
|
|
41
|
+
:aria-busy="indeterminate ? true : undefined"
|
|
42
|
+
:aria-label="effectiveAriaLabel"
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
v-if="indeterminate"
|
|
46
|
+
class="progress-bar-loading__indeterminate h-full rounded-full"
|
|
47
|
+
:class="fillClass"
|
|
48
|
+
/>
|
|
49
|
+
<div
|
|
50
|
+
v-else
|
|
51
|
+
class="h-full rounded-full transition-[width] duration-300 ease-out"
|
|
52
|
+
:class="fillClass"
|
|
53
|
+
:style="{ width: `${percentClamped}%` }"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div
|
|
58
|
+
v-if="reserveElapsedTimeSlot"
|
|
59
|
+
class="flex justify-end"
|
|
60
|
+
>
|
|
61
|
+
<span
|
|
62
|
+
class="inline-flex items-center gap-1 font-mono text-xs tabular-nums text-muted"
|
|
63
|
+
:class="{ invisible: !showElapsedTime }"
|
|
64
|
+
role="timer"
|
|
65
|
+
aria-live="off"
|
|
66
|
+
:aria-hidden="!showElapsedTime"
|
|
67
|
+
:aria-label="`Tempo decorrido: ${elapsedDisplay}`"
|
|
68
|
+
>
|
|
69
|
+
<UIcon
|
|
70
|
+
name="i-lucide-timer"
|
|
71
|
+
class="size-3.5 text-muted"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
/>
|
|
74
|
+
{{ elapsedDisplay }}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup lang="ts">
|
|
81
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
82
|
+
|
|
83
|
+
type ProgressBarLoadingVariant = "primary" | "success" | "error";
|
|
84
|
+
|
|
85
|
+
interface Props {
|
|
86
|
+
title?: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
icon?: string;
|
|
89
|
+
showIcon?: boolean;
|
|
90
|
+
spinIcon?: boolean;
|
|
91
|
+
indeterminate?: boolean;
|
|
92
|
+
percent?: number;
|
|
93
|
+
showPercent?: boolean;
|
|
94
|
+
showElapsedTime?: boolean;
|
|
95
|
+
ariaLabel?: string;
|
|
96
|
+
variant?: ProgressBarLoadingVariant;
|
|
97
|
+
rootClass?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
101
|
+
title: "",
|
|
102
|
+
description: "",
|
|
103
|
+
icon: "i-lucide-loader-circle",
|
|
104
|
+
showIcon: true,
|
|
105
|
+
spinIcon: true,
|
|
106
|
+
indeterminate: false,
|
|
107
|
+
percent: 0,
|
|
108
|
+
showPercent: true,
|
|
109
|
+
showElapsedTime: true,
|
|
110
|
+
ariaLabel: "",
|
|
111
|
+
variant: "primary",
|
|
112
|
+
rootClass: "flex flex-col gap-3",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const percentClamped = computed(() =>
|
|
116
|
+
Math.max(0, Math.min(100, Math.round(props.percent ?? 0))),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const fillClass = computed(() => {
|
|
120
|
+
if (props.variant === "success") return "bg-success-500";
|
|
121
|
+
if (props.variant === "error") return "bg-error-500";
|
|
122
|
+
return "bg-[var(--ads-progress-fill)]";
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const effectiveAriaLabel = computed(
|
|
126
|
+
() => props.ariaLabel || props.title || "Carregando",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const isPercentVisible = computed(
|
|
130
|
+
() => Boolean(props.showPercent) && !props.indeterminate,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Slots reservados de forma "sticky-true": uma vez que o slot é necessário (header,
|
|
135
|
+
* timer ou percent), mantemos o nó renderizado para sempre. Isso evita que mudanças
|
|
136
|
+
* de status (ex.: pipeline `uploading` → `success`, `indeterminate` → determinado,
|
|
137
|
+
* `showElapsedTime` true → false) movam outros elementos no eixo Y.
|
|
138
|
+
*
|
|
139
|
+
* A alternância visual é feita apenas via `invisible` (mantém o espaço reservado).
|
|
140
|
+
*/
|
|
141
|
+
const computedHasHeader = () =>
|
|
142
|
+
Boolean(props.showIcon) ||
|
|
143
|
+
Boolean(props.title) ||
|
|
144
|
+
Boolean(props.description) ||
|
|
145
|
+
(!props.indeterminate && Boolean(props.showPercent));
|
|
146
|
+
|
|
147
|
+
const hasHeader = ref(computedHasHeader());
|
|
148
|
+
const reserveElapsedTimeSlot = ref(Boolean(props.showElapsedTime));
|
|
149
|
+
const reservePercentSlot = ref(Boolean(props.showPercent));
|
|
150
|
+
|
|
151
|
+
watch(computedHasHeader, (val) => {
|
|
152
|
+
if (val) hasHeader.value = true;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
watch(
|
|
156
|
+
() => Boolean(props.showElapsedTime),
|
|
157
|
+
(val) => {
|
|
158
|
+
if (val) reserveElapsedTimeSlot.value = true;
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
watch(
|
|
163
|
+
() => Boolean(props.showPercent),
|
|
164
|
+
(val) => {
|
|
165
|
+
if (val) reservePercentSlot.value = true;
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const startedAt = ref<number>(0);
|
|
170
|
+
const now = ref<number>(0);
|
|
171
|
+
let elapsedTimerId: ReturnType<typeof setInterval> | null = null;
|
|
172
|
+
|
|
173
|
+
const stopElapsedTimer = () => {
|
|
174
|
+
if (elapsedTimerId != null) {
|
|
175
|
+
clearInterval(elapsedTimerId);
|
|
176
|
+
elapsedTimerId = null;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const startElapsedTimer = () => {
|
|
181
|
+
stopElapsedTimer();
|
|
182
|
+
startedAt.value = Date.now();
|
|
183
|
+
now.value = startedAt.value;
|
|
184
|
+
if (typeof window === "undefined") {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
elapsedTimerId = setInterval(() => {
|
|
188
|
+
now.value = Date.now();
|
|
189
|
+
}, 1000);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const elapsedSeconds = computed(() =>
|
|
193
|
+
Math.max(0, Math.floor((now.value - startedAt.value) / 1000)),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const elapsedDisplay = computed(() => {
|
|
197
|
+
const total = elapsedSeconds.value;
|
|
198
|
+
const hours = Math.floor(total / 3600);
|
|
199
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
200
|
+
const seconds = total % 60;
|
|
201
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
202
|
+
if (hours > 0) {
|
|
203
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
|
204
|
+
}
|
|
205
|
+
return `${pad(minutes)}:${pad(seconds)}`;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
watch(
|
|
209
|
+
() => props.showElapsedTime,
|
|
210
|
+
(enabled) => {
|
|
211
|
+
if (enabled) {
|
|
212
|
+
startElapsedTimer();
|
|
213
|
+
} else {
|
|
214
|
+
stopElapsedTimer();
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
onMounted(() => {
|
|
220
|
+
if (props.showElapsedTime) {
|
|
221
|
+
startElapsedTimer();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
onBeforeUnmount(() => {
|
|
226
|
+
stopElapsedTimer();
|
|
227
|
+
});
|
|
228
|
+
</script>
|
|
229
|
+
|
|
230
|
+
<style scoped>
|
|
231
|
+
.progress-bar-loading__indeterminate {
|
|
232
|
+
width: 35%;
|
|
233
|
+
animation: progress-bar-loading-indeterminate 1.4s ease-in-out infinite;
|
|
234
|
+
transform-origin: left;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@keyframes progress-bar-loading-indeterminate {
|
|
238
|
+
0% {
|
|
239
|
+
margin-inline-start: -35%;
|
|
240
|
+
}
|
|
241
|
+
60% {
|
|
242
|
+
margin-inline-start: 100%;
|
|
243
|
+
}
|
|
244
|
+
100% {
|
|
245
|
+
margin-inline-start: 100%;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@media (prefers-reduced-motion: reduce) {
|
|
250
|
+
.progress-bar-loading__indeterminate {
|
|
251
|
+
width: 100%;
|
|
252
|
+
animation: none;
|
|
253
|
+
opacity: 0.6;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
</style>
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import type { OverlayLoadingOptions } from "../types/overlay-loading.types";
|
|
2
|
-
|
|
3
|
-
export const useOverlayLoading = () => {
|
|
4
|
-
const overlay = useOverlay();
|
|
5
|
-
const OverlayLoadingComponent = defineAsyncComponent(
|
|
6
|
-
() => import("../components/OverlayLoading.vue")
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
let currentOverlay: {
|
|
10
|
-
id: symbol;
|
|
11
|
-
open: () => void;
|
|
12
|
-
close: () => void;
|
|
13
|
-
patch: (props: Record<string, unknown>) => void;
|
|
14
|
-
} | null = null;
|
|
15
|
-
const show = (options: OverlayLoadingOptions) => {
|
|
16
|
-
if (currentOverlay) {
|
|
17
|
-
currentOverlay.close();
|
|
18
|
-
}
|
|
19
|
-
currentOverlay = overlay.create(OverlayLoadingComponent, {
|
|
20
|
-
destroyOnClose: true,
|
|
21
|
-
props: {
|
|
22
|
-
isVisible: true,
|
|
23
|
-
...options,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
currentOverlay?.open();
|
|
28
|
-
|
|
29
|
-
return currentOverlay?.id ?? null;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const close = () => {
|
|
33
|
-
if (currentOverlay) {
|
|
34
|
-
currentOverlay.close();
|
|
35
|
-
currentOverlay = null;
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Mescla props no componente do overlay. Props devem ser planas (title, description, …):
|
|
41
|
-
* o `patch` do useOverlay faz `{ ...overlay.props, ...patch }`, não aceita `{ props: { … } }`.
|
|
42
|
-
*/
|
|
43
|
-
const update = (options: Partial<OverlayLoadingOptions>) => {
|
|
44
|
-
if (currentOverlay) {
|
|
45
|
-
currentOverlay.patch({
|
|
46
|
-
isVisible: true,
|
|
47
|
-
...options,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const isOpen = () => {
|
|
53
|
-
return currentOverlay !== null;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const getCurrentId = () => currentOverlay?.id ?? null;
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
show,
|
|
60
|
-
close,
|
|
61
|
-
update,
|
|
62
|
-
isOpen,
|
|
63
|
-
getCurrentId,
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
|
|
1
|
+
import type { OverlayLoadingOptions } from "../types/overlay-loading.types";
|
|
2
|
+
|
|
3
|
+
export const useOverlayLoading = () => {
|
|
4
|
+
const overlay = useOverlay();
|
|
5
|
+
const OverlayLoadingComponent = defineAsyncComponent(
|
|
6
|
+
() => import("../components/OverlayLoading.vue")
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
let currentOverlay: {
|
|
10
|
+
id: symbol;
|
|
11
|
+
open: () => void;
|
|
12
|
+
close: () => void;
|
|
13
|
+
patch: (props: Record<string, unknown>) => void;
|
|
14
|
+
} | null = null;
|
|
15
|
+
const show = (options: OverlayLoadingOptions) => {
|
|
16
|
+
if (currentOverlay) {
|
|
17
|
+
currentOverlay.close();
|
|
18
|
+
}
|
|
19
|
+
currentOverlay = overlay.create(OverlayLoadingComponent, {
|
|
20
|
+
destroyOnClose: true,
|
|
21
|
+
props: {
|
|
22
|
+
isVisible: true,
|
|
23
|
+
...options,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
currentOverlay?.open();
|
|
28
|
+
|
|
29
|
+
return currentOverlay?.id ?? null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const close = () => {
|
|
33
|
+
if (currentOverlay) {
|
|
34
|
+
currentOverlay.close();
|
|
35
|
+
currentOverlay = null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mescla props no componente do overlay. Props devem ser planas (title, description, …):
|
|
41
|
+
* o `patch` do useOverlay faz `{ ...overlay.props, ...patch }`, não aceita `{ props: { … } }`.
|
|
42
|
+
*/
|
|
43
|
+
const update = (options: Partial<OverlayLoadingOptions>) => {
|
|
44
|
+
if (currentOverlay) {
|
|
45
|
+
currentOverlay.patch({
|
|
46
|
+
isVisible: true,
|
|
47
|
+
...options,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isOpen = () => {
|
|
53
|
+
return currentOverlay !== null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getCurrentId = () => currentOverlay?.id ?? null;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
show,
|
|
60
|
+
close,
|
|
61
|
+
update,
|
|
62
|
+
isOpen,
|
|
63
|
+
getCurrentId,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
package/app/locale/pt-br.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { pt_br } from "@nuxt/ui/locale";
|
|
2
|
-
|
|
3
|
-
/** Locale do Nuxt UI para calendário, meses e rótulos em português do Brasil. */
|
|
4
|
-
export const uiLocale = pt_br;
|
|
5
|
-
|
|
6
|
-
export const UI_LOCALE_CODE = "pt-BR";
|
|
1
|
+
import { pt_br } from "@nuxt/ui/locale";
|
|
2
|
+
|
|
3
|
+
/** Locale do Nuxt UI para calendário, meses e rótulos em português do Brasil. */
|
|
4
|
+
export const uiLocale = pt_br;
|
|
5
|
+
|
|
6
|
+
export const UI_LOCALE_CODE = "pt-BR";
|