@stonecrop/aform 0.21.0 → 0.23.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.
@@ -0,0 +1,299 @@
1
+ <template>
2
+ <div class="adate_time">
3
+ <div class="adate_time_fields">
4
+ <input
5
+ v-model="timeData.hours"
6
+ type="text"
7
+ inputmode="numeric"
8
+ @paste="pasteInput($event, true)"
9
+ @focus="focusInput"
10
+ @blur="confirmTime"
11
+ @keydown.enter.prevent="confirmTime"
12
+ @keydown.up.prevent="tick('hours')"
13
+ @keydown.down.prevent="tick('hours', -1)" />
14
+ <span class="colon">:</span>
15
+ <input
16
+ v-model="timeData.minutes"
17
+ type="text"
18
+ inputmode="numeric"
19
+ @paste="pasteInput"
20
+ @focus="focusInput"
21
+ @blur="confirmTime"
22
+ @keydown.enter.prevent="confirmTime"
23
+ @keydown.up.prevent="tick('minutes')"
24
+ @keydown.down.prevent="tick('minutes', -1)" />
25
+ <span v-if="useSeconds" class="colon">:</span>
26
+ <input
27
+ v-if="useSeconds"
28
+ v-model="timeData.seconds"
29
+ type="text"
30
+ inputmode="numeric"
31
+ @paste="pasteInput"
32
+ @focus="focusInput"
33
+ @blur="confirmTime"
34
+ @keydown.enter.prevent="confirmTime"
35
+ @keydown.up.prevent="tick('seconds')"
36
+ @keydown.down.prevent="tick('seconds', -1)" />
37
+ <select
38
+ v-if="!allowMilitaryTime"
39
+ ref="meridiem-selector"
40
+ v-model="meridiem"
41
+ class="aform-select meridiem-selector"
42
+ @change="confirmTime">
43
+ <option value="AM">AM</option>
44
+ <option value="PM">PM</option>
45
+ </select>
46
+ </div>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { ref, reactive, useTemplateRef, watch, onMounted } from 'vue'
52
+
53
+ const {
54
+ allowMilitaryTime = false,
55
+ defaultHours = 12,
56
+ defaultMinutes = 0,
57
+ defaultSeconds = 0,
58
+ defaultMeridiem = 'AM',
59
+ useSeconds = true,
60
+ } = defineProps<{
61
+ allowMilitaryTime?: boolean
62
+ defaultHours?: number
63
+ defaultMinutes?: number
64
+ defaultSeconds?: number
65
+ defaultMeridiem?: string
66
+ useSeconds?: boolean
67
+ }>()
68
+
69
+ // `get-time` carries the widget's CURRENT VALUE, not a commit: it fires once as the widget
70
+ // mounts and again on every blur, Enter, arrow key and meridiem change — five times during one
71
+ // ordinary edit. `source` is what lets a parent tell the start-up announcement apart from a real
72
+ // user edit; without it a parent has nothing to key on and must guess from the value alone.
73
+ const emit = defineEmits<{
74
+ 'get-time': [
75
+ {
76
+ hours: number
77
+ minutes: number
78
+ seconds: number
79
+ meridiem: string
80
+ militaryTime: number
81
+ source: 'init' | 'user'
82
+ },
83
+ ]
84
+ }>()
85
+
86
+ const meridiemSelector = useTemplateRef<HTMLSelectElement>('meridiem-selector')
87
+
88
+ const timeData = reactive({
89
+ hours: String(defaultHours).padStart(2, '0'),
90
+ minutes: String(defaultMinutes).padStart(2, '0'),
91
+ seconds: String(defaultSeconds).padStart(2, '0'),
92
+ })
93
+
94
+ const meridiem = ref(defaultMeridiem == 'AM' ? 'AM' : 'PM')
95
+
96
+ onMounted(() => {
97
+ emitTime('init')
98
+ })
99
+
100
+ const confirmTime = () => {
101
+ const maxHours = allowMilitaryTime ? 23 : 12
102
+ const minHours = allowMilitaryTime ? 0 : 1
103
+ let hours = Number(timeData.hours)
104
+ let minutes = Number(timeData.minutes)
105
+ let seconds = Number(timeData.seconds)
106
+
107
+ if (isNaN(hours) || timeData.hours === '' || hours > maxHours) hours = maxHours
108
+ if (!allowMilitaryTime && hours < minHours) hours = minHours
109
+ if (isNaN(minutes) || timeData.minutes === '' || minutes > 59) minutes = 59
110
+ if (isNaN(seconds) || timeData.seconds === '' || seconds > 59) seconds = 59
111
+
112
+ timeData.hours = String(hours).padStart(2, '0')
113
+ timeData.minutes = String(minutes).padStart(2, '0')
114
+ timeData.seconds = String(seconds).padStart(2, '0')
115
+
116
+ emitTime('user')
117
+ }
118
+
119
+ const emitTime = (source: 'init' | 'user') => {
120
+ const hours = Number(timeData.hours)
121
+ const minutes = Number(timeData.minutes)
122
+ const seconds = Number(timeData.seconds)
123
+ emit('get-time', {
124
+ hours,
125
+ minutes,
126
+ seconds,
127
+ source,
128
+ meridiem: meridiem.value,
129
+ militaryTime: allowMilitaryTime ? hours : meridiem.value === 'PM' ? (hours === 12 ? 12 : hours + 12) : hours % 12,
130
+ })
131
+ }
132
+
133
+ const focusInput = (event: FocusEvent) => {
134
+ const target = event.target
135
+ if (target instanceof HTMLInputElement) {
136
+ target.select()
137
+ }
138
+ }
139
+
140
+ const tick = (target: 'hours' | 'minutes' | 'seconds', amount = 1) => {
141
+ const maxHours = allowMilitaryTime ? 23 : 12
142
+ const minHours = allowMilitaryTime ? 0 : 1
143
+
144
+ if (target == 'hours') {
145
+ const oldHours = Number(timeData.hours)
146
+ timeData.hours = String(oldHours + amount)
147
+ if ((oldHours == 11 && Number(timeData.hours) == 12) || (oldHours == 12 && Number(timeData.hours) == 11)) {
148
+ changeMeridiem()
149
+ }
150
+ } else if (target == 'minutes') {
151
+ timeData.minutes = String(Number(timeData.minutes) + amount)
152
+ } else if (target == 'seconds') {
153
+ timeData.seconds = String(Number(timeData.seconds) + amount)
154
+ }
155
+
156
+ const prevHours = Number(timeData.hours)
157
+
158
+ if (Number(timeData.seconds) < 0) timeData.minutes = String(Number(timeData.minutes) - 1)
159
+ else if (Number(timeData.seconds) > 59) timeData.minutes = String(Number(timeData.minutes) + 1)
160
+
161
+ if (Number(timeData.minutes) < 0) timeData.hours = String(prevHours - 1)
162
+ else if (Number(timeData.minutes) > 59) timeData.hours = String(prevHours + 1)
163
+
164
+ const newRawHours = Number(timeData.hours)
165
+ if (!allowMilitaryTime && newRawHours !== prevHours) {
166
+ if ((prevHours === 11 && newRawHours === 12) || (prevHours === 12 && newRawHours === 11)) {
167
+ changeMeridiem()
168
+ }
169
+ }
170
+
171
+ timeData.hours = String(formatTime(Number(timeData.hours), minHours, maxHours)).padStart(2, '0')
172
+ timeData.minutes = String(formatTime(Number(timeData.minutes), 0, 59)).padStart(2, '0')
173
+ timeData.seconds = String(formatTime(Number(timeData.seconds), 0, 59)).padStart(2, '0')
174
+ }
175
+
176
+ watch(
177
+ () => timeData.hours,
178
+ (newVal, oldVal) => {
179
+ timeData.hours = Number(newVal) > 99 ? oldVal : newVal
180
+ }
181
+ )
182
+ watch(
183
+ () => timeData.minutes,
184
+ (newVal, oldVal) => {
185
+ timeData.minutes = Number(newVal) > 99 ? oldVal : newVal
186
+ }
187
+ )
188
+ watch(
189
+ () => timeData.seconds,
190
+ (newVal, oldVal) => {
191
+ timeData.seconds = Number(newVal) > 99 ? oldVal : newVal
192
+ }
193
+ )
194
+
195
+ const formatTime = (target: number, min: number, max: number): number => {
196
+ if (target > max) return min
197
+ else if (target < min) return max
198
+ return target
199
+ }
200
+
201
+ const changeMeridiem = () => {
202
+ meridiem.value = meridiem.value == 'PM' ? 'AM' : 'PM'
203
+ emitTime('user')
204
+ }
205
+
206
+ const pasteInput = (event: ClipboardEvent, pasteAllFields = false) => {
207
+ event.stopPropagation()
208
+ event.preventDefault()
209
+
210
+ const clipboardData = event.clipboardData
211
+ if (!clipboardData) return
212
+ let pastedData: string = clipboardData.getData('Text')
213
+
214
+ pastedData = pastedData.replace(/[^0-9]/g, '')
215
+
216
+ if (pasteAllFields) {
217
+ if (pastedData.length % 2 != 0) pastedData = '0' + pastedData
218
+ if (pastedData.length < 3) pastedData += '00'
219
+ if (pastedData.length < 5) pastedData += '00'
220
+
221
+ const timeUnits = pastedData.match(/(..?)/g)
222
+ if (!timeUnits || timeUnits.length < 3) return
223
+
224
+ timeData.seconds = timeUnits[2]
225
+ timeData.minutes = timeUnits[1]
226
+ timeData.hours = timeUnits[0]
227
+ confirmTime()
228
+ if (!allowMilitaryTime) meridiemSelector.value?.focus()
229
+ } else {
230
+ if (pastedData.length > 2) pastedData = pastedData.slice(0, 2)
231
+ const target = event.target
232
+ if (target instanceof HTMLInputElement) {
233
+ target.value = pastedData
234
+ target.dispatchEvent(new Event('input'))
235
+ }
236
+ }
237
+ }
238
+ </script>
239
+
240
+ <style scoped>
241
+ .adate_time {
242
+ width: auto;
243
+ padding: 10px;
244
+ box-sizing: border-box;
245
+ font-size: 1rem;
246
+ background: var(--sc-gray-10);
247
+ }
248
+ .adate_time_fields {
249
+ display: flex;
250
+ align-items: stretch;
251
+ gap: 5px;
252
+ justify-content: flex-start;
253
+ }
254
+ .adate_time_fields > input {
255
+ min-width: 30px;
256
+ padding: 2px;
257
+ text-align: center;
258
+ display: inline-block;
259
+ flex-basis: 0;
260
+ }
261
+ .meridiem-selector {
262
+ cursor: pointer;
263
+ display: inline-block;
264
+ flex-basis: 0;
265
+ padding: 5px;
266
+ user-select: none;
267
+ }
268
+ .meridiem-selector:focus {
269
+ outline: 2px solid black;
270
+ outline-offset: -2px;
271
+ }
272
+ .adate_time_segment {
273
+ display: flex;
274
+ flex-direction: column;
275
+ width: 40px;
276
+ }
277
+ .colon {
278
+ display: flex;
279
+ align-items: normal;
280
+ }
281
+ .aform_form-btn {
282
+ cursor: pointer;
283
+ }
284
+ .aform-select {
285
+ border-radius: 0px;
286
+ border: 1px solid rgb(118, 118, 118);
287
+ font-size: 1rem;
288
+ padding: 0rem;
289
+ margin: 0;
290
+ border-radius: 0;
291
+ box-sizing: border-box;
292
+ min-height: auto;
293
+ position: relative;
294
+ color: var(--sc-cell-text-color);
295
+ }
296
+ .meridiem-selector {
297
+ margin-left: 6px;
298
+ }
299
+ </style>
@@ -1,50 +1,62 @@
1
1
  <template>
2
- <div v-if="mode === 'display'" class="input-wrapper">
3
- <span class="aform_display-value">{{ search ?? '' }}</span>
4
- <label>{{ label }}</label>
5
- <p v-show="errorText" class="aform_error" v-html="errorText"></p>
6
- </div>
7
- <div v-else v-on-click-outside="onClickOutside" class="autocomplete" :class="{ isOpen: dropdown.open }">
8
- <div class="input-wrapper">
9
- <input
10
- v-model="search"
11
- type="text"
12
- :disabled="mode === 'read'"
13
- @input="filter"
14
- @focus="openDropdown"
15
- @keydown.down="selectNextResult"
16
- @keydown.up="selectPrevResult"
17
- @keydown.enter="setCurrentResult"
18
- @keydown.esc="onClickOutside"
19
- @keydown.tab="onClickOutside" />
20
-
21
- <ul v-show="dropdown.open" id="autocomplete-results" class="autocomplete-results">
22
- <li v-if="dropdown.loading" class="loading autocomplete-result">Loading results...</li>
23
- <li
24
- v-for="(result, i) in dropdown.results"
25
- v-else
26
- :key="result"
27
- class="autocomplete-result"
28
- :class="{ 'is-active': i === dropdown.activeItemIndex }"
29
- @click.stop="setResult(result)">
30
- {{ result }}
31
- </li>
32
- </ul>
33
- <label>{{ label }}</label>
34
- </div>
35
- <p v-show="errorText" class="aform_error" v-html="errorText"></p>
2
+ <div class="aform_form-element">
3
+ <template v-if="mode === 'display'">
4
+ <span v-if="badgeDescriptor" class="aform_display-value" :style="displayAccentStyle">{{
5
+ badgeDescriptor.label
6
+ }}</span>
7
+ <span v-else class="aform_display-value">{{ search ?? '' }}</span>
8
+ <label class="aform_field-label">{{ label }}</label>
9
+ </template>
10
+ <template v-else>
11
+ <div v-on-click-outside="onClickOutside" class="autocomplete" :class="{ isOpen: dropdown.open }">
12
+ <input
13
+ v-model="search"
14
+ type="text"
15
+ class="aform_input-field"
16
+ :disabled="mode === 'read'"
17
+ :style="inputAccentStyle"
18
+ @input="filter"
19
+ @focus="openDropdown"
20
+ @keydown.down="selectNextResult"
21
+ @keydown.up="selectPrevResult"
22
+ @keydown.enter="setCurrentResult"
23
+ @keydown.esc="onClickOutside"
24
+ @keydown.tab="onClickOutside" />
25
+
26
+ <ul v-show="dropdown.open" id="autocomplete-results" class="autocomplete-results">
27
+ <li v-if="dropdown.loading" class="loading autocomplete-result">Loading results...</li>
28
+ <li
29
+ v-for="(result, i) in dropdown.results"
30
+ v-else
31
+ :key="result"
32
+ class="autocomplete-result"
33
+ :class="{ 'is-active': i === dropdown.activeItemIndex }"
34
+ @click.stop="setResult(result)">
35
+ {{ result }}
36
+ </li>
37
+ </ul>
38
+ <label class="aform_field-label">{{ label }}</label>
39
+ </div>
40
+ <p v-show="errorText" class="aform_error" v-html="errorText"></p>
41
+ </template>
36
42
  </div>
37
43
  </template>
38
44
 
39
45
  <script setup lang="ts">
46
+ import type { FieldOptions } from '@stonecrop/schema'
47
+ import { selectChoices } from '@stonecrop/schema'
40
48
  import { vOnClickOutside } from '@vueuse/components'
41
- import { computed, reactive, ref } from 'vue'
49
+ import { computed, reactive, ref, watch } from 'vue'
42
50
 
43
51
  import type { ComponentProps } from '../../types'
52
+ import type { BadgeFormatFn } from '../../utils/badge'
53
+ import { badgeInputAccentStyle, resolveFieldBadge } from '../../utils/badge'
54
+ import { deserializeFunction } from '../../utils/deserialize'
44
55
 
45
56
  const {
46
57
  label,
47
58
  options = [],
59
+ format,
48
60
  isAsync = false,
49
61
  filterFunction = undefined,
50
62
  mode,
@@ -52,26 +64,46 @@ const {
52
64
  validation = { errorMessage: '' },
53
65
  } = defineProps<
54
66
  ComponentProps & {
55
- options?: string[]
67
+ options?: FieldOptions
68
+ format?: string
56
69
  isAsync?: boolean
57
70
  filterFunction?: (search: string) => string[] | Promise<string[]>
58
71
  }
59
72
  >()
60
73
 
61
- // Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
74
+ const choiceList = computed(() => selectChoices(options))
75
+
76
+ // Compile the serialized formatter once per `format` string, not once per keystroke:
77
+ // badgeDescriptor re-evaluates on every input event and deserializeFunction runs the
78
+ // Function constructor.
79
+ const formatFn = computed(() => (format ? deserializeFunction<BadgeFormatFn>(format) : undefined))
80
+
81
+ const badgeDescriptor = computed(() => resolveFieldBadge(search.value, options, formatFn.value))
82
+
83
+ const inputAccentStyle = computed(() => badgeInputAccentStyle(badgeDescriptor.value))
84
+
85
+ const displayAccentStyle = computed(() => badgeInputAccentStyle(badgeDescriptor.value))
86
+
62
87
  const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
63
88
  const search = defineModel<string>()
64
89
 
65
- // tracks the last explicitly-committed value so outside-click reverts instead of clears
66
90
  const committedValue = ref(search.value ?? '')
67
91
 
68
92
  const dropdown = reactive({
69
93
  activeItemIndex: null as number | null,
70
94
  open: false,
71
95
  loading: false,
72
- results: options,
96
+ results: [] as string[],
73
97
  })
74
98
 
99
+ watch(
100
+ choiceList,
101
+ choices => {
102
+ dropdown.results = choices
103
+ },
104
+ { immediate: true }
105
+ )
106
+
75
107
  const onClickOutside = () => closeDropdown()
76
108
 
77
109
  const filter = async () => {
@@ -99,26 +131,25 @@ const setResult = (result: string) => {
99
131
  }
100
132
 
101
133
  const openDropdown = () => {
102
- const idx = options?.indexOf(search.value ?? '') ?? -1
134
+ const idx = choiceList.value.indexOf(search.value ?? '')
103
135
  dropdown.activeItemIndex = isAsync ? null : idx >= 0 ? idx : null
104
136
  dropdown.open = true
105
- // TODO: this should probably call the async function if it's async
106
- dropdown.results = isAsync ? [] : options
137
+ dropdown.results = isAsync ? [] : choiceList.value
107
138
  }
108
139
 
109
140
  const closeDropdown = (result?: string) => {
110
141
  dropdown.activeItemIndex = null
111
142
  dropdown.open = false
112
- if (!options?.includes(result || search.value || '')) {
143
+ if (!choiceList.value.includes(result || search.value || '')) {
113
144
  search.value = committedValue.value
114
145
  }
115
146
  }
116
147
 
117
148
  const filterResults = () => {
118
149
  if (!search.value) {
119
- dropdown.results = options
150
+ dropdown.results = choiceList.value
120
151
  } else {
121
- dropdown.results = options?.filter(item => item.toLowerCase().includes((search.value ?? '').toLowerCase()))
152
+ dropdown.results = choiceList.value.filter(item => item.toLowerCase().includes((search.value ?? '').toLowerCase()))
122
153
  }
123
154
  }
124
155
 
@@ -157,65 +188,26 @@ const setCurrentResult = () => {
157
188
  </script>
158
189
 
159
190
  <style scoped>
160
- /* variables taken from here: https://github.com/frappe/frappe/blob/version-13/frappe/public/scss/common/awesomeplete.scss */
161
191
  .autocomplete {
162
192
  position: relative;
163
193
  }
164
194
 
165
- .input-wrapper {
166
- border: 1px solid transparent;
167
- padding: 0rem;
168
- margin: 0rem;
169
- margin-right: 1ch;
170
- }
171
-
172
- input {
173
- width: calc(100% - 1ch);
174
- outline: 1px solid transparent;
175
- border: 1px solid var(--sc-input-border-color);
176
- padding: 1ch 0.5ch 0.5ch 1ch;
177
- margin: calc(1.15rem / 2) 0 0 0;
178
- min-height: 1.15rem;
179
- border-radius: 0.25rem;
180
- font-family: var(--sc-font-family);
181
- }
182
-
183
- input:focus {
184
- border: 1px solid var(--sc-input-active-border-color);
185
- border-radius: 0.25rem 0.25rem 0 0;
186
- border-bottom: none;
187
- }
188
-
189
- label {
190
- display: block;
191
- min-height: 1.15rem;
192
- padding: 0rem;
193
- margin: 0rem;
194
- border: 1px solid transparent;
195
- margin-bottom: 0.25rem;
196
- z-index: 0;
197
- font-size: 80%;
198
- position: absolute;
199
- background: white;
200
- margin: calc(-1.5rem - calc(2.15rem / 2)) 0 0 1ch;
201
- padding: 0 0.25ch 0 0.25ch;
202
- }
203
-
204
195
  .autocomplete-results {
205
196
  position: absolute;
206
- width: calc(100% - 1ch + 1.5px);
197
+ left: 0;
198
+ right: 0;
207
199
  z-index: 100;
208
200
  padding: 0;
209
201
  margin: 0;
210
202
  color: var(--sc-input-active-border-color);
211
203
  border: 1px solid var(--sc-input-active-border-color);
212
- border-radius: 0 0 0.25rem 0.25rem;
204
+ border-radius: 0;
213
205
  border-top: none;
214
- background-color: #fff;
206
+ background-color: var(--sc-input-field-background, #fff);
207
+ list-style: none;
215
208
  }
216
209
 
217
210
  .autocomplete-result {
218
- list-style: none;
219
211
  text-align: left;
220
212
  padding: 4px 6px;
221
213
  cursor: pointer;
@@ -227,14 +219,4 @@ label {
227
219
  background-color: var(--sc-row-color-zebra-light);
228
220
  color: var(--sc-input-active-border-color);
229
221
  }
230
-
231
- /* Keep the field error in-flow below the control. The shared .aform_error is absolutely
232
- positioned against a .aform_form-element anchor, which this component does not use. */
233
- p.aform_error {
234
- position: static;
235
- display: block;
236
- color: var(--sc-brand-danger, red);
237
- font-size: 0.7rem;
238
- margin: 0.25rem 0 0;
239
- }
240
222
  </style>
@@ -57,7 +57,12 @@ watch(duration, newMs => {
57
57
  modelValue.value = newMs
58
58
  })
59
59
 
60
- const handleRange = (data: { start: Date; end: Date }) => {
60
+ const handleRange = (data: { start: Date; end: Date; source?: 'init' | 'user' }) => {
61
+ // Both time widgets announce their starting values as they mount, which ADateSelection turns
62
+ // into a range. That is not a range the user picked: acting on it wrote a 0ms duration into the
63
+ // model, and lit the summary strip, on first render.
64
+ if (data.source === 'init') return
65
+
61
66
  startDatetime.value = data.start
62
67
  endDatetime.value = data.end
63
68
  modelValue.value = duration.value
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export type * from '@stonecrop/atable/types'
2
2
  export { deserializeFunction } from './utils/deserialize'
3
+ export { badgeInputAccentStyle, resolveFieldBadge } from './utils/badge'
4
+ export type { BadgeFormatContext, BadgeFormatFn } from './utils/badge'
3
5
  import { install as installATable } from '@stonecrop/atable'
4
6
  import type { App } from 'vue'
5
7
 
@@ -7,8 +9,10 @@ import ACheckbox from './components/form/ACheckbox.vue'
7
9
  import ACurrencyInput from './components/form/ACurrencyInput.vue'
8
10
  import ADate from './components/form/ADate.vue'
9
11
  import ADropdown from './components/form/ADropdown.vue'
12
+ import ABadge from './components/form/ABadge.vue'
10
13
  import ADatePicker from './components/form/ADatePicker.vue'
11
14
  import ADateTime from './components/form/ADateTime.vue'
15
+ import ADateTimeInput from './components/form/ADateTimeInput.vue'
12
16
  import ADateSelection from './components/form/ADateSelection.vue'
13
17
  import ADuration from './components/form/ADuration.vue'
14
18
  import ADateRange from './components/form/ADateRange.vue'
@@ -37,8 +41,10 @@ function install(app: App /* options */) {
37
41
  app.component('ACurrencyInput', ACurrencyInput)
38
42
  app.component('ADate', ADate)
39
43
  app.component('ADropdown', ADropdown)
44
+ app.component('ABadge', ABadge)
40
45
  app.component('ADatePicker', ADatePicker)
41
46
  app.component('ADateTime', ADateTime)
47
+ app.component('ADateTimeInput', ADateTimeInput)
42
48
  app.component('ADateRange', ADateRange)
43
49
  app.component('ADateSelection', ADateSelection)
44
50
  app.component('AFieldset', AFieldset)
@@ -58,11 +64,13 @@ export {
58
64
  ACurrencyInput,
59
65
  ADate,
60
66
  ADropdown,
67
+ ABadge,
61
68
  ADatePicker,
62
69
  ADateRange,
63
70
  ADateSelection,
64
71
  ADuration,
65
72
  ADateTime,
73
+ ADateTimeInput,
66
74
  AFieldset,
67
75
  AFileAttach,
68
76
  AForm,