@consumidor-positivo/aurora 1.2.0 → 1.3.0

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.
Files changed (96) hide show
  1. package/README.md +59 -3
  2. package/astro/AdBox/index.astro +28 -0
  3. package/astro/Alert/index.astro +257 -0
  4. package/astro/BadgeInfo/index.astro +55 -0
  5. package/astro/BadgeState/index.astro +79 -0
  6. package/astro/Button/index.astro +11 -3
  7. package/astro/Card/Container/index.astro +44 -0
  8. package/astro/Card/Emphasis/index.astro +39 -0
  9. package/astro/Card/Image/index.astro +26 -0
  10. package/astro/Card/Root/index.astro +53 -0
  11. package/astro/Card/Tag/index.astro +26 -0
  12. package/astro/Chip/index.astro +48 -0
  13. package/astro/ChipBanner/index.astro +160 -0
  14. package/astro/Container/index.astro +16 -0
  15. package/astro/Divider/index.astro +27 -0
  16. package/astro/Image/index.astro +26 -0
  17. package/astro/LazyImage/index.astro +55 -0
  18. package/astro/LinkButton/index.astro +54 -0
  19. package/astro/Logo/ac/BadgeFullWhite.astro +20 -0
  20. package/astro/Logo/ac/BadgeWhite.astro +23 -0
  21. package/astro/Logo/ac/BadgeWhiteStroke.astro +27 -0
  22. package/astro/Logo/ac/BadgeWhiteStrokeDefault.astro +28 -0
  23. package/astro/Logo/ac/Badget.astro +24 -0
  24. package/astro/Logo/ac/Primary.astro +28 -0
  25. package/astro/Logo/ac/PrimaryFillWhite.astro +28 -0
  26. package/astro/Logo/ac/PrimaryFullWhite.astro +20 -0
  27. package/astro/Logo/ac/PrimaryNegative.astro +20 -0
  28. package/astro/Logo/ac/PrimaryWhite.astro +20 -0
  29. package/astro/Logo/ac/TertiaryWhite.astro +72 -0
  30. package/astro/Logo/cp/BadgeWhiteStroke.astro +10 -0
  31. package/astro/Logo/cp/Badget.astro +14 -0
  32. package/astro/Logo/cp/PrimaryFillWhite.astro +63 -0
  33. package/astro/Logo/cp/PrimaryFullWhite.astro +27 -0
  34. package/astro/Logo/cp/PrimaryLogoWhite.astro +67 -0
  35. package/astro/Logo/cp/PrimaryNegative.astro +27 -0
  36. package/astro/Logo/cp/PrimaryWhite.astro +27 -0
  37. package/astro/Modal/index.astro +213 -0
  38. package/astro/NotificationsBar/Link/index.astro +70 -0
  39. package/astro/NotificationsBar/List/index.astro +38 -0
  40. package/astro/NotificationsBar/index.astro +24 -0
  41. package/astro/PartnerBanner/index.astro +52 -0
  42. package/astro/ProfileNav/index.astro +34 -0
  43. package/astro/ProgressBar/index.astro +70 -0
  44. package/astro/Skeleton/index.astro +50 -0
  45. package/astro/SpecialButton/index.astro +331 -0
  46. package/astro/Spinner/index.astro +75 -0
  47. package/astro/SubHeader/index.astro +57 -0
  48. package/astro/Switch/Card/index.astro +69 -0
  49. package/astro/Switch/Pure/index.astro +47 -0
  50. package/astro/Tooltip/index.astro +38 -0
  51. package/astro/form/Checkbox/Field/index.astro +68 -0
  52. package/astro/form/Checkbox/Group/index.astro +79 -0
  53. package/astro/form/Datepicker/Calendar/index.astro +53 -0
  54. package/astro/form/Datepicker/CalendarHeader/index.astro +57 -0
  55. package/astro/form/Datepicker/Segment/index.astro +53 -0
  56. package/astro/form/Datepicker/index.astro +756 -0
  57. package/astro/form/EmailField/index.astro +237 -0
  58. package/astro/form/Field/ErrorMessage/index.astro +20 -0
  59. package/astro/form/Field/Input/index.astro +15 -0
  60. package/astro/form/Field/InputHolder/index.astro +28 -0
  61. package/astro/form/Field/Label/index.astro +80 -0
  62. package/astro/form/Field/Message/index.astro +29 -0
  63. package/astro/form/Field/Root/index.astro +37 -0
  64. package/astro/form/Field/TextArea/index.astro +69 -0
  65. package/astro/form/InputField/index.astro +96 -0
  66. package/astro/form/PasswordField/index.astro +123 -0
  67. package/astro/form/Radio/Field/index.astro +52 -0
  68. package/astro/form/Radio/Group/index.astro +78 -0
  69. package/astro/form/SelectField/index.astro +561 -0
  70. package/astro/form/TextareaField/index.astro +81 -0
  71. package/astro/form/TokenField/index.astro +211 -0
  72. package/astro/tokens/package.json +6 -0
  73. package/dist/{tokens-C4ypB4bY.js → astro/tokens/index.es.js} +101 -101
  74. package/dist/astro/tokens/index.es.js.map +1 -0
  75. package/dist/components/Alert/index.es.js +1 -1
  76. package/dist/components/BadgeInfo/index.es.js +1 -1
  77. package/dist/components/BadgeState/index.es.js +1 -1
  78. package/dist/components/Image/index.es.js +1 -1
  79. package/dist/components/Spinner/index.es.js +1 -1
  80. package/dist/components/form/Checkbox/Field/index.es.js +1 -1
  81. package/dist/components/form/Datepicker/Calendar/index.es.js +2 -2
  82. package/dist/components/form/Datepicker/CalendarHeader/index.es.js +2 -2
  83. package/dist/components/form/Datepicker/PortalHolder/index.es.js +1 -1
  84. package/dist/components/form/Datepicker/Segment/index.es.js +1 -1
  85. package/dist/components/form/Datepicker/index.es.js +3 -3
  86. package/dist/components/form/Field/Label/index.es.js +1 -1
  87. package/dist/components/form/SelectField/index.es.js +1 -1
  88. package/dist/{index-BcC9liUl.js → index-Bkl5OBIY.js} +3 -3
  89. package/dist/{index-BcC9liUl.js.map → index-Bkl5OBIY.js.map} +1 -1
  90. package/dist/{index-CFYmtkje.js → index-CZfMkAoF.js} +2 -2
  91. package/dist/{index-CFYmtkje.js.map → index-CZfMkAoF.js.map} +1 -1
  92. package/dist/{index-CeClo_WS.js → index-Cw1KtRQr.js} +2 -2
  93. package/dist/{index-CeClo_WS.js.map → index-Cw1KtRQr.js.map} +1 -1
  94. package/dist/main.es.js +101 -101
  95. package/package.json +7 -1
  96. package/dist/tokens-C4ypB4bY.js.map +0 -1
@@ -0,0 +1,756 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import InputField from '../InputField/index.astro'
4
+ import DatepickerCalendar from './Calendar/index.astro'
5
+ import IconCalendar from '../../icons/default/IconCalendar.astro'
6
+ import { COLOR_NEUTRAL_40 } from '@consumidor-positivo/aurora/astro/tokens'
7
+ import '../../../dist/components/Datepicker/styles.css'
8
+
9
+ export type AUCalendarDateShape = { day: number; month: number; year: number }
10
+ export type DefaultValue = 'empty' | 'now' | AUCalendarDateShape
11
+
12
+ type FieldProps = Pick<
13
+ HTMLAttributes<'input'>,
14
+ 'id' | 'name' | 'required' | 'autocomplete' | 'style' | 'class'
15
+ >
16
+
17
+ /**
18
+ * Same props and markup as the React `DatepickerField`: an `InputField` with
19
+ * the DD/MM/YYYY mask and the calendar dialog. The React grid comes from
20
+ * `react-aria-components` 1.17; the inline controller reproduces its markup
21
+ * (roles, `aria-label` per day from `Intl`, roving `tabindex`, the `data-*`
22
+ * states the stylesheet uses), its keyboard (arrows, Home, End, PageUp,
23
+ * PageDown, Enter, space) and its live-region announcements, on top of the
24
+ * Aurora hooks it ports step by step: the mask and validation, the month and
25
+ * year pickers, the mobile sheet with Cancel and Confirm, the portal below
26
+ * 600px and the outside click above it.
27
+ *
28
+ * Differences from React: `value` is the initial date; `onChange` becomes
29
+ * `au:datechange` (`detail.value`, the date or `null` for an invalid one) and
30
+ * `onBlur` becomes `au:dateblur` (`detail.value`), both bubbling from the root.
31
+ * `format` has no counterpart: only the DD/MM/YYYY adapter ships. With
32
+ * `defaultValue="now"` the field starts empty in the HTML and the controller
33
+ * fills today, since "now" is the browser's day, not the build's. The
34
+ * calendar strings follow the browser language like react-aria; pt-BR and
35
+ * en-US are bundled, other languages fall back to en-US. Every other HTML
36
+ * attribute goes to the `<input>`, as the React spreads its props.
37
+ */
38
+ export type Props = FieldProps & {
39
+ label?: string
40
+ showOptionalLabel?: boolean
41
+ requiredInput?: boolean
42
+ success?: boolean
43
+ error?: boolean
44
+ errorMessage?: string
45
+ helpMessage?: string
46
+ disabled?: boolean
47
+ /** Use the calendar dialog to pick a date. */
48
+ calendar?: boolean
49
+ placeholder?: string
50
+ /** Initial date. */
51
+ value?: AUCalendarDateShape | null
52
+ /** Default date, as the React uncontrolled mode. */
53
+ defaultValue?: DefaultValue
54
+ /** Makes the calendar dialog the top z-indexed item on the page (below 600px). */
55
+ withPortal?: boolean
56
+ /** Min allowed date, defaults to 1900-01-01. */
57
+ minValue?: AUCalendarDateShape
58
+ /** Max allowed date, defaults to 2100-12-31. */
59
+ maxValue?: AUCalendarDateShape
60
+ }
61
+
62
+ const {
63
+ calendar = true,
64
+ placeholder,
65
+ disabled = false,
66
+ value,
67
+ defaultValue = 'empty',
68
+ withPortal = true,
69
+ minValue = { day: 1, month: 1, year: 1900 },
70
+ maxValue = { day: 31, month: 12, year: 2100 },
71
+ ...rest
72
+ } = Astro.props
73
+
74
+ const pad = (n: number) => String(n).padStart(2, '0')
75
+ const toString = (date: AUCalendarDateShape) => `${pad(date.day)}/${pad(date.month)}/${date.year}`
76
+ const toIso = (date: AUCalendarDateShape) => `${date.year}-${pad(date.month)}-${pad(date.day)}`
77
+
78
+ // Same precedence as `useDatepicker`: `value` wins, then a dated `defaultValue`.
79
+ const initial: AUCalendarDateShape | null =
80
+ value && value.day ? value : typeof defaultValue === 'object' ? defaultValue : null
81
+ const inputDate = initial ? toString(initial) : ''
82
+ const focused = initial ?? null
83
+ ---
84
+
85
+ <div
86
+ class="au-datepicker"
87
+ data-element="au-datepicker"
88
+ data-au-datepicker-min={toIso(minValue)}
89
+ data-au-datepicker-max={toIso(maxValue)}
90
+ data-au-datepicker-selected={initial ? toIso(initial) : undefined}
91
+ data-au-datepicker-now={defaultValue === 'now' && !initial ? '' : undefined}
92
+ data-au-datepicker-portal={withPortal ? '' : undefined}
93
+ data-au-datepicker-calendar={calendar ? '' : undefined}>
94
+ <InputField
95
+ class="au-datepicker__input"
96
+ {...rest}
97
+ maxlength={10}
98
+ value={inputDate}
99
+ type="tel"
100
+ disabled={disabled}
101
+ placeholder={placeholder || 'DD/MM/YYYY'}>
102
+ {
103
+ calendar && (
104
+ <button
105
+ slot="right"
106
+ type="button"
107
+ class="au-datepicker__calendar-holder"
108
+ aria-label="Abrir calendário">
109
+ <IconCalendar rawColor={disabled ? COLOR_NEUTRAL_40 : undefined} />
110
+ </button>
111
+ )
112
+ }
113
+ </InputField>
114
+ {
115
+ calendar && (
116
+ <template data-au-datepicker-calendar>
117
+ <DatepickerCalendar
118
+ minYear={minValue.year}
119
+ maxYear={maxValue.year}
120
+ month={focused?.month ?? 1}
121
+ year={focused?.year ?? minValue.year}
122
+ />
123
+ </template>
124
+ )
125
+ }
126
+ </div>
127
+
128
+ <script>
129
+ import { elementController } from '@consumidor-positivo/aurora/astro/runtime'
130
+
131
+ type DateShape = { day: number; month: number; year: number }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Dates. Plain gregorian arithmetic, the subset of @internationalized/date
135
+ // the React Datepicker uses.
136
+ const BREAKPOINT_MD = '600px'
137
+ const above = (breakpoint: string) => window.matchMedia(`screen and (min-width: ${breakpoint})`).matches
138
+ const pad = (n: number) => String(n).padStart(2, '0')
139
+ const toNative = (d: DateShape) => new Date(d.year, d.month - 1, d.day)
140
+ const fromNative = (n: Date): DateShape => ({ day: n.getDate(), month: n.getMonth() + 1, year: n.getFullYear() })
141
+ const daysInMonth = (year: number, month: number) => new Date(year, month, 0).getDate()
142
+ const compare = (a: DateShape, b: DateShape) => a.year - b.year || a.month - b.month || a.day - b.day
143
+ const isSameDay = (a: DateShape | null | undefined, b: DateShape | null | undefined) => !!a && !!b && compare(a, b) === 0
144
+ const addDays = (d: DateShape, days: number) => fromNative(new Date(d.year, d.month - 1, d.day + days))
145
+ // `CalendarDate.add({months})` keeps the day and constrains it to the month.
146
+ const addMonths = (d: DateShape, months: number) => {
147
+ const total = d.year * 12 + (d.month - 1) + months
148
+ const year = Math.floor(total / 12)
149
+ const month = (total % 12) + 1
150
+ return { year, month, day: Math.min(d.day, daysInMonth(year, month)) }
151
+ }
152
+ const addYears = (d: DateShape, years: number) => ({
153
+ year: d.year + years,
154
+ month: d.month,
155
+ day: Math.min(d.day, daysInMonth(d.year + years, d.month)),
156
+ })
157
+ const startOfMonth = (d: DateShape): DateShape => ({ ...d, day: 1 })
158
+ const endOfMonth = (d: DateShape): DateShape => ({ ...d, day: daysInMonth(d.year, d.month) })
159
+ const today = () => fromNative(new Date())
160
+ const parseIso = (iso: string): DateShape => {
161
+ const [year, month, day] = iso.split('-').map(Number)
162
+ return { year, month, day }
163
+ }
164
+
165
+ // First day of the week per region (CLDR, as @internationalized/date ships
166
+ // it; regions that start on Sunday are omitted).
167
+ const WEEK_START: Record<string, number> = { '001': 1, AD: 1, AE: 6, AF: 6, AI: 1, AL: 1, AM: 1, AN: 1, AR: 1, AT: 1, AU: 1, AX: 1, AZ: 1, BA: 1, BE: 1, BG: 1, BH: 6, BM: 1, BN: 1, BY: 1, CH: 1, CL: 1, CM: 1, CN: 1, CR: 1, CY: 1, CZ: 1, DE: 1, DJ: 6, DK: 1, DZ: 6, EC: 1, EE: 1, EG: 6, ES: 1, FI: 1, FJ: 1, FO: 1, FR: 1, GB: 1, GE: 1, GF: 1, GP: 1, GR: 1, HR: 1, HU: 1, IE: 1, IQ: 6, IR: 6, IS: 1, IT: 1, JO: 6, KG: 1, KW: 6, KZ: 1, LB: 1, LI: 1, LK: 1, LT: 1, LU: 1, LV: 1, LY: 6, MC: 1, MD: 1, ME: 1, MK: 1, MN: 1, MQ: 1, MV: 5, MY: 1, NL: 1, NO: 1, NZ: 1, OM: 6, PL: 1, QA: 6, RE: 1, RO: 1, RS: 1, RU: 1, SD: 6, SE: 1, SI: 1, SK: 1, SM: 1, SY: 6, TJ: 1, TM: 1, TR: 1, UA: 1, UY: 1, UZ: 1, VA: 1, VN: 1, XK: 1 }
168
+ function getWeekStart(locale: string) {
169
+ try {
170
+ const inst = new Intl.Locale(locale)
171
+ const info = (inst as unknown as { getWeekInfo?: () => { firstDay: number } }).getWeekInfo?.()
172
+ if (info) return info.firstDay % 7
173
+ const region = inst.maximize().region
174
+ return region ? WEEK_START[region] || 0 : 0
175
+ } catch {
176
+ return 0
177
+ }
178
+ }
179
+
180
+ // The react-aria calendar strings the Datepicker reaches.
181
+ const STRINGS: Record<string, Record<string, string>> = {
182
+ 'pt-BR': {
183
+ next: 'Próximo',
184
+ minimumDate: 'Primeira data disponível',
185
+ maximumDate: 'Última data disponível',
186
+ dateSelected: '{date} selecionado',
187
+ todayDate: 'Hoje, {date}',
188
+ todayDateSelected: 'Hoje, {date} selecionado',
189
+ selectedDateDescription: 'Data selecionada: {date}',
190
+ },
191
+ 'en-US': {
192
+ next: 'Next',
193
+ minimumDate: 'First available date',
194
+ maximumDate: 'Last available date',
195
+ dateSelected: '{date} selected',
196
+ todayDate: 'Today, {date}',
197
+ todayDateSelected: 'Today, {date} selected',
198
+ selectedDateDescription: 'Selected Date: {date}',
199
+ },
200
+ }
201
+
202
+ // Same DOM as react-aria's LiveAnnouncer, created on first use.
203
+ let announcerNode: HTMLElement | null = null
204
+ function announce(message: string, assertiveness: 'assertive' | 'polite' = 'assertive', timeout = 7000) {
205
+ if (!announcerNode) {
206
+ announcerNode = document.createElement('div')
207
+ announcerNode.dataset.liveAnnouncer = 'true'
208
+ Object.assign(announcerNode.style, {
209
+ border: '0',
210
+ clip: 'rect(0 0 0 0)',
211
+ clipPath: 'inset(50%)',
212
+ height: '1px',
213
+ margin: '-1px',
214
+ overflow: 'hidden',
215
+ padding: '0',
216
+ position: 'absolute',
217
+ width: '1px',
218
+ whiteSpace: 'nowrap',
219
+ })
220
+ for (const live of ['assertive', 'polite']) {
221
+ const log = document.createElement('div')
222
+ log.setAttribute('role', 'log')
223
+ log.setAttribute('aria-live', live)
224
+ log.setAttribute('aria-relevant', 'additions')
225
+ announcerNode.appendChild(log)
226
+ }
227
+ document.body.prepend(announcerNode)
228
+ }
229
+ const log = announcerNode.children[assertiveness === 'assertive' ? 0 : 1]
230
+ const node = document.createElement('div')
231
+ node.textContent = message
232
+ log.appendChild(node)
233
+ if (message !== '') window.setTimeout(() => node.remove(), timeout)
234
+ }
235
+
236
+ // react-aria's interaction modality: keyboard focus shows `data-focus-visible`.
237
+ let modality: 'keyboard' | 'pointer' = 'pointer'
238
+ const toPointer = () => {
239
+ modality = 'pointer'
240
+ document.querySelectorAll('.au-datepicker__calendar-day[data-focus-visible]').forEach((cell) => cell.removeAttribute('data-focus-visible'))
241
+ }
242
+ document.addEventListener('keydown', () => { modality = 'keyboard' }, true)
243
+ document.addEventListener('pointerdown', toPointer, true)
244
+ document.addEventListener('pointermove', toPointer, true)
245
+
246
+ // ---------------------------------------------------------------------------
247
+ elementController('au-datepicker', ({ root, query, emit }) => {
248
+ const input = query<HTMLInputElement>('.au-field__input')
249
+ const template = query<HTMLTemplateElement>('template[data-au-datepicker-calendar]')
250
+ if (!input) return
251
+ const field = input
252
+
253
+ // Controller settings only; React's root carries no such attributes.
254
+ const minValue = parseIso(root.dataset.auDatepickerMin ?? '1900-01-01')
255
+ const maxValue = parseIso(root.dataset.auDatepickerMax ?? '2100-12-31')
256
+ const withPortal = root.hasAttribute('data-au-datepicker-portal')
257
+ const hasCalendar = root.hasAttribute('data-au-datepicker-calendar')
258
+ const startsNow = root.hasAttribute('data-au-datepicker-now')
259
+ let selectedDate: DateShape | null = root.dataset.auDatepickerSelected ? parseIso(root.dataset.auDatepickerSelected) : null
260
+ for (const name of ['min', 'max', 'selected', 'now', 'portal', 'calendar']) root.removeAttribute(`data-au-datepicker-${name}`)
261
+
262
+ const disabled = field.disabled
263
+ const locale = navigator.language || 'en-US'
264
+ const strings = STRINGS[locale] ?? STRINGS[locale.split('-')[0] === 'pt' ? 'pt-BR' : 'en-US']
265
+ const t = (key: string, date = '') => strings[key].replace('{date}', date)
266
+ const monthFormatter = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' })
267
+ const dayLabelFormatter = new Intl.DateTimeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })
268
+ const dayNumberFormatter = new Intl.DateTimeFormat(locale, { day: 'numeric' })
269
+ const weekdayFormatter = new Intl.DateTimeFormat(locale, { weekday: 'short' })
270
+ const weekStart = getWeekStart(locale)
271
+
272
+ // ---- the field (useDatepicker) -----------------------------------------
273
+ const format = {
274
+ placeholder: 'DD/MM/YYYY',
275
+ maskDate(dateStr: string) {
276
+ let fmt = dateStr.replace(/\D/g, '')
277
+ if (fmt.length > 2) fmt = `${fmt.slice(0, 2)}/${fmt.slice(2)}`
278
+ if (fmt.length > 5) fmt = `${fmt.slice(0, 5)}/${fmt.slice(5)}`
279
+ return fmt
280
+ },
281
+ validateFormat: (dateStr: string) => /^\d{2}\/\d{2}\/\d{4}$/.test(dateStr),
282
+ toCalendarDate(dateStr: string): DateShape {
283
+ const [day, month, year] = dateStr.split('/').map(Number)
284
+ return { day, month, year }
285
+ },
286
+ toString: (d: DateShape) => `${pad(d.day)}/${pad(d.month)}/${d.year}`,
287
+ validate(dateStr: string) {
288
+ const [day, month, year] = dateStr.split('/').map(Number)
289
+ const date = new Date(year, month - 1, day)
290
+ const olderThanMin = date < toNative(minValue)
291
+ const exceedsMax = date > toNative(maxValue)
292
+ return date.getDate() === day && date.getMonth() + 1 === month && date.getFullYear() === year && !olderThanMin && !exceedsMax
293
+ },
294
+ }
295
+
296
+ // `isMobileInputWithCalendar`, computed once like the React `useMemo`.
297
+ if (!above(BREAKPOINT_MD) && hasCalendar) field.setAttribute('inputmode', 'none')
298
+
299
+ if (startsNow && !selectedDate) {
300
+ selectedDate = today()
301
+ field.value = format.toString(selectedDate)
302
+ }
303
+
304
+ let isCalendarVisible = false
305
+
306
+ function handleLoseFocus(event: MouseEvent) {
307
+ if (!root.contains(event.target as Node)) {
308
+ closeCalendar()
309
+ document.removeEventListener('mousedown', handleLoseFocus)
310
+ }
311
+ }
312
+ // `useOutsideClick` with the MD breakpoint: only above it.
313
+ function listenOutsideClick() {
314
+ if (!above(BREAKPOINT_MD)) return
315
+ document.addEventListener('mousedown', handleLoseFocus)
316
+ }
317
+
318
+ function handleInputChange() {
319
+ const maskedValue = format.maskDate(field.value)
320
+ field.value = maskedValue
321
+ if (!format.validateFormat(maskedValue)) return
322
+ const isDateValid = format.validate(maskedValue)
323
+ const newFieldValue = isDateValid ? format.toCalendarDate(maskedValue) : null
324
+ selectedDate = newFieldValue
325
+ // The React calendar only follows a dated `value` (`if (!value) return`),
326
+ // so an invalid date leaves the previous selection in the calendar.
327
+ if (newFieldValue) calendar?.setValue(newFieldValue)
328
+ emit('au:datechange', { value: newFieldValue })
329
+ if (!isDateValid) field.value = ''
330
+ }
331
+
332
+ function openCalendar() {
333
+ if (!isCalendarVisible && disabled) return
334
+ listenOutsideClick()
335
+ if (isCalendarVisible) return
336
+ isCalendarVisible = true
337
+ calendar?.show()
338
+ }
339
+
340
+ function closeCalendar() {
341
+ if (!isCalendarVisible) return
342
+ isCalendarVisible = false
343
+ calendar?.hide()
344
+ }
345
+
346
+ function updateDateFromCalendar(pickerDate: DateShape) {
347
+ selectedDate = pickerDate
348
+ field.value = format.toString(pickerDate)
349
+ calendar?.setValue(pickerDate)
350
+ emit('au:datechange', { value: pickerDate })
351
+ }
352
+
353
+ field.addEventListener('input', handleInputChange)
354
+ field.addEventListener('focus', openCalendar)
355
+ field.addEventListener('blur', () => emit('au:dateblur', { value: selectedDate }))
356
+ query<HTMLButtonElement>('.au-datepicker__calendar-holder')?.addEventListener('click', () => field.focus())
357
+
358
+ // ---- the calendar (useCalendar + react-aria) ---------------------------
359
+ type Calendar = {
360
+ show(): void
361
+ hide(): void
362
+ setValue(date: DateShape | null): void
363
+ }
364
+
365
+ const calendar: Calendar | null = template ? createCalendar(template) : null
366
+
367
+ function createCalendar(template: HTMLTemplateElement): Calendar {
368
+ const calendarEl = template.content.firstElementChild as HTMLElement
369
+ template.remove()
370
+ const base = calendarEl.querySelector<HTMLElement>('.au-datepicker__calendar-base')!
371
+ const title = base.querySelector<HTMLElement>(':scope > div > h2')!
372
+ const grid = base.querySelector<HTMLTableElement>('table')!
373
+ const headerCells = Array.from(grid.querySelectorAll<HTMLElement>('thead th'))
374
+ const body = grid.querySelector<HTMLElement>('tbody')!
375
+ const hiddenNext = base.querySelector<HTMLButtonElement>(':scope > div:last-child > button')!
376
+ const prevButton = base.querySelector<HTMLButtonElement>('.au-datepicker__header > button:first-child')!
377
+ const nextButton = base.querySelector<HTMLButtonElement>('.au-datepicker__header > button:last-child')!
378
+ const segments = Array.from(base.querySelectorAll<HTMLElement>('.au-datepicker__segment'))
379
+ const cancelButton = calendarEl.querySelector<HTMLButtonElement>('.au-datepicker__calendar-cancel')!
380
+ const confirmButton = calendarEl.querySelector<HTMLButtonElement>('.au-datepicker__calendar-actions-dock > button:last-child')!
381
+ const backdrop = calendarEl.querySelector<HTMLElement>('.au-datepicker__calendar-backdrop')!
382
+
383
+ // react-stately calendar state.
384
+ let value: DateShape | null = selectedDate
385
+ let focusedDate: DateShape = constrain(value ?? today())
386
+ let startDate: DateShape = startOfMonth(focusedDate)
387
+ let isFocused = true // `autoFocus`
388
+ let mounted = false
389
+ let enterTimer: number | undefined
390
+ let leaveTimer: number | undefined
391
+ let lastRange = ''
392
+ let lastSelectedDescription = ''
393
+ let showing = false
394
+ let lastFocusKey = ''
395
+ const portalDoor = document.createElement('div')
396
+
397
+ function constrain(date: DateShape) {
398
+ if (compare(date, minValue) < 0) return minValue
399
+ if (compare(date, maxValue) > 0) return maxValue
400
+ return date
401
+ }
402
+ const isInvalid = (date: DateShape) => compare(date, minValue) < 0 || compare(date, maxValue) > 0
403
+ const endDate = () => endOfMonth(startDate)
404
+ const isCellDisabled = (date: DateShape) => compare(date, startDate) < 0 || compare(date, endDate()) > 0 || isInvalid(date)
405
+ const isSelected = (date: DateShape) => value !== null && isSameDay(date, value) && !isCellDisabled(date)
406
+ const rangeDescription = () => monthFormatter.format(toNative(startDate))
407
+ const selectedDescription = () => (value ? t('selectedDateDescription', dayLabelFormatter.format(toNative(value))) : '')
408
+
409
+ function setFocusedDate(date: DateShape) {
410
+ focusedDate = constrain(date)
411
+ if (compare(focusedDate, startDate) < 0 || compare(focusedDate, endDate()) > 0) startDate = startOfMonth(focusedDate)
412
+ render()
413
+ }
414
+ // `focusNextPage` / `focusPreviousPage`, one month per page.
415
+ function focusPage(direction: 1 | -1) {
416
+ focusedDate = constrain(addMonths(focusedDate, direction))
417
+ startDate = startOfMonth(constrain(addMonths(startDate, direction)))
418
+ render()
419
+ }
420
+ function setValue(next: DateShape | null) {
421
+ value = next
422
+ if (next) focusedDate = constrain(next)
423
+ if (compare(focusedDate, startDate) < 0 || compare(focusedDate, endDate()) > 0) startDate = startOfMonth(focusedDate)
424
+ render()
425
+ }
426
+ function selectDate(date: DateShape, byPointer = false) {
427
+ // `calendarChange`: on desktop the pick is final and closes; on mobile
428
+ // it only moves the selection until "Confirmar".
429
+ value = constrain(date)
430
+ focusedDate = value
431
+ if (!byPointer) isFocused = true
432
+ render()
433
+ if (!above(BREAKPOINT_MD)) return
434
+ triggerChange(value)
435
+ }
436
+ function triggerChange(date: DateShape) {
437
+ updateDateFromCalendar(date)
438
+ closeCalendar()
439
+ }
440
+
441
+ // ---- rendering ---------------------------------------------------------
442
+ function weekdays() {
443
+ const start = addDays(today(), -((toNative(today()).getDay() - weekStart + 7) % 7))
444
+ return Array.from({ length: 7 }, (_, i) => {
445
+ const day = weekdayFormatter.format(toNative(addDays(start, i)))
446
+ // `fmtWeekday` in `useCalendar`.
447
+ return `${day.charAt(0).toUpperCase()}${day.slice(1)}`.replace('.', '')
448
+ })
449
+ }
450
+ function datesInWeek(weekIndex: number): DateShape[] {
451
+ const from = addDays(startDate, weekIndex * 7)
452
+ const offset = (toNative(from).getDay() - weekStart + 7) % 7
453
+ const first = addDays(from, -offset)
454
+ return Array.from({ length: 7 }, (_, i) => addDays(first, i))
455
+ }
456
+ function weeksInMonth() {
457
+ const offset = (toNative(startDate).getDay() - weekStart + 7) % 7
458
+ return Math.ceil((offset + daysInMonth(startDate.year, startDate.month)) / 7)
459
+ }
460
+ function cellLabel(date: DateShape) {
461
+ let label = dayLabelFormatter.format(toNative(date))
462
+ const selected = isSelected(date)
463
+ if (isSameDay(date, today())) label = t(selected ? 'todayDateSelected' : 'todayDate', label)
464
+ else if (selected) label = t('dateSelected', label)
465
+ if (isSameDay(date, minValue)) label += `, ${t('minimumDate')}`
466
+ else if (isSameDay(date, maxValue)) label += `, ${t('maximumDate')}`
467
+ return label
468
+ }
469
+
470
+ const cells = new Map<string, HTMLElement>()
471
+ const segmentResets: Array<() => void> = []
472
+ function renderGrid() {
473
+ const weeks = weeksInMonth()
474
+ const rows = Array.from(body.children) as HTMLTableRowElement[]
475
+ while (rows.length > weeks) rows.pop()!.remove()
476
+ cells.clear()
477
+ for (let w = 0; w < weeks; w++) {
478
+ let row = rows[w]
479
+ if (!row) {
480
+ row = document.createElement('tr')
481
+ body.appendChild(row)
482
+ }
483
+ const dates = datesInWeek(w)
484
+ const tds = Array.from(row.children) as HTMLTableCellElement[]
485
+ while (tds.length > 7) tds.pop()!.remove()
486
+ dates.forEach((date, i) => {
487
+ let td = tds[i]
488
+ if (!td) {
489
+ td = document.createElement('td')
490
+ td.setAttribute('role', 'gridcell')
491
+ td.appendChild(createCell())
492
+ row!.appendChild(td)
493
+ }
494
+ renderCell(td, date)
495
+ })
496
+ }
497
+ }
498
+ function createCell() {
499
+ const cell = document.createElement('div')
500
+ cell.dataset.reactAriaPressable = 'true'
501
+ cell.setAttribute('role', 'button')
502
+ cell.className = 'au-datepicker__calendar-day'
503
+ cell.dataset.rac = ''
504
+ // useHover (mouse only) and usePress with `preventFocusOnPress`: the
505
+ // press never moves focus, the pick happens on release.
506
+ let pressing = false
507
+ cell.addEventListener('pointerenter', (e) => { if (e.pointerType !== 'touch' && !cell.hasAttribute('aria-disabled')) cell.dataset.hovered = 'true' })
508
+ cell.addEventListener('pointerleave', () => { delete cell.dataset.hovered; pressing = false })
509
+ cell.addEventListener('mousedown', (e) => e.preventDefault())
510
+ cell.addEventListener('pointerdown', () => { pressing = !cell.hasAttribute('aria-disabled') })
511
+ cell.addEventListener('pointerup', () => {
512
+ if (!pressing) return
513
+ pressing = false
514
+ const date = cellDate(cell)
515
+ if (date) selectDate(date, true)
516
+ })
517
+ cell.addEventListener('pointercancel', () => { pressing = false })
518
+ cell.addEventListener('focus', () => {
519
+ const date = cellDate(cell)
520
+ if (date && !cell.hasAttribute('aria-disabled')) {
521
+ focusedDate = date
522
+ isFocused = true
523
+ render()
524
+ }
525
+ })
526
+ cell.addEventListener('contextmenu', (e) => e.preventDefault())
527
+ return cell
528
+ }
529
+ const cellDate = (cell: HTMLElement) => (cell.dataset.auDate ? parseIso(cell.dataset.auDate) : null)
530
+ function renderCell(td: HTMLTableCellElement, date: DateShape) {
531
+ const cell = td.firstElementChild as HTMLElement
532
+ const disabledCell = isCellDisabled(date)
533
+ const selected = isSelected(date)
534
+ const outsideMonth = date.month !== startDate.month || date.year !== startDate.year
535
+ const focusedCell = isFocused && isSameDay(date, focusedDate) && !outsideMonth
536
+ const iso = `${date.year}-${pad(date.month)}-${pad(date.day)}`
537
+ cell.dataset.auDate = iso
538
+ cells.set(iso, cell)
539
+
540
+ setAttr(td, 'aria-disabled', disabledCell ? 'true' : null)
541
+ setAttr(td, 'aria-selected', selected ? 'true' : null)
542
+
543
+ setAttr(cell, 'tabindex', disabledCell ? null : isSameDay(date, focusedDate) ? '0' : '-1')
544
+ setAttr(cell, 'aria-disabled', disabledCell ? 'true' : null)
545
+ setAttr(cell, 'aria-label', cellLabel(date))
546
+ setAttr(cell, 'data-focused', focusedCell ? 'true' : null)
547
+ setAttr(cell, 'data-disabled', disabledCell ? 'true' : null)
548
+ setAttr(cell, 'data-focus-visible', focusedCell && modality === 'keyboard' && document.activeElement === cell ? 'true' : null)
549
+ setAttr(cell, 'data-outside-visible-range', outsideMonth ? 'true' : null)
550
+ setAttr(cell, 'data-outside-month', outsideMonth ? 'true' : null)
551
+ setAttr(cell, 'data-selected', selected ? 'true' : null)
552
+ setAttr(cell, 'data-today', isSameDay(date, today()) ? 'true' : null)
553
+ const text = dayNumberFormatter.formatToParts(toNative(date)).find((p) => p.type === 'day')?.value ?? String(date.day)
554
+ if (cell.textContent !== text) cell.textContent = text
555
+ }
556
+ function setAttr(el: Element, name: string, val: string | null) {
557
+ if (val === null) el.removeAttribute(name)
558
+ else if (el.getAttribute(name) !== val) el.setAttribute(name, val)
559
+ }
560
+
561
+ function renderSegments() {
562
+ segments.forEach((segment, index) => {
563
+ const current = index === 0 ? focusedDate.month : focusedDate.year
564
+ const button = segment.querySelector<HTMLButtonElement>('.au-datepicker__segment-input')!
565
+ const items = Array.from(segment.querySelectorAll<HTMLLIElement>('.au-datepicker__segment-list-item'))
566
+ let label = ''
567
+ items.forEach((item) => {
568
+ const active = Number(item.dataset.auValue) === current
569
+ item.classList.toggle('au-datepicker__segment-list-item--active', active)
570
+ item.setAttribute('aria-selected', String(active))
571
+ if (active) label = item.textContent?.trim() ?? ''
572
+ })
573
+ if (button.firstChild?.nodeType === 3) {
574
+ if (button.firstChild.nodeValue !== label) button.firstChild.nodeValue = label
575
+ } else button.prepend(label)
576
+ })
577
+ }
578
+
579
+ function render() {
580
+ const range = rangeDescription()
581
+ base.setAttribute('aria-label', range)
582
+ title.textContent = range
583
+ grid.setAttribute('aria-label', range)
584
+ hiddenNext.setAttribute('aria-label', t('next'))
585
+ hiddenNext.disabled = isInvalid(addDays(endDate(), 1))
586
+ weekdays().forEach((day, i) => { if (headerCells[i].textContent !== day) headerCells[i].textContent = day })
587
+ renderGrid()
588
+ renderSegments()
589
+ confirmButton.classList.toggle('au-btn--disabled', value === null)
590
+ confirmButton.disabled = value === null
591
+
592
+ // react-aria announces the visible range when it changes with the
593
+ // grid unfocused, and the selected date whenever it changes.
594
+ if (mounted && range !== lastRange && !isFocused) announce(range)
595
+ lastRange = range
596
+ const description = selectedDescription()
597
+ if (mounted && description !== lastSelectedDescription && description) announce(description, 'polite', 4000)
598
+ lastSelectedDescription = description
599
+
600
+ // The react-aria cell effect: when a cell becomes the focused one
601
+ // while the calendar is focused, it takes DOM focus wherever it was
602
+ // (the field included), except on mount, where the field's own focus
603
+ // wins.
604
+ const focusKey = isFocused ? `${focusedDate.year}-${pad(focusedDate.month)}-${pad(focusedDate.day)}` : ''
605
+ const focused = focusKey ? cells.get(focusKey) : undefined
606
+ if (focused && focusKey !== lastFocusKey && !showing && document.activeElement !== focused) {
607
+ focused.focus({ preventScroll: true })
608
+ setAttr(focused, 'data-focus-visible', modality === 'keyboard' ? 'true' : null)
609
+ }
610
+ lastFocusKey = focusKey
611
+ }
612
+
613
+ // ---- keyboard (useCalendarGrid) ----------------------------------------
614
+ grid.addEventListener('keydown', (event) => {
615
+ const stop = () => { event.preventDefault(); event.stopPropagation() }
616
+ switch (event.key) {
617
+ case 'Enter':
618
+ case ' ':
619
+ event.preventDefault()
620
+ selectDate(focusedDate)
621
+ break
622
+ case 'PageUp':
623
+ stop()
624
+ if (event.shiftKey) setFocusedDate(addYears(focusedDate, -1))
625
+ else setFocusedDate(addMonths(focusedDate, -1))
626
+ break
627
+ case 'PageDown':
628
+ stop()
629
+ if (event.shiftKey) setFocusedDate(addYears(focusedDate, 1))
630
+ else setFocusedDate(addMonths(focusedDate, 1))
631
+ break
632
+ case 'End':
633
+ stop()
634
+ setFocusedDate(endOfMonth(focusedDate))
635
+ break
636
+ case 'Home':
637
+ stop()
638
+ setFocusedDate(startOfMonth(focusedDate))
639
+ break
640
+ case 'ArrowLeft':
641
+ stop()
642
+ setFocusedDate(addDays(focusedDate, -1))
643
+ break
644
+ case 'ArrowUp':
645
+ stop()
646
+ setFocusedDate(addDays(focusedDate, -7))
647
+ break
648
+ case 'ArrowRight':
649
+ stop()
650
+ setFocusedDate(addDays(focusedDate, 1))
651
+ break
652
+ case 'ArrowDown':
653
+ stop()
654
+ setFocusedDate(addDays(focusedDate, 7))
655
+ break
656
+ }
657
+ })
658
+ grid.addEventListener('focusin', () => { isFocused = true; render() })
659
+ grid.addEventListener('focusout', () => { isFocused = false; render() })
660
+
661
+ // ---- header (useCalendarHeader + useSegment) ---------------------------
662
+ prevButton.addEventListener('click', () => focusPage(-1))
663
+ nextButton.addEventListener('click', () => focusPage(1))
664
+ segments.forEach((segment, index) => {
665
+ const button = segment.querySelector<HTMLButtonElement>('.au-datepicker__segment-input')!
666
+ const list = segment.querySelector<HTMLElement>('.au-datepicker__segment-list')!
667
+ let open = false
668
+ const setOpen = (next: boolean) => {
669
+ open = next
670
+ segment.classList.toggle('au-datepicker__segment--open', open)
671
+ button.setAttribute('aria-expanded', String(open))
672
+ if (open) list.querySelector<HTMLElement>('.au-datepicker__segment-list-item--active')?.scrollIntoView({ block: 'center', inline: 'center' })
673
+ }
674
+ const outside = (event: MouseEvent) => {
675
+ if (segment.contains(event.target as Node)) return
676
+ setOpen(false)
677
+ document.removeEventListener('mousedown', outside)
678
+ }
679
+ segmentResets.push(() => { if (open) setOpen(false); document.removeEventListener('mousedown', outside) })
680
+ button.addEventListener('click', () => {
681
+ setOpen(true)
682
+ if (above(BREAKPOINT_MD)) document.addEventListener('mousedown', outside)
683
+ })
684
+ segment.querySelectorAll<HTMLButtonElement>('.au-datepicker__segment-list-header button').forEach((close) => close.addEventListener('click', () => setOpen(false)))
685
+ list.addEventListener('click', (event) => {
686
+ const item = (event.target as Element | null)?.closest<HTMLElement>('.au-datepicker__segment-list-item')
687
+ if (!item) return
688
+ const picked = Number(item.dataset.auValue)
689
+ const next = index === 0
690
+ ? { year: focusedDate.year, month: picked, day: Math.min(focusedDate.day, daysInMonth(focusedDate.year, picked)) }
691
+ : { year: picked, month: focusedDate.month, day: Math.min(focusedDate.day, daysInMonth(picked, focusedDate.month)) }
692
+ setFocusedDate(next)
693
+ setOpen(false)
694
+ })
695
+ })
696
+
697
+ cancelButton.addEventListener('click', closeCalendar)
698
+ confirmButton.addEventListener('click', () => { if (value) triggerChange(value) })
699
+ backdrop.addEventListener('click', closeCalendar)
700
+
701
+ // ---- mount / unmount (useCalendar delays + PortalHolder) ---------------
702
+ function mount() {
703
+ if (mounted) return
704
+ mounted = true
705
+ if (!above(BREAKPOINT_MD) && withPortal) {
706
+ let portalRoot = document.getElementById('au-portal')
707
+ if (!portalRoot) {
708
+ portalRoot = document.createElement('div')
709
+ portalRoot.id = 'au-portal'
710
+ document.body.appendChild(portalRoot)
711
+ }
712
+ portalRoot.appendChild(portalDoor)
713
+ portalDoor.appendChild(calendarEl)
714
+ } else {
715
+ root.appendChild(calendarEl)
716
+ }
717
+ }
718
+ function unmount() {
719
+ if (!mounted) return
720
+ mounted = false
721
+ calendarEl.remove()
722
+ portalDoor.remove()
723
+ cells.clear()
724
+ // React mounts a fresh calendar every time: no hover left over and
725
+ // every picker closed.
726
+ body.querySelectorAll<HTMLElement>('[data-hovered]').forEach((cell) => { delete cell.dataset.hovered })
727
+ segmentResets.forEach((reset) => reset())
728
+ }
729
+
730
+ return {
731
+ show() {
732
+ window.clearTimeout(leaveTimer)
733
+ // `defaultFocusDate` → `setFocusedDate` on the header effect.
734
+ if (value) { focusedDate = constrain(value); startDate = startOfMonth(focusedDate) }
735
+ isFocused = true
736
+ lastRange = rangeDescription()
737
+ lastSelectedDescription = selectedDescription()
738
+ showing = true
739
+ render()
740
+ showing = false
741
+ lastFocusKey = `${focusedDate.year}-${pad(focusedDate.month)}-${pad(focusedDate.day)}`
742
+ mount()
743
+ enterTimer = window.setTimeout(() => calendarEl.classList.add('au-datepicker__calendar--visible'), 100)
744
+ },
745
+ hide() {
746
+ window.clearTimeout(enterTimer)
747
+ calendarEl.classList.remove('au-datepicker__calendar--visible')
748
+ leaveTimer = window.setTimeout(unmount, 200)
749
+ },
750
+ setValue(next) {
751
+ setValue(next)
752
+ },
753
+ }
754
+ }
755
+ })
756
+ </script>