@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,237 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import FieldRoot from '../Field/Root/index.astro'
4
+ import FieldLabel from '../Field/Label/index.astro'
5
+ import FieldInputHolder from '../Field/InputHolder/index.astro'
6
+ import FieldInput from '../Field/Input/index.astro'
7
+ import FieldMessage from '../Field/Message/index.astro'
8
+ import '../../../dist/components/EmailField/styles.css'
9
+
10
+ /**
11
+ * Same props as the React `EmailField`, and the same split: `class` and
12
+ * `style` go on the `au-field` root, every other HTML attribute (`name`,
13
+ * `value`, `required`, `autocomplete`, `data-*`...) lands on the `<input>`.
14
+ *
15
+ * The domain autocomplete is the component's own behavior, kept by an inline
16
+ * controller with the same domain list and keys as the React hook (ArrowUp,
17
+ * ArrowDown, Enter, Escape). The React `onChange(value)` has no counterpart:
18
+ * listen to the input's native `input` event (also fired when a suggestion is
19
+ * picked) or to `au:emailselect` (`detail.value`, the completed e-mail), which
20
+ * bubbles from the root. `rightSlot` is the `right` named slot; `inputRef` has
21
+ * no counterpart. Like the React version, the input only gets an `id` when one
22
+ * is passed.
23
+ */
24
+ export type Props = Omit<HTMLAttributes<'input'>, 'type' | 'disabled'> & {
25
+ showOptionalLabel?: boolean
26
+ /** Marks the label as required. Pass `required` too for native validation. */
27
+ requiredInput?: boolean
28
+ success?: boolean
29
+ error?: boolean
30
+ errorMessage?: string
31
+ helpMessage?: string
32
+ label?: string
33
+ disabled?: boolean
34
+ /** Inline style of the `<input>` (`style` goes on the root). */
35
+ inputStyle?: HTMLAttributes<'input'>['style']
36
+ }
37
+
38
+ const uid = crypto.randomUUID().slice(0, 8)
39
+
40
+ const {
41
+ showOptionalLabel,
42
+ requiredInput,
43
+ success = false,
44
+ error = false,
45
+ errorMessage,
46
+ helpMessage,
47
+ label,
48
+ id: idProp,
49
+ disabled = false,
50
+ style,
51
+ class: className,
52
+ inputStyle,
53
+ ...rest
54
+ } = Astro.props
55
+
56
+ const id = idProp
57
+
58
+ // Stands in for the React `useId()` of the listbox.
59
+ const listboxId = `au-email-${uid}-listbox`
60
+ ---
61
+
62
+ <FieldRoot
63
+ style={style}
64
+ class={className}
65
+ success={success}
66
+ error={error}
67
+ disabled={disabled}
68
+ data-element="au-email-field">
69
+ <FieldLabel
70
+ text={label}
71
+ id={id}
72
+ showOptionalLabel={showOptionalLabel}
73
+ required={requiredInput}
74
+ success={success}
75
+ error={error}
76
+ disabled={disabled}
77
+ />
78
+ <FieldInputHolder>
79
+ <FieldInput
80
+ id={id}
81
+ type="email"
82
+ disabled={disabled}
83
+ style={inputStyle}
84
+ role="combobox"
85
+ aria-haspopup="listbox"
86
+ aria-expanded="false"
87
+ aria-autocomplete="list"
88
+ aria-controls={listboxId}
89
+ {...rest}
90
+ />
91
+ <ul
92
+ id={listboxId}
93
+ class="au-field__input-autocomplete"
94
+ tabindex="-1"
95
+ role="listbox"
96
+ style="overflow-y: auto">
97
+ </ul>
98
+ <slot name="right" slot="right" />
99
+ </FieldInputHolder>
100
+ <FieldMessage
101
+ hasError={error}
102
+ errorMessage={errorMessage}
103
+ helpMessage={helpMessage}
104
+ />
105
+ </FieldRoot>
106
+
107
+ <script>
108
+ import { elementController } from '@consumidor-positivo/aurora/astro/runtime'
109
+
110
+ // Same list, same order, as the React hook (`EmailField/hook.ts`).
111
+ const EMAIL_DOMAINS = [
112
+ 'gmail.com',
113
+ 'hotmail.com',
114
+ 'yahoo.com',
115
+ 'outlook.com',
116
+ 'yahoo.com.br',
117
+ 'uol.com.br',
118
+ 'ig.com.br',
119
+ 'terra.com.br',
120
+ 'aol.com',
121
+ 'live.com',
122
+ 'msn.com',
123
+ ]
124
+ const OPEN_CLASS = 'au-field__input-autocomplete--open'
125
+ const OPTION_CLASS = 'au-field__input-option'
126
+
127
+ elementController('au-email-field', ({ query, on, emit }) => {
128
+ const input = query<HTMLInputElement>('.au-field__input')
129
+ const listbox = query<HTMLUListElement>('.au-field__input-autocomplete')
130
+ if (!input || !listbox) return
131
+
132
+ let suggestions: string[] = []
133
+ let activeIndex = -1
134
+ let isOpen = false
135
+ let isSelecting = false
136
+
137
+ const optionId = (index: number) => `${listbox.id}-option-${index}`
138
+
139
+ function syncActiveDescendant() {
140
+ if (isOpen && activeIndex >= 0) {
141
+ input!.setAttribute('aria-activedescendant', optionId(activeIndex))
142
+ } else {
143
+ input!.removeAttribute('aria-activedescendant')
144
+ }
145
+ }
146
+
147
+ function setOpen(open: boolean) {
148
+ isOpen = open
149
+ listbox!.classList.toggle(OPEN_CLASS, open)
150
+ input!.setAttribute('aria-expanded', String(open))
151
+ syncActiveDescendant()
152
+ }
153
+
154
+ function setActive(index: number) {
155
+ activeIndex = index
156
+ Array.from(listbox!.children).forEach((option, i) => {
157
+ option.setAttribute('aria-selected', String(i === index))
158
+ })
159
+ syncActiveDescendant()
160
+ }
161
+
162
+ function renderSuggestions(next: string[]) {
163
+ suggestions = next
164
+ activeIndex = -1
165
+ listbox!.replaceChildren(
166
+ ...next.map((suggestion, index) => {
167
+ const option = document.createElement('li')
168
+ option.id = optionId(index)
169
+ option.className = OPTION_CLASS
170
+ option.setAttribute('role', 'option')
171
+ option.setAttribute('aria-selected', 'false')
172
+ option.textContent = suggestion
173
+ return option
174
+ }),
175
+ )
176
+ }
177
+
178
+ function update(value: string) {
179
+ const atIndex = value.indexOf('@')
180
+ if (atIndex < 0) {
181
+ renderSuggestions([])
182
+ setOpen(false)
183
+ return
184
+ }
185
+
186
+ const domainPart = value.slice(atIndex + 1)
187
+ const filtered = EMAIL_DOMAINS.filter((domain) => domain.startsWith(domainPart))
188
+ renderSuggestions(filtered)
189
+ const isComplete = filtered.some((domain) => value.endsWith(domain))
190
+ setOpen(filtered.length > 0 && !isComplete)
191
+ }
192
+
193
+ function select(suggestion: string) {
194
+ const atIndex = input!.value.indexOf('@')
195
+ if (atIndex > -1) {
196
+ const value = input!.value.slice(0, atIndex + 1) + suggestion
197
+ input!.value = value
198
+ // Lets the app's own `input` listeners see the completed value.
199
+ isSelecting = true
200
+ input!.dispatchEvent(new Event('input', { bubbles: true }))
201
+ isSelecting = false
202
+ emit('au:emailselect', { value })
203
+ }
204
+ renderSuggestions([])
205
+ setOpen(false)
206
+ }
207
+
208
+ on('input', (event) => {
209
+ if (isSelecting || event.target !== input) return
210
+ update(input.value)
211
+ })
212
+
213
+ on('click', (event) => {
214
+ const option = (event.target as Element | null)?.closest<HTMLElement>(
215
+ `.${OPTION_CLASS}`,
216
+ )
217
+ if (option && listbox.contains(option)) select(option.textContent ?? '')
218
+ })
219
+
220
+ on('keydown', (event) => {
221
+ if (event.target !== input || !isOpen || suggestions.length === 0) return
222
+
223
+ if (event.key === 'ArrowDown') {
224
+ event.preventDefault()
225
+ setActive((activeIndex + 1) % suggestions.length)
226
+ } else if (event.key === 'ArrowUp') {
227
+ event.preventDefault()
228
+ setActive(activeIndex <= 0 ? suggestions.length - 1 : activeIndex - 1)
229
+ } else if (event.key === 'Enter' && activeIndex >= 0) {
230
+ event.preventDefault()
231
+ select(suggestions[activeIndex])
232
+ } else if (event.key === 'Escape') {
233
+ setOpen(false)
234
+ }
235
+ })
236
+ })
237
+ </script>
@@ -0,0 +1,20 @@
1
+ ---
2
+ import '../../../../dist/components/Field/styles2.css'
3
+
4
+ /** Same props as the React `Field.ErrorMessage`. */
5
+ export type Props = {
6
+ hasError?: boolean
7
+ message?: string
8
+ id?: string
9
+ }
10
+
11
+ const { hasError, message, id } = Astro.props
12
+ ---
13
+
14
+ {
15
+ hasError && message && (
16
+ <p id={id} class="au-field__message au-field__message--error" role="alert">
17
+ {message}
18
+ </p>
19
+ )
20
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import '../../../../dist/components/Field/styles2.css'
4
+
5
+ /**
6
+ * The bare `au-field__input`. Every HTML attribute lands on the `<input>`;
7
+ * the React `customclass` is the plain `class`, and `inputRef` has no
8
+ * counterpart (select the element by `id` or a `data-*` attribute).
9
+ */
10
+ export type Props = HTMLAttributes<'input'>
11
+
12
+ const { class: className, 'class:list': classList, ...rest } = Astro.props
13
+ ---
14
+
15
+ <input class:list={['au-field__input', className, classList]} {...rest} />
@@ -0,0 +1,28 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import '../../../../dist/components/Field/styles2.css'
4
+
5
+ /**
6
+ * The React `rightSideSlot` is the `right` named slot here. The
7
+ * `au-field__right-slot` wrapper only renders when that slot has content, so a
8
+ * parent can forward the slot unconditionally.
9
+ */
10
+ export type Props = HTMLAttributes<'div'>
11
+
12
+ const { class: className, ...rest } = Astro.props
13
+
14
+ const right = Astro.slots.has('right')
15
+ ? (await Astro.slots.render('right')).trim()
16
+ : ''
17
+ ---
18
+
19
+ <div class:list={['au-field__input-holder', className]} {...rest}>
20
+ <slot />
21
+ {
22
+ right && (
23
+ <div class="au-field__right-slot">
24
+ <Fragment set:html={right} />
25
+ </div>
26
+ )
27
+ }
28
+ </div>
@@ -0,0 +1,80 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import IconAlertOctagon from '../../../icons/default/IconAlertOctagon.astro'
4
+ import IconCheck from '../../../icons/default/IconCheck.astro'
5
+ import IconSlash from '../../../icons/default/IconSlash.astro'
6
+ import {
7
+ COLOR_ERROR_50,
8
+ COLOR_NEUTRAL_40,
9
+ COLOR_SUCCESS_50,
10
+ } from '@consumidor-positivo/aurora/astro/tokens'
11
+ import '../../../../dist/components/Field/styles2.css'
12
+
13
+ /**
14
+ * Same props as the React `Field.Label`. `id` is the id of the control the
15
+ * label points to (`for`), not the id of this element. Rich label content,
16
+ * the React `text` as a ReactNode, goes in the default slot; the label renders
17
+ * nothing when neither `text` nor the slot has content. Other HTML attributes
18
+ * land on the `au-field__header` wrapper.
19
+ */
20
+ export type Props = Omit<HTMLAttributes<'div'>, 'id'> & {
21
+ /** Id of the field control, rendered as the label's `for`. */
22
+ id?: string | null
23
+ /** Id of the `<label>` element itself, for `aria-labelledby`. */
24
+ labelId?: string | null
25
+ text?: string
26
+ required?: boolean
27
+ success?: boolean
28
+ error?: boolean
29
+ disabled?: boolean
30
+ /** Shows "(Opcional)" when the field is not required. Defaults to true. */
31
+ showOptionalLabel?: boolean
32
+ }
33
+
34
+ const {
35
+ id,
36
+ labelId,
37
+ text,
38
+ required = false,
39
+ success = false,
40
+ error = false,
41
+ disabled = false,
42
+ showOptionalLabel = true,
43
+ class: className,
44
+ ...rest
45
+ } = Astro.props
46
+
47
+ const slotted = Astro.slots.has('default')
48
+ ? (await Astro.slots.render('default')).trim()
49
+ : ''
50
+ const hasText = !!text || !!slotted
51
+ // Same precedence as React: success, then error, then disabled.
52
+ const state = success ? 'success' : error ? 'error' : disabled ? 'disabled' : null
53
+ ---
54
+
55
+ {
56
+ hasText && (
57
+ <div class:list={['au-field__header', className]} {...rest}>
58
+ <label id={labelId} for={id} class="au-field__header-label">
59
+ {text}
60
+ <Fragment set:html={slotted} />{' '}
61
+ {showOptionalLabel && !required && (
62
+ <span class="au-field__header-label--optional">(Opcional)</span>
63
+ )}
64
+ {!showOptionalLabel && required && (
65
+ <Fragment>
66
+ <strong class="au-field__header-label--required" aria-hidden="true">
67
+ *
68
+ </strong>
69
+ <span class="au-sr-only">obrigatório</span>
70
+ </Fragment>
71
+ )}
72
+ </label>
73
+ <div class="au-field__header-icon" aria-hidden="true">
74
+ {state === 'success' && <IconCheck rawColor={COLOR_SUCCESS_50} />}
75
+ {state === 'error' && <IconAlertOctagon rawColor={COLOR_ERROR_50} />}
76
+ {state === 'disabled' && <IconSlash rawColor={COLOR_NEUTRAL_40} />}
77
+ </div>
78
+ </div>
79
+ )
80
+ }
@@ -0,0 +1,29 @@
1
+ ---
2
+ import '../../../../dist/components/Field/styles2.css'
3
+
4
+ /**
5
+ * Same props as the React `Field.Message`: the error wins over the help text,
6
+ * and nothing renders when neither applies.
7
+ */
8
+ export type Props = {
9
+ hasError?: boolean
10
+ errorMessage?: string
11
+ helpMessage?: string
12
+ errorId?: string
13
+ helpId?: string
14
+ }
15
+
16
+ const { hasError, errorMessage, helpMessage, errorId, helpId } = Astro.props
17
+ ---
18
+
19
+ {
20
+ hasError && errorMessage ? (
21
+ <p id={errorId} class="au-field__message au-field__message--error" role="alert">
22
+ {errorMessage}
23
+ </p>
24
+ ) : helpMessage ? (
25
+ <p id={helpId} class="au-field__message">
26
+ {helpMessage}
27
+ </p>
28
+ ) : null
29
+ }
@@ -0,0 +1,37 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import '../../../../dist/components/Field/styles2.css'
4
+
5
+ /**
6
+ * Same states as the React `Field.Root`. The React `customclass` is the plain
7
+ * `class` here, and every other HTML attribute (`id`, `style`, `data-*`) lands
8
+ * on the root `div`.
9
+ */
10
+ export type Props = HTMLAttributes<'div'> & {
11
+ disabled?: boolean
12
+ success?: boolean
13
+ error?: boolean
14
+ }
15
+
16
+ const {
17
+ disabled = false,
18
+ success = false,
19
+ error = false,
20
+ class: className,
21
+ 'class:list': classList,
22
+ ...rest
23
+ } = Astro.props
24
+ ---
25
+
26
+ <div
27
+ class:list={[
28
+ 'au-field',
29
+ className,
30
+ classList,
31
+ disabled && 'au-field--disabled',
32
+ success && 'au-field--success',
33
+ error && 'au-field--error',
34
+ ]}
35
+ {...rest}>
36
+ <slot />
37
+ </div>
@@ -0,0 +1,69 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import Text from '../../../Text/index.astro'
4
+ import '../../../../dist/components/Field/styles2.css'
5
+
6
+ /**
7
+ * Every HTML attribute lands on the `<textarea>`; the React `customClass` is
8
+ * the plain `class`, and `textareaRef` has no counterpart. `value` becomes the
9
+ * textarea's initial content. With `maxLength` the character counter is kept
10
+ * up to date by an inline controller on every `input`; like React, it starts
11
+ * at 0 even when there is an initial value.
12
+ */
13
+ export type Props = Omit<HTMLAttributes<'textarea'>, 'maxlength'> & {
14
+ /** Allows the user to resize the textarea horizontally. Not mapped in Figma. */
15
+ horizontalResize?: boolean
16
+ /** Native `maxlength`; also turns the `n/max` counter on. */
17
+ maxLength?: number
18
+ }
19
+
20
+ const {
21
+ horizontalResize = false,
22
+ maxLength,
23
+ value,
24
+ class: className,
25
+ 'class:list': classList,
26
+ ...rest
27
+ } = Astro.props
28
+
29
+ const initialValue = value == null ? '' : String(value)
30
+ ---
31
+
32
+ <div
33
+ class="au-field__textarea-container"
34
+ data-element={maxLength ? 'au-field-textarea' : undefined}>
35
+ <textarea
36
+ class:list={[
37
+ 'au-field__textarea',
38
+ className,
39
+ classList,
40
+ horizontalResize && 'au-field__textarea--horizontal-resize',
41
+ ]}
42
+ maxlength={maxLength}
43
+ {...rest}>{initialValue}</textarea>
44
+ {
45
+ maxLength && (
46
+ <Text variant="caption" class="au-field__char-counter" aria-live="polite">
47
+ {0}/{maxLength}
48
+ </Text>
49
+ )
50
+ }
51
+ </div>
52
+
53
+ <script>
54
+ import { elementController } from '@consumidor-positivo/aurora/astro/runtime'
55
+
56
+ elementController('au-field-textarea', ({ query, on }) => {
57
+ const textarea = query<HTMLTextAreaElement>('.au-field__textarea')
58
+ const counter = query('.au-field__char-counter')
59
+ if (!textarea || !counter) return
60
+
61
+ const update = () => {
62
+ counter.textContent = `${textarea.value.length}/${textarea.maxLength}`
63
+ }
64
+
65
+ // Like the React hook: only a change updates the count (not the initial
66
+ // value, not a form reset).
67
+ on('input', update)
68
+ })
69
+ </script>
@@ -0,0 +1,96 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types'
3
+ import FieldRoot from '../Field/Root/index.astro'
4
+ import FieldLabel from '../Field/Label/index.astro'
5
+ import FieldInputHolder from '../Field/InputHolder/index.astro'
6
+ import FieldInput from '../Field/Input/index.astro'
7
+ import FieldMessage from '../Field/Message/index.astro'
8
+
9
+ /**
10
+ * Same props as the React `InputField`, and the same split: `class` and
11
+ * `style` go on the `au-field` root, every other HTML attribute (`name`,
12
+ * `value`, `type`, `required`, `placeholder`, `data-*`...) lands on the
13
+ * `<input>`, as the React spreads its input props.
14
+ *
15
+ * Differences from React: `rightSlot` is the `right` named slot and a rich
16
+ * `label` goes in the `label` slot; `inputRef` and the `on*` callbacks have no
17
+ * counterpart (listen on the input through its `id` or a `data-*`). Like
18
+ * React, without `id` the label has no `for` and the input no `id`; the
19
+ * error/help message ids are generated (React's `useId`).
20
+ */
21
+ export type Props = Omit<HTMLAttributes<'input'>, 'disabled'> & {
22
+ showOptionalLabel?: boolean
23
+ /** Marks the label as required. Pass `required` too for native validation. */
24
+ requiredInput?: boolean
25
+ success?: boolean
26
+ error?: boolean
27
+ errorMessage?: string
28
+ helpMessage?: string
29
+ label?: string
30
+ disabled?: boolean
31
+ /** Inline style of the `<input>` (`style` goes on the root). */
32
+ inputStyle?: HTMLAttributes<'input'>['style']
33
+ /** Opens the numeric keypad on mobile (`inputmode="numeric"`). */
34
+ numericKeypad?: boolean
35
+ }
36
+
37
+ const uid = crypto.randomUUID().slice(0, 8)
38
+
39
+ const {
40
+ showOptionalLabel,
41
+ requiredInput,
42
+ success = false,
43
+ error = false,
44
+ errorMessage,
45
+ helpMessage,
46
+ label,
47
+ id,
48
+ disabled = false,
49
+ style,
50
+ class: className,
51
+ inputStyle,
52
+ numericKeypad = false,
53
+ ...rest
54
+ } = Astro.props
55
+
56
+ const errorId = error && errorMessage ? `au-field-${uid}-error` : undefined
57
+ const helpId = helpMessage ? `au-field-${uid}-help` : undefined
58
+ const describedBy = [errorId, helpId].filter(Boolean).join(' ') || undefined
59
+ ---
60
+
61
+ <FieldRoot
62
+ style={style}
63
+ class={className}
64
+ success={success}
65
+ error={error}
66
+ disabled={disabled}>
67
+ <FieldLabel
68
+ text={label}
69
+ id={id}
70
+ showOptionalLabel={showOptionalLabel}
71
+ required={requiredInput}
72
+ success={success}
73
+ error={error}
74
+ disabled={disabled}>
75
+ <slot name="label" />
76
+ </FieldLabel>
77
+ <FieldInputHolder>
78
+ <FieldInput
79
+ id={id}
80
+ inputmode={numericKeypad ? 'numeric' : undefined}
81
+ disabled={disabled}
82
+ style={inputStyle}
83
+ aria-invalid={error ? 'true' : 'false'}
84
+ aria-describedby={describedBy}
85
+ {...rest}
86
+ />
87
+ <slot name="right" slot="right" />
88
+ </FieldInputHolder>
89
+ <FieldMessage
90
+ hasError={error}
91
+ errorMessage={errorMessage}
92
+ helpMessage={helpMessage}
93
+ errorId={errorId}
94
+ helpId={helpId}
95
+ />
96
+ </FieldRoot>