ads-nuxt 0.1.0 → 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 -1
- 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,473 +1,473 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { isEqualMonth, type CalendarDate } from "@internationalized/date";
|
|
3
|
-
import { computed, nextTick, ref, shallowRef, watch } from "vue";
|
|
4
|
-
import AdsButton from "./AdsButton.vue";
|
|
5
|
-
import { UI_LOCALE_CODE } from "../locale/pt-br";
|
|
6
|
-
import { formatDateOnlyToPtBr, getTodayDateString } from "../utils/parser";
|
|
7
|
-
import {
|
|
8
|
-
calendarDateToDateString,
|
|
9
|
-
calendarMonthTitle,
|
|
10
|
-
calendarRangeToDateStrings,
|
|
11
|
-
calendarRangesEqual,
|
|
12
|
-
dateStringToCalendarDate,
|
|
13
|
-
dateStringsToCalendarRange,
|
|
14
|
-
inclusiveCalendarDays,
|
|
15
|
-
isCalendarDateRange,
|
|
16
|
-
isOutsideMaximumWindow,
|
|
17
|
-
shiftVisibleMonth,
|
|
18
|
-
visibleMonthAfterPlaceholder,
|
|
19
|
-
type CalendarDateRangeValue,
|
|
20
|
-
} from "../utils/calendar-date";
|
|
21
|
-
|
|
22
|
-
const DATE_PLACEHOLDER = "dd/mm/aaaa";
|
|
23
|
-
const RANGE_SEPARATOR = " – ";
|
|
24
|
-
|
|
25
|
-
const props = withDefaults(
|
|
26
|
-
defineProps<{
|
|
27
|
-
range?: boolean;
|
|
28
|
-
min?: string;
|
|
29
|
-
max?: string;
|
|
30
|
-
disabled?: boolean;
|
|
31
|
-
readonly?: boolean;
|
|
32
|
-
size?: "sm" | "md" | "lg";
|
|
33
|
-
maximumDays?: number;
|
|
34
|
-
class?: string;
|
|
35
|
-
}>(),
|
|
36
|
-
{
|
|
37
|
-
range: false,
|
|
38
|
-
size: "md",
|
|
39
|
-
},
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const modelValue = defineModel<string>({ default: "" });
|
|
43
|
-
const start = defineModel<string>("start", { default: "" });
|
|
44
|
-
const end = defineModel<string>("end", { default: "" });
|
|
45
|
-
|
|
46
|
-
const inputDateRef = useTemplateRef("inputDateRef");
|
|
47
|
-
const syncingFromCalendar = ref(false);
|
|
48
|
-
const rangeOpen = ref(false);
|
|
49
|
-
const rangeCalendarKey = ref(0);
|
|
50
|
-
const ignoreDraftUpdates = ref(false);
|
|
51
|
-
const rangeSessionStart = ref("");
|
|
52
|
-
const rangeSessionEnd = ref("");
|
|
53
|
-
|
|
54
|
-
type CalendarModel = CalendarDate | CalendarDateRangeValue | undefined;
|
|
55
|
-
|
|
56
|
-
const calendarModel = shallowRef<CalendarModel>();
|
|
57
|
-
const draftRange = shallowRef<CalendarDateRangeValue | undefined>();
|
|
58
|
-
const leftPlaceholder = shallowRef<CalendarDate>(
|
|
59
|
-
dateStringToCalendarDate(getTodayDateString())!.set({ day: 1 }),
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const minCalendarDate = computed(() =>
|
|
63
|
-
props.min ? dateStringToCalendarDate(props.min) : undefined,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const maxCalendarDate = computed(() =>
|
|
67
|
-
props.max ? dateStringToCalendarDate(props.max) : undefined,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
const leftMonth = computed(() => leftPlaceholder.value.set({ day: 1 }));
|
|
71
|
-
const rightMonth = computed(() => leftMonth.value.add({ months: 1 }));
|
|
72
|
-
|
|
73
|
-
const navigationMax = computed(() => {
|
|
74
|
-
const hardMax = maxCalendarDate.value;
|
|
75
|
-
const draft = draftRange.value;
|
|
76
|
-
if (!props.maximumDays || !draft?.start || draft.end) return hardMax;
|
|
77
|
-
|
|
78
|
-
const windowEnd = draft.start.add({ days: props.maximumDays - 1 });
|
|
79
|
-
if (!hardMax || windowEnd.compare(hardMax) < 0) return windowEnd;
|
|
80
|
-
return hardMax;
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const canShiftToPreviousMonth = computed(
|
|
84
|
-
() =>
|
|
85
|
-
shiftVisibleMonth(
|
|
86
|
-
leftPlaceholder.value,
|
|
87
|
-
-1,
|
|
88
|
-
minCalendarDate.value,
|
|
89
|
-
navigationMax.value,
|
|
90
|
-
).compare(leftMonth.value) !== 0,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const canShiftToNextMonth = computed(
|
|
94
|
-
() =>
|
|
95
|
-
shiftVisibleMonth(
|
|
96
|
-
leftPlaceholder.value,
|
|
97
|
-
1,
|
|
98
|
-
minCalendarDate.value,
|
|
99
|
-
navigationMax.value,
|
|
100
|
-
).compare(leftMonth.value) !== 0,
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
const rangeStepLabel = computed(() => {
|
|
104
|
-
const draft = draftRange.value;
|
|
105
|
-
if (!draft?.start) return "Escolha o início";
|
|
106
|
-
|
|
107
|
-
const startLabel = formatDateOnlyToPtBr(calendarDateToDateString(draft.start));
|
|
108
|
-
if (!draft.end) {
|
|
109
|
-
return props.maximumDays
|
|
110
|
-
? `Escolha o fim. Máximo de ${props.maximumDays} dias.`
|
|
111
|
-
: "Escolha o fim";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const endLabel = formatDateOnlyToPtBr(calendarDateToDateString(draft.end));
|
|
115
|
-
const days = inclusiveCalendarDays(draft.start, draft.end);
|
|
116
|
-
const dayLabel = days === 1 ? "dia" : "dias";
|
|
117
|
-
return `${startLabel} – ${endLabel} · ${days} ${dayLabel}`;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const canApplyRange = computed(
|
|
121
|
-
() => Boolean(draftRange.value?.start && draftRange.value?.end),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
const rangeDisplayText = computed(() => {
|
|
125
|
-
const startLabel = start.value
|
|
126
|
-
? formatDateOnlyToPtBr(start.value)
|
|
127
|
-
: DATE_PLACEHOLDER;
|
|
128
|
-
const endLabel = end.value
|
|
129
|
-
? formatDateOnlyToPtBr(end.value)
|
|
130
|
-
: DATE_PLACEHOLDER;
|
|
131
|
-
|
|
132
|
-
if (!start.value && !end.value) {
|
|
133
|
-
return `${DATE_PLACEHOLDER}${RANGE_SEPARATOR}${DATE_PLACEHOLDER}`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return `${startLabel}${RANGE_SEPARATOR}${endLabel}`;
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const popoverReference = computed(() => {
|
|
140
|
-
const input = inputDateRef.value;
|
|
141
|
-
const segments = input?.inputsRef;
|
|
142
|
-
if (!segments?.length) return undefined;
|
|
143
|
-
return segments[segments.length - 1]?.$el;
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const rangeFieldClass = computed(() => [
|
|
147
|
-
"group relative inline-flex w-full cursor-pointer items-center text-left transition-colors",
|
|
148
|
-
"h-[var(--ads-date-height)] rounded-[var(--ads-date-radius)] px-[var(--ads-date-padding-x)] text-[length:var(--ads-date-font-size)] [font-weight:var(--ads-date-weight)] leading-[var(--ads-date-line-height)] text-[var(--ads-date-text)] bg-[var(--ads-date-background)] ring ring-inset ring-[var(--ads-date-border)]",
|
|
149
|
-
"enabled:hover:ring-[var(--ads-date-hover-border)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ads-date-focus-border)]",
|
|
150
|
-
"disabled:cursor-not-allowed disabled:opacity-75",
|
|
151
|
-
props.class,
|
|
152
|
-
]);
|
|
153
|
-
|
|
154
|
-
const popoverUi = { content: "w-auto max-w-none p-0" };
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Grade do range no Figma (5712:6691): mês com 258px, 7 colunas de 35,14px e 2px entre elas,
|
|
158
|
-
* linhas de 36px (semana + 5 semanas = 216px) e dia de 32px de altura.
|
|
159
|
-
*/
|
|
160
|
-
const rangeCalendarUi = {
|
|
161
|
-
body: "flex-row gap-14 space-x-0 space-y-0 pt-0 sm:space-x-0",
|
|
162
|
-
grid: "w-[258px] space-y-0",
|
|
163
|
-
gridWeekDaysRow: "mb-0 h-9 grid-cols-7 items-center gap-x-0.5",
|
|
164
|
-
gridBody: "grid",
|
|
165
|
-
gridRow: "h-9 grid-cols-7 items-center gap-x-0.5",
|
|
166
|
-
cell: "flex h-9 w-full items-center justify-self-stretch",
|
|
167
|
-
cellTrigger: "m-0 h-8 w-full",
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
/** Intl em pt-BR devolve o mês em minúscula ("outubro de 2026"). Só a primeira letra muda; o ano fica igual. */
|
|
171
|
-
function capitalizeFirst(value: string) {
|
|
172
|
-
return value ? value.charAt(0).toLocaleUpperCase(UI_LOCALE_CODE) + value.slice(1) : value;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Cabeçalho do mês no Figma (_Date Picker Header, 5709:19265): Inter Bold 14, linha 130%, text/primary. */
|
|
176
|
-
const singleCalendarUi = {
|
|
177
|
-
heading: "text-sm font-bold leading-[1.3] text-[var(--ads-text-primary)]",
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
function monthTitle(date: CalendarDate) {
|
|
181
|
-
return calendarMonthTitle(date, UI_LOCALE_CODE);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function onRangePlaceholderUpdate(value: CalendarDate) {
|
|
185
|
-
const next = visibleMonthAfterPlaceholder(leftPlaceholder.value, value);
|
|
186
|
-
if (next.compare(leftPlaceholder.value) !== 0) {
|
|
187
|
-
leftPlaceholder.value = next;
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (isEqualMonth(value, leftPlaceholder.value) && value.day === 1) return;
|
|
191
|
-
leftPlaceholder.value = leftPlaceholder.value.copy();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function moveVisibleMonth(delta: -1 | 1) {
|
|
195
|
-
const next = shiftVisibleMonth(
|
|
196
|
-
leftPlaceholder.value,
|
|
197
|
-
delta,
|
|
198
|
-
minCalendarDate.value,
|
|
199
|
-
navigationMax.value,
|
|
200
|
-
);
|
|
201
|
-
if (next.compare(leftPlaceholder.value) === 0) return;
|
|
202
|
-
leftPlaceholder.value = next;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function weekdayLetter(day: string) {
|
|
206
|
-
return day.replace(".", "").charAt(0).toUpperCase();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function applyExternalModels() {
|
|
210
|
-
if (syncingFromCalendar.value || props.range) return;
|
|
211
|
-
|
|
212
|
-
const next = dateStringToCalendarDate(modelValue.value);
|
|
213
|
-
if (!calendarRangesEqual(calendarModel.value, next)) {
|
|
214
|
-
calendarModel.value = next;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function onCalendarModelUpdate(value: CalendarModel) {
|
|
219
|
-
if (calendarRangesEqual(calendarModel.value, value)) return;
|
|
220
|
-
|
|
221
|
-
syncingFromCalendar.value = true;
|
|
222
|
-
calendarModel.value = value;
|
|
223
|
-
|
|
224
|
-
if (!props.range && value && !isCalendarDateRange(value)) {
|
|
225
|
-
modelValue.value = calendarDateToDateString(value);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
nextTick(() => {
|
|
229
|
-
syncingFromCalendar.value = false;
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function resetDraftFromCommitted() {
|
|
234
|
-
draftRange.value = dateStringsToCalendarRange(start.value, end.value);
|
|
235
|
-
const anchor =
|
|
236
|
-
draftRange.value?.start ?? dateStringToCalendarDate(getTodayDateString())!;
|
|
237
|
-
leftPlaceholder.value = anchor.set({ day: 1 });
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function onDraftUpdate(value: CalendarDateRangeValue | undefined) {
|
|
241
|
-
if (ignoreDraftUpdates.value) return;
|
|
242
|
-
draftRange.value = value;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function clearDraft() {
|
|
246
|
-
// Os dois calendários guardam o intervalo por conta própria. Remontar
|
|
247
|
-
// descarta o destaque; o flag ignora o eco desse estado ao desmontar.
|
|
248
|
-
ignoreDraftUpdates.value = true;
|
|
249
|
-
draftRange.value = undefined;
|
|
250
|
-
start.value = "";
|
|
251
|
-
end.value = "";
|
|
252
|
-
rangeCalendarKey.value += 1;
|
|
253
|
-
nextTick(() => {
|
|
254
|
-
draftRange.value = undefined;
|
|
255
|
-
nextTick(() => {
|
|
256
|
-
ignoreDraftUpdates.value = false;
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function applyDraft(close: () => void) {
|
|
262
|
-
if (!draftRange.value?.start || !draftRange.value.end) return;
|
|
263
|
-
const next = calendarRangeToDateStrings({
|
|
264
|
-
start: draftRange.value.start,
|
|
265
|
-
end: draftRange.value.end,
|
|
266
|
-
});
|
|
267
|
-
start.value = next.start;
|
|
268
|
-
end.value = next.end;
|
|
269
|
-
close();
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function cancelDraft(close: () => void) {
|
|
273
|
-
start.value = rangeSessionStart.value;
|
|
274
|
-
end.value = rangeSessionEnd.value;
|
|
275
|
-
resetDraftFromCommitted();
|
|
276
|
-
close();
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
watch(rangeOpen, (isOpen) => {
|
|
280
|
-
if (!isOpen) return;
|
|
281
|
-
rangeSessionStart.value = start.value;
|
|
282
|
-
rangeSessionEnd.value = end.value;
|
|
283
|
-
resetDraftFromCommitted();
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
watch([modelValue, start, end, () => props.range], applyExternalModels, {
|
|
287
|
-
immediate: true,
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
function isDateUnavailable(date: CalendarDate) {
|
|
291
|
-
if (minCalendarDate.value && date.compare(minCalendarDate.value) < 0) {
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
if (maxCalendarDate.value && date.compare(maxCalendarDate.value) > 0) {
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
if (!props.range || !props.maximumDays) return false;
|
|
298
|
-
const draft = draftRange.value;
|
|
299
|
-
if (!draft?.start || draft.end) return false;
|
|
300
|
-
return isOutsideMaximumWindow(date, draft.start, props.maximumDays);
|
|
301
|
-
}
|
|
302
|
-
</script>
|
|
303
|
-
|
|
304
|
-
<template>
|
|
305
|
-
<UPopover
|
|
306
|
-
v-if="range"
|
|
307
|
-
v-model:open="rangeOpen"
|
|
308
|
-
:content="{ align: 'start', side: 'bottom' }"
|
|
309
|
-
:ui="popoverUi"
|
|
310
|
-
>
|
|
311
|
-
<button
|
|
312
|
-
type="button"
|
|
313
|
-
:disabled="disabled || readonly"
|
|
314
|
-
:aria-label="`Selecionar período. ${rangeDisplayText}`"
|
|
315
|
-
:class="rangeFieldClass"
|
|
316
|
-
>
|
|
317
|
-
<span
|
|
318
|
-
class="min-w-0 flex-1 truncate tabular-nums"
|
|
319
|
-
:class="start || end ? 'text-[var(--ads-date-text)]' : 'text-[var(--ads-date-placeholder)]'"
|
|
320
|
-
>
|
|
321
|
-
{{ rangeDisplayText }}
|
|
322
|
-
</span>
|
|
323
|
-
<UIcon name="i-lucide-calendar" class="size-5 shrink-0 text-[var(--ads-date-placeholder)]" />
|
|
324
|
-
</button>
|
|
325
|
-
|
|
326
|
-
<template #content="{ close }">
|
|
327
|
-
<div class="ads-date-range-panel max-w-[calc(100vw-2rem)] overflow-x-auto bg-white">
|
|
328
|
-
<p class="px-4 pt-4 text-sm font-medium text-neutral-900" aria-live="polite">
|
|
329
|
-
{{ rangeStepLabel }}
|
|
330
|
-
</p>
|
|
331
|
-
<!-- Figma Range Date Picker (5712:6691): dois painéis de 290×284, 24px entre eles. -->
|
|
332
|
-
<div class="relative w-[604px]">
|
|
333
|
-
<div class="pointer-events-none absolute inset-0 grid grid-cols-[290px_290px] gap-6" aria-hidden="true">
|
|
334
|
-
<div class="rounded-s-[var(--ads-radius-xsm)] bg-[var(--ads-surface-card)]" />
|
|
335
|
-
<div class="rounded-e-[var(--ads-radius-xsm)] bg-[var(--ads-surface-card)]" />
|
|
336
|
-
</div>
|
|
337
|
-
<div class="relative flex flex-col gap-3 p-4">
|
|
338
|
-
<div class="grid grid-cols-[258px_258px] gap-x-14">
|
|
339
|
-
<div class="flex h-6 items-center justify-between">
|
|
340
|
-
<button
|
|
341
|
-
type="button"
|
|
342
|
-
class="ads-date-range-nav"
|
|
343
|
-
aria-label="Mês anterior"
|
|
344
|
-
:disabled="!canShiftToPreviousMonth"
|
|
345
|
-
@click="moveVisibleMonth(-1)"
|
|
346
|
-
>
|
|
347
|
-
<UIcon name="i-lucide-chevron-left" class="size-6" />
|
|
348
|
-
</button>
|
|
349
|
-
<span class="ads-date-range-title">{{ monthTitle(leftMonth) }}</span>
|
|
350
|
-
<span class="size-6 shrink-0" aria-hidden="true" />
|
|
351
|
-
</div>
|
|
352
|
-
<div class="flex h-6 items-center justify-between">
|
|
353
|
-
<span class="size-6 shrink-0" aria-hidden="true" />
|
|
354
|
-
<span class="ads-date-range-title">{{ monthTitle(rightMonth) }}</span>
|
|
355
|
-
<button
|
|
356
|
-
type="button"
|
|
357
|
-
class="ads-date-range-nav"
|
|
358
|
-
aria-label="Próximo mês"
|
|
359
|
-
:disabled="!canShiftToNextMonth"
|
|
360
|
-
@click="moveVisibleMonth(1)"
|
|
361
|
-
>
|
|
362
|
-
<UIcon name="i-lucide-chevron-right" class="size-6" />
|
|
363
|
-
</button>
|
|
364
|
-
</div>
|
|
365
|
-
</div>
|
|
366
|
-
<UCalendar
|
|
367
|
-
:key="`range-${rangeCalendarKey}`"
|
|
368
|
-
:model-value="draftRange"
|
|
369
|
-
:placeholder="leftMonth"
|
|
370
|
-
:locale="UI_LOCALE_CODE"
|
|
371
|
-
range
|
|
372
|
-
:number-of-months="2"
|
|
373
|
-
:month-controls="false"
|
|
374
|
-
:year-controls="false"
|
|
375
|
-
weekday-format="narrow"
|
|
376
|
-
:week-starts-on="0"
|
|
377
|
-
:min-value="minCalendarDate"
|
|
378
|
-
:max-value="maxCalendarDate"
|
|
379
|
-
disable-days-outside-current-view
|
|
380
|
-
:is-date-unavailable="isDateUnavailable"
|
|
381
|
-
color="neutral"
|
|
382
|
-
variant="outline"
|
|
383
|
-
class="ads-date-picker"
|
|
384
|
-
:ui="rangeCalendarUi"
|
|
385
|
-
@update:model-value="onDraftUpdate"
|
|
386
|
-
@update:placeholder="onRangePlaceholderUpdate"
|
|
387
|
-
>
|
|
388
|
-
<template #week-day="{ day }">
|
|
389
|
-
{{ weekdayLetter(day) }}
|
|
390
|
-
</template>
|
|
391
|
-
</UCalendar>
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
|
-
<div class="flex items-center justify-between gap-3 px-4 pb-4">
|
|
396
|
-
<AdsButton
|
|
397
|
-
type="unfilled"
|
|
398
|
-
size="sm"
|
|
399
|
-
leading
|
|
400
|
-
icon="i-lucide-x"
|
|
401
|
-
label="Limpar seleção"
|
|
402
|
-
@click="clearDraft"
|
|
403
|
-
/>
|
|
404
|
-
<div class="flex items-center gap-2">
|
|
405
|
-
<AdsButton
|
|
406
|
-
type="unfilled"
|
|
407
|
-
label="Cancelar"
|
|
408
|
-
@click="cancelDraft(close)"
|
|
409
|
-
/>
|
|
410
|
-
<AdsButton
|
|
411
|
-
label="Aplicar"
|
|
412
|
-
:disabled="!canApplyRange"
|
|
413
|
-
@click="applyDraft(close)"
|
|
414
|
-
/>
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
417
|
-
</div>
|
|
418
|
-
</template>
|
|
419
|
-
</UPopover>
|
|
420
|
-
|
|
421
|
-
<UInputDate
|
|
422
|
-
v-else
|
|
423
|
-
ref="inputDateRef"
|
|
424
|
-
:model-value="calendarModel"
|
|
425
|
-
:locale="UI_LOCALE_CODE"
|
|
426
|
-
:size="size"
|
|
427
|
-
:disabled="disabled"
|
|
428
|
-
:readonly="readonly"
|
|
429
|
-
:min-value="minCalendarDate"
|
|
430
|
-
:max-value="maxCalendarDate"
|
|
431
|
-
:is-date-unavailable="isDateUnavailable"
|
|
432
|
-
trailing
|
|
433
|
-
:class="props.class"
|
|
434
|
-
@update:model-value="onCalendarModelUpdate"
|
|
435
|
-
>
|
|
436
|
-
<template #trailing>
|
|
437
|
-
<UPopover
|
|
438
|
-
:reference="popoverReference"
|
|
439
|
-
:content="{ align: 'end', side: 'bottom' }"
|
|
440
|
-
:ui="popoverUi"
|
|
441
|
-
>
|
|
442
|
-
<AdsButton
|
|
443
|
-
type="unfilled"
|
|
444
|
-
size="sm"
|
|
445
|
-
leading
|
|
446
|
-
icon="i-lucide-calendar"
|
|
447
|
-
aria-label="Selecionar data"
|
|
448
|
-
class="px-0"
|
|
449
|
-
:disabled="disabled || readonly"
|
|
450
|
-
/>
|
|
451
|
-
|
|
452
|
-
<template #content>
|
|
453
|
-
<UCalendar
|
|
454
|
-
:model-value="calendarModel"
|
|
455
|
-
:locale="UI_LOCALE_CODE"
|
|
456
|
-
:min-value="minCalendarDate"
|
|
457
|
-
:max-value="maxCalendarDate"
|
|
458
|
-
:is-date-unavailable="isDateUnavailable"
|
|
459
|
-
color="primary"
|
|
460
|
-
variant="solid"
|
|
461
|
-
class="ads-date-picker p-2"
|
|
462
|
-
:ui="singleCalendarUi"
|
|
463
|
-
@update:model-value="onCalendarModelUpdate"
|
|
464
|
-
>
|
|
465
|
-
<template #heading="{ value }">
|
|
466
|
-
{{ capitalizeFirst(value) }}
|
|
467
|
-
</template>
|
|
468
|
-
</UCalendar>
|
|
469
|
-
</template>
|
|
470
|
-
</UPopover>
|
|
471
|
-
</template>
|
|
472
|
-
</UInputDate>
|
|
473
|
-
</template>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { isEqualMonth, type CalendarDate } from "@internationalized/date";
|
|
3
|
+
import { computed, nextTick, ref, shallowRef, watch } from "vue";
|
|
4
|
+
import AdsButton from "./AdsButton.vue";
|
|
5
|
+
import { UI_LOCALE_CODE } from "../locale/pt-br";
|
|
6
|
+
import { formatDateOnlyToPtBr, getTodayDateString } from "../utils/parser";
|
|
7
|
+
import {
|
|
8
|
+
calendarDateToDateString,
|
|
9
|
+
calendarMonthTitle,
|
|
10
|
+
calendarRangeToDateStrings,
|
|
11
|
+
calendarRangesEqual,
|
|
12
|
+
dateStringToCalendarDate,
|
|
13
|
+
dateStringsToCalendarRange,
|
|
14
|
+
inclusiveCalendarDays,
|
|
15
|
+
isCalendarDateRange,
|
|
16
|
+
isOutsideMaximumWindow,
|
|
17
|
+
shiftVisibleMonth,
|
|
18
|
+
visibleMonthAfterPlaceholder,
|
|
19
|
+
type CalendarDateRangeValue,
|
|
20
|
+
} from "../utils/calendar-date";
|
|
21
|
+
|
|
22
|
+
const DATE_PLACEHOLDER = "dd/mm/aaaa";
|
|
23
|
+
const RANGE_SEPARATOR = " – ";
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(
|
|
26
|
+
defineProps<{
|
|
27
|
+
range?: boolean;
|
|
28
|
+
min?: string;
|
|
29
|
+
max?: string;
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
readonly?: boolean;
|
|
32
|
+
size?: "sm" | "md" | "lg";
|
|
33
|
+
maximumDays?: number;
|
|
34
|
+
class?: string;
|
|
35
|
+
}>(),
|
|
36
|
+
{
|
|
37
|
+
range: false,
|
|
38
|
+
size: "md",
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const modelValue = defineModel<string>({ default: "" });
|
|
43
|
+
const start = defineModel<string>("start", { default: "" });
|
|
44
|
+
const end = defineModel<string>("end", { default: "" });
|
|
45
|
+
|
|
46
|
+
const inputDateRef = useTemplateRef("inputDateRef");
|
|
47
|
+
const syncingFromCalendar = ref(false);
|
|
48
|
+
const rangeOpen = ref(false);
|
|
49
|
+
const rangeCalendarKey = ref(0);
|
|
50
|
+
const ignoreDraftUpdates = ref(false);
|
|
51
|
+
const rangeSessionStart = ref("");
|
|
52
|
+
const rangeSessionEnd = ref("");
|
|
53
|
+
|
|
54
|
+
type CalendarModel = CalendarDate | CalendarDateRangeValue | undefined;
|
|
55
|
+
|
|
56
|
+
const calendarModel = shallowRef<CalendarModel>();
|
|
57
|
+
const draftRange = shallowRef<CalendarDateRangeValue | undefined>();
|
|
58
|
+
const leftPlaceholder = shallowRef<CalendarDate>(
|
|
59
|
+
dateStringToCalendarDate(getTodayDateString())!.set({ day: 1 }),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const minCalendarDate = computed(() =>
|
|
63
|
+
props.min ? dateStringToCalendarDate(props.min) : undefined,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const maxCalendarDate = computed(() =>
|
|
67
|
+
props.max ? dateStringToCalendarDate(props.max) : undefined,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const leftMonth = computed(() => leftPlaceholder.value.set({ day: 1 }));
|
|
71
|
+
const rightMonth = computed(() => leftMonth.value.add({ months: 1 }));
|
|
72
|
+
|
|
73
|
+
const navigationMax = computed(() => {
|
|
74
|
+
const hardMax = maxCalendarDate.value;
|
|
75
|
+
const draft = draftRange.value;
|
|
76
|
+
if (!props.maximumDays || !draft?.start || draft.end) return hardMax;
|
|
77
|
+
|
|
78
|
+
const windowEnd = draft.start.add({ days: props.maximumDays - 1 });
|
|
79
|
+
if (!hardMax || windowEnd.compare(hardMax) < 0) return windowEnd;
|
|
80
|
+
return hardMax;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const canShiftToPreviousMonth = computed(
|
|
84
|
+
() =>
|
|
85
|
+
shiftVisibleMonth(
|
|
86
|
+
leftPlaceholder.value,
|
|
87
|
+
-1,
|
|
88
|
+
minCalendarDate.value,
|
|
89
|
+
navigationMax.value,
|
|
90
|
+
).compare(leftMonth.value) !== 0,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const canShiftToNextMonth = computed(
|
|
94
|
+
() =>
|
|
95
|
+
shiftVisibleMonth(
|
|
96
|
+
leftPlaceholder.value,
|
|
97
|
+
1,
|
|
98
|
+
minCalendarDate.value,
|
|
99
|
+
navigationMax.value,
|
|
100
|
+
).compare(leftMonth.value) !== 0,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const rangeStepLabel = computed(() => {
|
|
104
|
+
const draft = draftRange.value;
|
|
105
|
+
if (!draft?.start) return "Escolha o início";
|
|
106
|
+
|
|
107
|
+
const startLabel = formatDateOnlyToPtBr(calendarDateToDateString(draft.start));
|
|
108
|
+
if (!draft.end) {
|
|
109
|
+
return props.maximumDays
|
|
110
|
+
? `Escolha o fim. Máximo de ${props.maximumDays} dias.`
|
|
111
|
+
: "Escolha o fim";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const endLabel = formatDateOnlyToPtBr(calendarDateToDateString(draft.end));
|
|
115
|
+
const days = inclusiveCalendarDays(draft.start, draft.end);
|
|
116
|
+
const dayLabel = days === 1 ? "dia" : "dias";
|
|
117
|
+
return `${startLabel} – ${endLabel} · ${days} ${dayLabel}`;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const canApplyRange = computed(
|
|
121
|
+
() => Boolean(draftRange.value?.start && draftRange.value?.end),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const rangeDisplayText = computed(() => {
|
|
125
|
+
const startLabel = start.value
|
|
126
|
+
? formatDateOnlyToPtBr(start.value)
|
|
127
|
+
: DATE_PLACEHOLDER;
|
|
128
|
+
const endLabel = end.value
|
|
129
|
+
? formatDateOnlyToPtBr(end.value)
|
|
130
|
+
: DATE_PLACEHOLDER;
|
|
131
|
+
|
|
132
|
+
if (!start.value && !end.value) {
|
|
133
|
+
return `${DATE_PLACEHOLDER}${RANGE_SEPARATOR}${DATE_PLACEHOLDER}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return `${startLabel}${RANGE_SEPARATOR}${endLabel}`;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const popoverReference = computed(() => {
|
|
140
|
+
const input = inputDateRef.value;
|
|
141
|
+
const segments = input?.inputsRef;
|
|
142
|
+
if (!segments?.length) return undefined;
|
|
143
|
+
return segments[segments.length - 1]?.$el;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const rangeFieldClass = computed(() => [
|
|
147
|
+
"group relative inline-flex w-full cursor-pointer items-center text-left transition-colors",
|
|
148
|
+
"h-[var(--ads-date-height)] rounded-[var(--ads-date-radius)] px-[var(--ads-date-padding-x)] text-[length:var(--ads-date-font-size)] [font-weight:var(--ads-date-weight)] leading-[var(--ads-date-line-height)] text-[var(--ads-date-text)] bg-[var(--ads-date-background)] ring ring-inset ring-[var(--ads-date-border)]",
|
|
149
|
+
"enabled:hover:ring-[var(--ads-date-hover-border)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--ads-date-focus-border)]",
|
|
150
|
+
"disabled:cursor-not-allowed disabled:opacity-75",
|
|
151
|
+
props.class,
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const popoverUi = { content: "w-auto max-w-none p-0" };
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Grade do range no Figma (5712:6691): mês com 258px, 7 colunas de 35,14px e 2px entre elas,
|
|
158
|
+
* linhas de 36px (semana + 5 semanas = 216px) e dia de 32px de altura.
|
|
159
|
+
*/
|
|
160
|
+
const rangeCalendarUi = {
|
|
161
|
+
body: "flex-row gap-14 space-x-0 space-y-0 pt-0 sm:space-x-0",
|
|
162
|
+
grid: "w-[258px] space-y-0",
|
|
163
|
+
gridWeekDaysRow: "mb-0 h-9 grid-cols-7 items-center gap-x-0.5",
|
|
164
|
+
gridBody: "grid",
|
|
165
|
+
gridRow: "h-9 grid-cols-7 items-center gap-x-0.5",
|
|
166
|
+
cell: "flex h-9 w-full items-center justify-self-stretch",
|
|
167
|
+
cellTrigger: "m-0 h-8 w-full",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** Intl em pt-BR devolve o mês em minúscula ("outubro de 2026"). Só a primeira letra muda; o ano fica igual. */
|
|
171
|
+
function capitalizeFirst(value: string) {
|
|
172
|
+
return value ? value.charAt(0).toLocaleUpperCase(UI_LOCALE_CODE) + value.slice(1) : value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Cabeçalho do mês no Figma (_Date Picker Header, 5709:19265): Inter Bold 14, linha 130%, text/primary. */
|
|
176
|
+
const singleCalendarUi = {
|
|
177
|
+
heading: "text-sm font-bold leading-[1.3] text-[var(--ads-text-primary)]",
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
function monthTitle(date: CalendarDate) {
|
|
181
|
+
return calendarMonthTitle(date, UI_LOCALE_CODE);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function onRangePlaceholderUpdate(value: CalendarDate) {
|
|
185
|
+
const next = visibleMonthAfterPlaceholder(leftPlaceholder.value, value);
|
|
186
|
+
if (next.compare(leftPlaceholder.value) !== 0) {
|
|
187
|
+
leftPlaceholder.value = next;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (isEqualMonth(value, leftPlaceholder.value) && value.day === 1) return;
|
|
191
|
+
leftPlaceholder.value = leftPlaceholder.value.copy();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function moveVisibleMonth(delta: -1 | 1) {
|
|
195
|
+
const next = shiftVisibleMonth(
|
|
196
|
+
leftPlaceholder.value,
|
|
197
|
+
delta,
|
|
198
|
+
minCalendarDate.value,
|
|
199
|
+
navigationMax.value,
|
|
200
|
+
);
|
|
201
|
+
if (next.compare(leftPlaceholder.value) === 0) return;
|
|
202
|
+
leftPlaceholder.value = next;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function weekdayLetter(day: string) {
|
|
206
|
+
return day.replace(".", "").charAt(0).toUpperCase();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function applyExternalModels() {
|
|
210
|
+
if (syncingFromCalendar.value || props.range) return;
|
|
211
|
+
|
|
212
|
+
const next = dateStringToCalendarDate(modelValue.value);
|
|
213
|
+
if (!calendarRangesEqual(calendarModel.value, next)) {
|
|
214
|
+
calendarModel.value = next;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function onCalendarModelUpdate(value: CalendarModel) {
|
|
219
|
+
if (calendarRangesEqual(calendarModel.value, value)) return;
|
|
220
|
+
|
|
221
|
+
syncingFromCalendar.value = true;
|
|
222
|
+
calendarModel.value = value;
|
|
223
|
+
|
|
224
|
+
if (!props.range && value && !isCalendarDateRange(value)) {
|
|
225
|
+
modelValue.value = calendarDateToDateString(value);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
nextTick(() => {
|
|
229
|
+
syncingFromCalendar.value = false;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resetDraftFromCommitted() {
|
|
234
|
+
draftRange.value = dateStringsToCalendarRange(start.value, end.value);
|
|
235
|
+
const anchor =
|
|
236
|
+
draftRange.value?.start ?? dateStringToCalendarDate(getTodayDateString())!;
|
|
237
|
+
leftPlaceholder.value = anchor.set({ day: 1 });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function onDraftUpdate(value: CalendarDateRangeValue | undefined) {
|
|
241
|
+
if (ignoreDraftUpdates.value) return;
|
|
242
|
+
draftRange.value = value;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function clearDraft() {
|
|
246
|
+
// Os dois calendários guardam o intervalo por conta própria. Remontar
|
|
247
|
+
// descarta o destaque; o flag ignora o eco desse estado ao desmontar.
|
|
248
|
+
ignoreDraftUpdates.value = true;
|
|
249
|
+
draftRange.value = undefined;
|
|
250
|
+
start.value = "";
|
|
251
|
+
end.value = "";
|
|
252
|
+
rangeCalendarKey.value += 1;
|
|
253
|
+
nextTick(() => {
|
|
254
|
+
draftRange.value = undefined;
|
|
255
|
+
nextTick(() => {
|
|
256
|
+
ignoreDraftUpdates.value = false;
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function applyDraft(close: () => void) {
|
|
262
|
+
if (!draftRange.value?.start || !draftRange.value.end) return;
|
|
263
|
+
const next = calendarRangeToDateStrings({
|
|
264
|
+
start: draftRange.value.start,
|
|
265
|
+
end: draftRange.value.end,
|
|
266
|
+
});
|
|
267
|
+
start.value = next.start;
|
|
268
|
+
end.value = next.end;
|
|
269
|
+
close();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cancelDraft(close: () => void) {
|
|
273
|
+
start.value = rangeSessionStart.value;
|
|
274
|
+
end.value = rangeSessionEnd.value;
|
|
275
|
+
resetDraftFromCommitted();
|
|
276
|
+
close();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
watch(rangeOpen, (isOpen) => {
|
|
280
|
+
if (!isOpen) return;
|
|
281
|
+
rangeSessionStart.value = start.value;
|
|
282
|
+
rangeSessionEnd.value = end.value;
|
|
283
|
+
resetDraftFromCommitted();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
watch([modelValue, start, end, () => props.range], applyExternalModels, {
|
|
287
|
+
immediate: true,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
function isDateUnavailable(date: CalendarDate) {
|
|
291
|
+
if (minCalendarDate.value && date.compare(minCalendarDate.value) < 0) {
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
if (maxCalendarDate.value && date.compare(maxCalendarDate.value) > 0) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (!props.range || !props.maximumDays) return false;
|
|
298
|
+
const draft = draftRange.value;
|
|
299
|
+
if (!draft?.start || draft.end) return false;
|
|
300
|
+
return isOutsideMaximumWindow(date, draft.start, props.maximumDays);
|
|
301
|
+
}
|
|
302
|
+
</script>
|
|
303
|
+
|
|
304
|
+
<template>
|
|
305
|
+
<UPopover
|
|
306
|
+
v-if="range"
|
|
307
|
+
v-model:open="rangeOpen"
|
|
308
|
+
:content="{ align: 'start', side: 'bottom' }"
|
|
309
|
+
:ui="popoverUi"
|
|
310
|
+
>
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
:disabled="disabled || readonly"
|
|
314
|
+
:aria-label="`Selecionar período. ${rangeDisplayText}`"
|
|
315
|
+
:class="rangeFieldClass"
|
|
316
|
+
>
|
|
317
|
+
<span
|
|
318
|
+
class="min-w-0 flex-1 truncate tabular-nums"
|
|
319
|
+
:class="start || end ? 'text-[var(--ads-date-text)]' : 'text-[var(--ads-date-placeholder)]'"
|
|
320
|
+
>
|
|
321
|
+
{{ rangeDisplayText }}
|
|
322
|
+
</span>
|
|
323
|
+
<UIcon name="i-lucide-calendar" class="size-5 shrink-0 text-[var(--ads-date-placeholder)]" />
|
|
324
|
+
</button>
|
|
325
|
+
|
|
326
|
+
<template #content="{ close }">
|
|
327
|
+
<div class="ads-date-range-panel max-w-[calc(100vw-2rem)] overflow-x-auto bg-white">
|
|
328
|
+
<p class="px-4 pt-4 text-sm font-medium text-neutral-900" aria-live="polite">
|
|
329
|
+
{{ rangeStepLabel }}
|
|
330
|
+
</p>
|
|
331
|
+
<!-- Figma Range Date Picker (5712:6691): dois painéis de 290×284, 24px entre eles. -->
|
|
332
|
+
<div class="relative w-[604px]">
|
|
333
|
+
<div class="pointer-events-none absolute inset-0 grid grid-cols-[290px_290px] gap-6" aria-hidden="true">
|
|
334
|
+
<div class="rounded-s-[var(--ads-radius-xsm)] bg-[var(--ads-surface-card)]" />
|
|
335
|
+
<div class="rounded-e-[var(--ads-radius-xsm)] bg-[var(--ads-surface-card)]" />
|
|
336
|
+
</div>
|
|
337
|
+
<div class="relative flex flex-col gap-3 p-4">
|
|
338
|
+
<div class="grid grid-cols-[258px_258px] gap-x-14">
|
|
339
|
+
<div class="flex h-6 items-center justify-between">
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
class="ads-date-range-nav"
|
|
343
|
+
aria-label="Mês anterior"
|
|
344
|
+
:disabled="!canShiftToPreviousMonth"
|
|
345
|
+
@click="moveVisibleMonth(-1)"
|
|
346
|
+
>
|
|
347
|
+
<UIcon name="i-lucide-chevron-left" class="size-6" />
|
|
348
|
+
</button>
|
|
349
|
+
<span class="ads-date-range-title">{{ monthTitle(leftMonth) }}</span>
|
|
350
|
+
<span class="size-6 shrink-0" aria-hidden="true" />
|
|
351
|
+
</div>
|
|
352
|
+
<div class="flex h-6 items-center justify-between">
|
|
353
|
+
<span class="size-6 shrink-0" aria-hidden="true" />
|
|
354
|
+
<span class="ads-date-range-title">{{ monthTitle(rightMonth) }}</span>
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
class="ads-date-range-nav"
|
|
358
|
+
aria-label="Próximo mês"
|
|
359
|
+
:disabled="!canShiftToNextMonth"
|
|
360
|
+
@click="moveVisibleMonth(1)"
|
|
361
|
+
>
|
|
362
|
+
<UIcon name="i-lucide-chevron-right" class="size-6" />
|
|
363
|
+
</button>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
<UCalendar
|
|
367
|
+
:key="`range-${rangeCalendarKey}`"
|
|
368
|
+
:model-value="draftRange"
|
|
369
|
+
:placeholder="leftMonth"
|
|
370
|
+
:locale="UI_LOCALE_CODE"
|
|
371
|
+
range
|
|
372
|
+
:number-of-months="2"
|
|
373
|
+
:month-controls="false"
|
|
374
|
+
:year-controls="false"
|
|
375
|
+
weekday-format="narrow"
|
|
376
|
+
:week-starts-on="0"
|
|
377
|
+
:min-value="minCalendarDate"
|
|
378
|
+
:max-value="maxCalendarDate"
|
|
379
|
+
disable-days-outside-current-view
|
|
380
|
+
:is-date-unavailable="isDateUnavailable"
|
|
381
|
+
color="neutral"
|
|
382
|
+
variant="outline"
|
|
383
|
+
class="ads-date-picker"
|
|
384
|
+
:ui="rangeCalendarUi"
|
|
385
|
+
@update:model-value="onDraftUpdate"
|
|
386
|
+
@update:placeholder="onRangePlaceholderUpdate"
|
|
387
|
+
>
|
|
388
|
+
<template #week-day="{ day }">
|
|
389
|
+
{{ weekdayLetter(day) }}
|
|
390
|
+
</template>
|
|
391
|
+
</UCalendar>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div class="flex items-center justify-between gap-3 px-4 pb-4">
|
|
396
|
+
<AdsButton
|
|
397
|
+
type="unfilled"
|
|
398
|
+
size="sm"
|
|
399
|
+
leading
|
|
400
|
+
icon="i-lucide-x"
|
|
401
|
+
label="Limpar seleção"
|
|
402
|
+
@click="clearDraft"
|
|
403
|
+
/>
|
|
404
|
+
<div class="flex items-center gap-2">
|
|
405
|
+
<AdsButton
|
|
406
|
+
type="unfilled"
|
|
407
|
+
label="Cancelar"
|
|
408
|
+
@click="cancelDraft(close)"
|
|
409
|
+
/>
|
|
410
|
+
<AdsButton
|
|
411
|
+
label="Aplicar"
|
|
412
|
+
:disabled="!canApplyRange"
|
|
413
|
+
@click="applyDraft(close)"
|
|
414
|
+
/>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</template>
|
|
419
|
+
</UPopover>
|
|
420
|
+
|
|
421
|
+
<UInputDate
|
|
422
|
+
v-else
|
|
423
|
+
ref="inputDateRef"
|
|
424
|
+
:model-value="calendarModel"
|
|
425
|
+
:locale="UI_LOCALE_CODE"
|
|
426
|
+
:size="size"
|
|
427
|
+
:disabled="disabled"
|
|
428
|
+
:readonly="readonly"
|
|
429
|
+
:min-value="minCalendarDate"
|
|
430
|
+
:max-value="maxCalendarDate"
|
|
431
|
+
:is-date-unavailable="isDateUnavailable"
|
|
432
|
+
trailing
|
|
433
|
+
:class="props.class"
|
|
434
|
+
@update:model-value="onCalendarModelUpdate"
|
|
435
|
+
>
|
|
436
|
+
<template #trailing>
|
|
437
|
+
<UPopover
|
|
438
|
+
:reference="popoverReference"
|
|
439
|
+
:content="{ align: 'end', side: 'bottom' }"
|
|
440
|
+
:ui="popoverUi"
|
|
441
|
+
>
|
|
442
|
+
<AdsButton
|
|
443
|
+
type="unfilled"
|
|
444
|
+
size="sm"
|
|
445
|
+
leading
|
|
446
|
+
icon="i-lucide-calendar"
|
|
447
|
+
aria-label="Selecionar data"
|
|
448
|
+
class="px-0"
|
|
449
|
+
:disabled="disabled || readonly"
|
|
450
|
+
/>
|
|
451
|
+
|
|
452
|
+
<template #content>
|
|
453
|
+
<UCalendar
|
|
454
|
+
:model-value="calendarModel"
|
|
455
|
+
:locale="UI_LOCALE_CODE"
|
|
456
|
+
:min-value="minCalendarDate"
|
|
457
|
+
:max-value="maxCalendarDate"
|
|
458
|
+
:is-date-unavailable="isDateUnavailable"
|
|
459
|
+
color="primary"
|
|
460
|
+
variant="solid"
|
|
461
|
+
class="ads-date-picker p-2"
|
|
462
|
+
:ui="singleCalendarUi"
|
|
463
|
+
@update:model-value="onCalendarModelUpdate"
|
|
464
|
+
>
|
|
465
|
+
<template #heading="{ value }">
|
|
466
|
+
{{ capitalizeFirst(value) }}
|
|
467
|
+
</template>
|
|
468
|
+
</UCalendar>
|
|
469
|
+
</template>
|
|
470
|
+
</UPopover>
|
|
471
|
+
</template>
|
|
472
|
+
</UInputDate>
|
|
473
|
+
</template>
|