@stonecrop/aform 0.31.0 → 0.32.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 (92) hide show
  1. package/dist/aform.js +3024 -2256
  2. package/dist/aform.js.map +1 -1
  3. package/dist/assets/index.css +1170 -1
  4. package/dist/src/components/AForm.vue.d.ts +20 -0
  5. package/dist/src/components/AForm.vue.d.ts.map +1 -0
  6. package/dist/src/components/AFormLoading.vue.d.ts +14 -0
  7. package/dist/src/components/AFormLoading.vue.d.ts.map +1 -0
  8. package/dist/src/components/base/CollapseButton.vue.d.ts +7 -0
  9. package/dist/src/components/base/CollapseButton.vue.d.ts.map +1 -0
  10. package/dist/src/components/base/ExpandButton.vue.d.ts +7 -0
  11. package/dist/src/components/base/ExpandButton.vue.d.ts.map +1 -0
  12. package/dist/src/components/form/ABadge.vue.d.ts +14 -0
  13. package/dist/src/components/form/ABadge.vue.d.ts.map +1 -0
  14. package/dist/src/components/form/ACheckbox.vue.d.ts +15 -0
  15. package/dist/src/components/form/ACheckbox.vue.d.ts.map +1 -0
  16. package/dist/src/components/form/ACurrencyInput.vue.d.ts +20 -0
  17. package/dist/src/components/form/ACurrencyInput.vue.d.ts.map +1 -0
  18. package/dist/src/components/form/ADate.vue.d.ts +14 -0
  19. package/dist/src/components/form/ADate.vue.d.ts.map +1 -0
  20. package/dist/src/components/form/ADatePicker.vue.d.ts +28 -0
  21. package/dist/src/components/form/ADatePicker.vue.d.ts.map +1 -0
  22. package/dist/src/components/form/ADateRange.vue.d.ts +18 -0
  23. package/dist/src/components/form/ADateRange.vue.d.ts.map +1 -0
  24. package/dist/src/components/form/ADateSelection.vue.d.ts +52 -0
  25. package/dist/src/components/form/ADateSelection.vue.d.ts.map +1 -0
  26. package/dist/src/components/form/ADateTime.vue.d.ts +17 -0
  27. package/dist/src/components/form/ADateTime.vue.d.ts.map +1 -0
  28. package/dist/src/components/form/ADateTimeInput.vue.d.ts +30 -0
  29. package/dist/src/components/form/ADateTimeInput.vue.d.ts.map +1 -0
  30. package/dist/src/components/form/ADropdown.vue.d.ts +20 -0
  31. package/dist/src/components/form/ADropdown.vue.d.ts.map +1 -0
  32. package/dist/src/components/form/ADuration.vue.d.ts +18 -0
  33. package/dist/src/components/form/ADuration.vue.d.ts.map +1 -0
  34. package/dist/src/components/form/AFieldset.vue.d.ts +28 -0
  35. package/dist/src/components/form/AFieldset.vue.d.ts.map +1 -0
  36. package/dist/src/components/form/AFileAttach.vue.d.ts +5 -0
  37. package/dist/src/components/form/AFileAttach.vue.d.ts.map +1 -0
  38. package/dist/src/components/form/AFormLink.vue.d.ts +40 -0
  39. package/dist/src/components/form/AFormLink.vue.d.ts.map +1 -0
  40. package/dist/src/components/form/ANumericInput.vue.d.ts +14 -0
  41. package/dist/src/components/form/ANumericInput.vue.d.ts.map +1 -0
  42. package/dist/src/components/form/AQuantityInput.vue.d.ts +20 -0
  43. package/dist/src/components/form/AQuantityInput.vue.d.ts.map +1 -0
  44. package/dist/src/components/form/ASegmentedControl.vue.d.ts +23 -0
  45. package/dist/src/components/form/ASegmentedControl.vue.d.ts.map +1 -0
  46. package/dist/src/components/form/ATextInput.vue.d.ts +14 -0
  47. package/dist/src/components/form/ATextInput.vue.d.ts.map +1 -0
  48. package/dist/src/components/form/ATextboxInput.vue.d.ts +21 -0
  49. package/dist/src/components/form/ATextboxInput.vue.d.ts.map +1 -0
  50. package/dist/src/components/utilities/Login.vue.d.ts +14 -0
  51. package/dist/src/components/utilities/Login.vue.d.ts.map +1 -0
  52. package/dist/{tsdoc-metadata.json → src/tsdoc-metadata.json} +1 -1
  53. package/package.json +35 -30
  54. package/dist/aform.d.ts +0 -441
  55. package/dist/aform.tsbuildinfo +0 -1
  56. package/dist/src/directives/mask.js +0 -107
  57. package/dist/src/index.js +0 -58
  58. package/dist/src/types/index.js +0 -0
  59. package/dist/src/utils/badge.js +0 -71
  60. package/dist/src/utils/columns.js +0 -33
  61. package/dist/src/utils/deserialize.js +0 -20
  62. package/src/components/AForm.vue +0 -314
  63. package/src/components/AFormLoading.vue +0 -76
  64. package/src/components/base/CollapseButton.vue +0 -36
  65. package/src/components/base/ExpandButton.vue +0 -34
  66. package/src/components/form/ABadge.vue +0 -135
  67. package/src/components/form/ACheckbox.vue +0 -62
  68. package/src/components/form/ACurrencyInput.vue +0 -357
  69. package/src/components/form/ADate.vue +0 -93
  70. package/src/components/form/ADatePicker.vue +0 -470
  71. package/src/components/form/ADateRange.vue +0 -133
  72. package/src/components/form/ADateSelection.vue +0 -174
  73. package/src/components/form/ADateTime.vue +0 -149
  74. package/src/components/form/ADateTimeInput.vue +0 -299
  75. package/src/components/form/ADropdown.vue +0 -222
  76. package/src/components/form/ADuration.vue +0 -124
  77. package/src/components/form/AFieldset.vue +0 -72
  78. package/src/components/form/AFileAttach.vue +0 -97
  79. package/src/components/form/AFormLink.vue +0 -368
  80. package/src/components/form/ANumericInput.vue +0 -32
  81. package/src/components/form/AQuantityInput.vue +0 -349
  82. package/src/components/form/ASegmentedControl.vue +0 -342
  83. package/src/components/form/ATextInput.vue +0 -40
  84. package/src/components/form/ATextboxInput.vue +0 -66
  85. package/src/components/utilities/Login.vue +0 -189
  86. package/src/directives/mask.ts +0 -122
  87. package/src/index.ts +0 -92
  88. package/src/shims-vue.d.ts +0 -5
  89. package/src/types/index.ts +0 -333
  90. package/src/utils/badge.ts +0 -94
  91. package/src/utils/columns.ts +0 -38
  92. package/src/utils/deserialize.ts +0 -20
@@ -1,135 +0,0 @@
1
- <template>
2
- <span v-if="displayLabel" :class="classes" :style="styleVars">{{ displayLabel }}</span>
3
- </template>
4
-
5
- <script setup lang="ts">
6
- import type { BadgeDescriptor, BadgePresentation, BadgeVariant, FieldOptions } from '@stonecrop/schema'
7
- import { lookupBadge } from '@stonecrop/schema'
8
- import { computed, type CSSProperties } from 'vue'
9
-
10
- const props = defineProps<
11
- Partial<BadgeDescriptor> & {
12
- /** Defaults to `cell-fill`: every call site fully determines this, so omitting it should
13
- * degrade to the common case rather than rendering an unstyled badge. */
14
- presentation?: BadgePresentation
15
- /** Stored field value — used with `options` when `label` is omitted. */
16
- value?: unknown
17
- options?: FieldOptions
18
- }
19
- >()
20
-
21
- const resolved = computed((): BadgeDescriptor | undefined => {
22
- if (props.label?.trim()) {
23
- return {
24
- label: props.label.trim(),
25
- variant: props.variant,
26
- color: props.color,
27
- }
28
- }
29
- if (props.value === null || props.value === undefined || props.value === '') return undefined
30
- const key = typeof props.value === 'string' ? props.value : String(props.value)
31
- return lookupBadge(props.options, key)
32
- })
33
-
34
- const displayLabel = computed(() => resolved.value?.label?.trim() ?? '')
35
-
36
- const activeVariant = computed((): BadgeVariant => resolved.value?.variant ?? props.variant ?? 'neutral')
37
-
38
- const activeColor = computed(() => resolved.value?.color ?? props.color)
39
-
40
- const classes = computed(() => [
41
- 'abadge',
42
- `abadge--${props.presentation ?? 'cell-fill'}`,
43
- `abadge--${activeVariant.value}`,
44
- ])
45
-
46
- const styleVars = computed((): CSSProperties => {
47
- const color = activeColor.value
48
- if (!color) return {}
49
- return {
50
- '--sc-badge-bg': color,
51
- '--sc-badge-text': color,
52
- '--sc-badge-accent': color,
53
- } as CSSProperties
54
- })
55
-
56
- export type { BadgePresentation, BadgeVariant }
57
- </script>
58
-
59
- <style scoped>
60
- .abadge {
61
- display: block;
62
- box-sizing: border-box;
63
- font: inherit;
64
- line-height: inherit;
65
- white-space: nowrap;
66
- overflow: hidden;
67
- text-overflow: ellipsis;
68
- }
69
-
70
- .abadge--cell-fill {
71
- display: block;
72
- box-sizing: border-box;
73
- width: 100%;
74
- min-height: calc(var(--sc-atable-row-height) + 2 * var(--sc-atable-row-padding));
75
- line-height: calc(var(--sc-atable-row-height) + 2 * var(--sc-atable-row-padding));
76
- margin: 0;
77
- padding: 0 0.5ch;
78
- border-radius: 0;
79
- text-align: inherit;
80
- background: var(--sc-badge-bg);
81
- color: var(--sc-badge-text);
82
- }
83
-
84
- .abadge--cell-fill.abadge--neutral {
85
- --sc-badge-bg: var(--sc-badge-neutral-bg);
86
- --sc-badge-text: var(--sc-badge-neutral-text);
87
- }
88
-
89
- .abadge--cell-fill.abadge--success {
90
- --sc-badge-bg: var(--sc-badge-success-bg);
91
- --sc-badge-text: var(--sc-badge-success-text);
92
- }
93
-
94
- .abadge--cell-fill.abadge--warning {
95
- --sc-badge-bg: var(--sc-badge-warning-bg);
96
- --sc-badge-text: var(--sc-badge-warning-text);
97
- }
98
-
99
- .abadge--cell-fill.abadge--danger {
100
- --sc-badge-bg: var(--sc-badge-danger-bg);
101
- --sc-badge-text: var(--sc-badge-danger-text);
102
- }
103
-
104
- .abadge--cell-fill.abadge--brand {
105
- --sc-badge-bg: var(--sc-badge-brand-bg);
106
- --sc-badge-text: var(--sc-badge-brand-text);
107
- }
108
-
109
- .abadge--input-accent {
110
- background: var(--sc-input-field-background);
111
- color: inherit;
112
- border-left: 4px solid var(--sc-badge-accent);
113
- padding-left: calc(1ch - 4px);
114
- }
115
-
116
- .abadge--input-accent.abadge--neutral {
117
- --sc-badge-accent: var(--sc-badge-neutral-accent);
118
- }
119
-
120
- .abadge--input-accent.abadge--success {
121
- --sc-badge-accent: var(--sc-badge-success-accent);
122
- }
123
-
124
- .abadge--input-accent.abadge--warning {
125
- --sc-badge-accent: var(--sc-badge-warning-accent);
126
- }
127
-
128
- .abadge--input-accent.abadge--danger {
129
- --sc-badge-accent: var(--sc-badge-danger-accent);
130
- }
131
-
132
- .abadge--input-accent.abadge--brand {
133
- --sc-badge-accent: var(--sc-badge-brand-accent);
134
- }
135
- </style>
@@ -1,62 +0,0 @@
1
- <template>
2
- <div class="aform_form-element">
3
- <template v-if="mode === 'display'">
4
- <label class="aform_field-label">{{ label }}</label>
5
- <span class="aform_display-value">{{ checkbox ? '✓' : '✗' }}</span>
6
- </template>
7
- <template v-else>
8
- <label class="aform_field-label" :for="uuid">{{ label }}</label>
9
- <span class="aform_checkbox-container aform_input-field">
10
- <input
11
- :id="uuid"
12
- v-model="checkbox"
13
- type="checkbox"
14
- class="aform_checkbox"
15
- :disabled="mode === 'read'"
16
- :required="required" />
17
- </span>
18
- <p v-show="errorText" class="aform_error" v-html="errorText"></p>
19
- </template>
20
- </div>
21
- </template>
22
-
23
- <script setup lang="ts">
24
- import { computed, InputHTMLAttributes } from 'vue'
25
-
26
- import { ComponentProps } from '../../types'
27
-
28
- const { label, required, mode, uuid, errors, validation = { errorMessage: '' } } = defineProps<ComponentProps>()
29
-
30
- // Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
31
- const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
32
-
33
- const checkbox = defineModel<InputHTMLAttributes['checked']>()
34
- </script>
35
-
36
- <style scoped>
37
- .aform_checkbox {
38
- cursor: pointer;
39
- width: auto;
40
- margin-top: 0;
41
- display: block;
42
- }
43
-
44
- .aform_checkbox:checked {
45
- accent-color: var(--sc-primary-color);
46
- border: 1px solid black;
47
- }
48
-
49
- .aform_checkbox-container {
50
- width: 100%;
51
- display: inline-block;
52
- text-align: left;
53
- }
54
-
55
- .aform_checkbox-container input {
56
- width: auto;
57
- }
58
-
59
- .aform_checkbox-container:hover + .aform_field-label {
60
- color: var(--sc-input-active-label-color);
61
- }
62
- </style>
@@ -1,357 +0,0 @@
1
- <template>
2
- <div class="aform_form-element acurrency">
3
- <template v-if="mode === 'display'">
4
- <span class="aform_display-value">{{ displayText }}</span>
5
- <label class="aform_field-label">{{ label }}</label>
6
- </template>
7
- <template v-else>
8
- <div class="acurrency__row">
9
- <div class="acurrency__field acurrency__field--amount">
10
- <div class="acurrency__group">
11
- <div class="acurrency__currency">
12
- <AFormLink
13
- v-model="currency"
14
- :mode="mode"
15
- embedded
16
- :placeholder="currencyLabel"
17
- :aria-label="currencyLabel"
18
- :required="required"
19
- :formatter="currencySymbol"
20
- :doctype="options.doctype"
21
- :filter-function="options.filterFunction"
22
- :is-async="options.isAsync">
23
- <template #option="{ option }"
24
- >{{ option.symbol ? `${option.symbol} — ` : '' }}{{ option.displayText ?? option.id }}</template
25
- >
26
- </AFormLink>
27
- </div>
28
- <div class="acurrency__amount-wrap">
29
- <input
30
- :id="uuid"
31
- v-model.number="amount"
32
- class="acurrency__amount"
33
- type="number"
34
- :disabled="mode === 'read'"
35
- :required="required"
36
- @keydown="onAmountKeydown"
37
- @paste="onAmountPaste" />
38
- <label class="aform_field-label" :for="uuid">{{ label }}</label>
39
- </div>
40
- </div>
41
- </div>
42
- </div>
43
- <div class="acurrency__row acurrency__row--base">
44
- <div class="acurrency__field acurrency__field--base-currency">
45
- <input :value="baseCurrencyText" class="aform_input-field acurrency__base-field" type="text" disabled />
46
- <label class="aform_field-label">{{ baseCurrencyLabel }}</label>
47
- </div>
48
- <div class="acurrency__field acurrency__field--base-amount">
49
- <input
50
- :value="modelValue.baseAmount"
51
- class="aform_input-field acurrency__base-field"
52
- type="number"
53
- disabled />
54
- <label class="aform_field-label">{{ baseAmountLabel }}</label>
55
- </div>
56
- <div class="acurrency__field acurrency__field--exchange-rate">
57
- <input
58
- :value="modelValue.exchangeRate"
59
- class="aform_input-field acurrency__base-field"
60
- type="number"
61
- disabled />
62
- <label class="aform_field-label">{{ exchangeRateLabel }}</label>
63
- </div>
64
- </div>
65
- <p v-show="validation.errorMessage" class="aform_error" v-html="validation.errorMessage"></p>
66
- </template>
67
- </div>
68
- </template>
69
-
70
- <script setup lang="ts">
71
- import { computed, inject, ref, watch } from 'vue'
72
-
73
- import type { AFormLinkValue, ComponentProps, CurrencyOptions, CurrencyValue } from '../../types'
74
- import AFormLink from './AFormLink.vue'
75
-
76
- const {
77
- label,
78
- required,
79
- mode,
80
- uuid,
81
- validation = { errorMessage: '&nbsp;' },
82
- options = {},
83
- currencyLabel = 'Currency',
84
- baseCurrencyLabel = 'Base Currency',
85
- baseAmountLabel = 'Base Amount',
86
- exchangeRateLabel = 'Exchange Rate',
87
- } = defineProps<
88
- ComponentProps & {
89
- options?: CurrencyOptions
90
- currencyLabel?: string
91
- baseCurrencyLabel?: string
92
- baseAmountLabel?: string
93
- exchangeRateLabel?: string
94
- }
95
- >()
96
-
97
- // The merged currency prefix is compact by design, so it shows the symbol rather than the
98
- // full currency name once a value is picked — falls back gracefully when a currency record
99
- // (or the story/app data behind it) doesn't carry a `symbol`.
100
- const currencySymbol = (value: AFormLinkValue): string => value.symbol ?? value.displayText ?? String(value.id)
101
-
102
- const modelValue = defineModel<CurrencyValue>({
103
- default: () => ({
104
- amount: 0,
105
- currency: { id: '' },
106
- baseAmount: 0,
107
- baseCurrency: { id: '' },
108
- exchangeRate: 1,
109
- }),
110
- })
111
-
112
- // The base currency is fixed configuration, not user-editable. It may be supplied as a bare id
113
- // (resolved to displayText below via the same `aformLinkResolver` injection AFormLink uses) or
114
- // as a full AFormLinkValue that already carries displayText.
115
- const normalizedBaseCurrency = computed<AFormLinkValue>(() => {
116
- const base = options.baseCurrency ?? modelValue.value?.baseCurrency
117
- if (!base) return { id: '' }
118
- return typeof base === 'string' ? { id: base } : base
119
- })
120
-
121
- type ResolverFn = (doctype: string, id: string) => string | undefined | Promise<string | undefined>
122
- const resolver = inject<ResolverFn | null>('aformLinkResolver', null)
123
-
124
- const resolvedBaseCurrency = ref<AFormLinkValue>(normalizedBaseCurrency.value)
125
-
126
- watch(
127
- normalizedBaseCurrency,
128
- async base => {
129
- if (!base.id || base.displayText || !resolver || !options.doctype) {
130
- resolvedBaseCurrency.value = base
131
- return
132
- }
133
- try {
134
- const displayText = await resolver(options.doctype, String(base.id))
135
- resolvedBaseCurrency.value = displayText ? { ...base, displayText } : base
136
- } catch {
137
- resolvedBaseCurrency.value = base
138
- }
139
- },
140
- { immediate: true }
141
- )
142
-
143
- const baseCurrencyText = computed(() => {
144
- const base = resolvedBaseCurrency.value
145
- if (!base?.id) return ''
146
- return base.displayText ?? String(base.id)
147
- })
148
-
149
- const resolveExchangeRate = (currencyId: string | number | undefined): number => {
150
- const baseId = resolvedBaseCurrency.value.id
151
- if (!currencyId || String(currencyId) === String(baseId)) return 1
152
- // The rate the value was booked at wins for as long as the currency is unchanged. Rates are
153
- // time-varying in a way conversion factors are not (see AQuantityInput), so `exchangeRates`
154
- // carries *today's* rates: preferring it here would silently re-rate a stored line to the
155
- // current rate on any touch — including the write-back AFormLink does when it resolves the
156
- // currency's display text, i.e. on mere render.
157
- if (String(currencyId) === String(modelValue.value?.currency?.id)) {
158
- return modelValue.value?.exchangeRate ?? options.exchangeRates?.[String(currencyId)] ?? 1
159
- }
160
- // Switching to a currency absent from the rate map resets to 1 rather than silently reusing
161
- // the outgoing currency's rate.
162
- return options.exchangeRates?.[String(currencyId)] ?? 1
163
- }
164
-
165
- // Enough decimal places to shed floating-point noise from the multiplication (e.g. 4 * 1.1 → 4.4
166
- // rather than 4.4000000000000004) without discarding a digit the rate actually produced. Matches
167
- // AQuantityInput's roundQty.
168
- const FLOAT_NOISE_DECIMALS = 6
169
-
170
- // How far to round the base amount is the *base currency's* business, and only the app knows what
171
- // that is — so it says so via `precision` (JPY carries 0 decimals, most currencies 2, KWD 3).
172
- // Unset stays deliberately loose rather than defaulting to 2: hard-rounding every currency to
173
- // cents destroys value outright, e.g. 50 IDR at 0.000063 rounds to a base amount of 0. A garbage
174
- // precision (non-integer, negative, or past toFixed's 100 ceiling) falls back rather than throwing
175
- // inside the setter and breaking the field.
176
- const baseDecimals = computed(() => {
177
- const { precision } = options
178
- if (precision === undefined) return FLOAT_NOISE_DECIMALS
179
- return Number.isInteger(precision) && precision >= 0 && precision <= 100 ? precision : FLOAT_NOISE_DECIMALS
180
- })
181
-
182
- const roundAmount = (value: number): number => Number(value.toFixed(baseDecimals.value))
183
-
184
- const recompute = (amount: number, currencyValue: AFormLinkValue) => {
185
- const exchangeRate = resolveExchangeRate(currencyValue.id)
186
- modelValue.value = {
187
- amount,
188
- currency: currencyValue,
189
- exchangeRate,
190
- baseCurrency: resolvedBaseCurrency.value,
191
- baseAmount: roundAmount(amount * exchangeRate),
192
- }
193
- }
194
-
195
- const amount = computed({
196
- get: () => modelValue.value?.amount ?? 0,
197
- set: (value: number) => recompute(value, modelValue.value?.currency ?? { id: '' }),
198
- })
199
-
200
- const currency = computed<AFormLinkValue>({
201
- get: () => modelValue.value?.currency ?? { id: '' },
202
- set: (value: AFormLinkValue) => recompute(modelValue.value?.amount ?? 0, value),
203
- })
204
-
205
- const amountNavigationKeys = new Set([
206
- 'Backspace',
207
- 'Delete',
208
- 'Tab',
209
- 'Escape',
210
- 'Enter',
211
- 'ArrowLeft',
212
- 'ArrowRight',
213
- 'ArrowUp',
214
- 'ArrowDown',
215
- 'Home',
216
- 'End',
217
- ])
218
-
219
- const onAmountKeydown = (event: KeyboardEvent) => {
220
- if (event.ctrlKey || event.metaKey || event.altKey) return
221
- if (amountNavigationKeys.has(event.key)) return
222
- if (/^[0-9]$/.test(event.key)) return
223
- // A currency amount is signed — credit notes, refunds and adjustments are negative.
224
- //
225
- // Both dedup checks below are best-effort: a `type="number"` input reports `value` as `''`
226
- // whenever its content isn't a valid number, so mid-entry states like "1." or "-" read as
227
- // empty and a second separator can slip through. The browser rejects the resulting value
228
- // anyway; this guard exists to stop the obviously-wrong keys (letters, 'e', '+'), not to be
229
- // the sole arbiter of well-formedness.
230
- const input = event.target as HTMLInputElement
231
- if (event.key === '.' && !input.value.includes('.')) return
232
- if (event.key === '-' && !input.value.includes('-')) return
233
- event.preventDefault()
234
- }
235
-
236
- const onAmountPaste = (event: ClipboardEvent) => {
237
- const pasted = event.clipboardData?.getData('text') ?? ''
238
- if (!/^-?\d*\.?\d*$/.test(pasted)) event.preventDefault()
239
- }
240
-
241
- const showBase = computed(() => {
242
- const v = modelValue.value
243
- return !!v?.baseCurrency?.id && (v.currency?.id !== v.baseCurrency?.id || v.amount !== v.baseAmount)
244
- })
245
-
246
- const displayText = computed(() => {
247
- const v = modelValue.value
248
- if (!v || !v.currency?.id) return '—'
249
- const currencyText = v.currency.displayText ?? String(v.currency.id)
250
- const base = `${v.amount} ${currencyText}`
251
- if (!showBase.value) return base
252
- const baseText = v.baseCurrency.displayText ?? String(v.baseCurrency.id)
253
- return `${base} (${v.baseAmount} ${baseText})`
254
- })
255
- </script>
256
-
257
- <style scoped>
258
- .acurrency__row {
259
- display: flex;
260
- gap: 1ch;
261
- }
262
-
263
- .acurrency__row--base {
264
- margin-top: 1.5rem;
265
- }
266
-
267
- .acurrency__field {
268
- position: relative;
269
- flex: 1;
270
- min-width: 0;
271
- }
272
-
273
- .acurrency__group {
274
- display: flex;
275
- align-items: stretch;
276
- width: 100%;
277
- border: 1px solid var(--sc-input-border-color);
278
- border-radius: 0.25rem;
279
- }
280
-
281
- .acurrency__group:focus-within {
282
- border-color: var(--sc-input-active-border-color);
283
- }
284
-
285
- /* Wraps just the amount input so its floating label (`left: 10px` of the nearest
286
- `position: relative` ancestor, per .aform_field-label) anchors above the amount box
287
- specifically, not the group's outer left edge — which is now the currency prefix. */
288
- .acurrency__amount-wrap {
289
- position: relative;
290
- flex: 1;
291
- min-width: 0;
292
- }
293
-
294
- /* The amount label ("Total") is the primary label for the whole merged group — bumped up
295
- slightly from the shared .aform_field-label size so it reads as the group's main label. */
296
- .acurrency__amount-wrap .aform_field-label {
297
- font-size: 0.85rem;
298
- }
299
-
300
- .acurrency__amount {
301
- width: 100%;
302
- box-sizing: border-box;
303
- border: none;
304
- outline: none;
305
- padding: 0.5ch 1ch;
306
- background: transparent;
307
- border-radius: 0 0.25rem 0.25rem 0;
308
- text-align: right;
309
- appearance: textfield;
310
- -moz-appearance: textfield;
311
- }
312
-
313
- .acurrency__amount::-webkit-outer-spin-button,
314
- .acurrency__amount::-webkit-inner-spin-button {
315
- appearance: none;
316
- -webkit-appearance: none;
317
- margin: 0;
318
- }
319
-
320
- /* The currency picker reads as a simple prefix addon (like Bootstrap's "$" prepend) rather
321
- than an equal partner to the amount box: compact width (it shows a symbol once a value is
322
- picked, not the full name), tinted background, left-rounded to match the group's own
323
- corner so the tint doesn't overhang the border. The dropdown itself isn't bound by this
324
- width — see AFormLink's .autocomplete-results — so search results still show full names. */
325
- .acurrency__currency {
326
- position: relative;
327
- flex: 0 0 auto;
328
- min-width: 4.5rem;
329
- background: var(--sc-gray-5);
330
- border-right: 1px solid var(--sc-input-border-color);
331
- border-radius: 0.25rem 0 0 0.25rem;
332
- }
333
-
334
- .acurrency__base-field {
335
- width: 100%;
336
- box-sizing: border-box;
337
- font-size: 1rem;
338
- padding: 0.5ch 1ch;
339
- border: 1px solid var(--sc-input-border-color);
340
- border-radius: 0.25rem;
341
- outline: none;
342
- appearance: textfield;
343
- -moz-appearance: textfield;
344
- }
345
-
346
- .acurrency__base-field:disabled {
347
- color: var(--sc-gray-50);
348
- }
349
-
350
- /* Base Amount and Exchange Rate are read-only — the number spinner offers nothing here. */
351
- .acurrency__base-field::-webkit-outer-spin-button,
352
- .acurrency__base-field::-webkit-inner-spin-button {
353
- appearance: none;
354
- -webkit-appearance: none;
355
- margin: 0;
356
- }
357
- </style>
@@ -1,93 +0,0 @@
1
- <template>
2
- <div class="aform_form-element">
3
- <template v-if="mode === 'display'">
4
- <span class="aform_display-value">{{ modelValue ? new Date(inputDate).toLocaleDateString() : '' }}</span>
5
- <label class="aform_field-label">{{ label }}</label>
6
- </template>
7
- <template v-else>
8
- <input
9
- :id="uuid"
10
- ref="date"
11
- v-model="inputDate"
12
- class="aform_input-field"
13
- type="date"
14
- :disabled="mode === 'read'"
15
- :required="required"
16
- @click="openPicker" />
17
- <label class="aform_field-label" :for="uuid">{{ label }}</label>
18
- <p v-show="errorText" class="aform_error" v-html="errorText"></p>
19
- <ADateSelection
20
- v-if="showPicker"
21
- ref="picker"
22
- class="adate-picker"
23
- :select-range="false"
24
- :show-time="false"
25
- @get-date="handleDate" />
26
- </template>
27
- </div>
28
- </template>
29
-
30
- <script setup lang="ts">
31
- import { useTemplateRef, ref, computed, watch } from 'vue'
32
- import { onClickOutside } from '@vueuse/core'
33
-
34
- import ADateSelection from './ADateSelection.vue'
35
- import type { ComponentProps } from '../../types'
36
-
37
- const {
38
- label = 'Date',
39
- required,
40
- mode,
41
- uuid,
42
- errors,
43
- validation = { errorMessage: '' },
44
- } = defineProps<ComponentProps>()
45
-
46
- // Dynamic trigger errors take precedence over a static schema errorMessage; empty means the slot hides.
47
- const errorText = computed(() => (errors?.length ? errors.join('; ') : (validation.errorMessage ?? '')))
48
-
49
- const modelValue = defineModel<string | Date>()
50
-
51
- const currentDate = ref(modelValue.value ? new Date(modelValue.value) : new Date())
52
- const inputDate = computed({
53
- get: () => currentDate.value.toISOString().split('T')[0],
54
- set: (value: string) => {
55
- currentDate.value = new Date(value)
56
- modelValue.value = value
57
- },
58
- })
59
-
60
- const pickerRef = useTemplateRef<HTMLDivElement>('picker')
61
- const showPicker = ref(false)
62
-
63
- onClickOutside(pickerRef, () => (showPicker.value = false))
64
-
65
- const openPicker = () => {
66
- if (mode !== 'read') showPicker.value = !showPicker.value
67
- }
68
-
69
- watch(
70
- () => modelValue.value,
71
- newValue => {
72
- if (newValue) {
73
- currentDate.value = new Date(newValue)
74
- }
75
- }
76
- )
77
-
78
- const handleDate = (data: { selected: Date }) => {
79
- currentDate.value = data.selected
80
- modelValue.value = inputDate.value
81
- showPicker.value = false
82
- }
83
- </script>
84
-
85
- <style scoped>
86
- .adate-picker {
87
- position: absolute;
88
- top: 100%;
89
- left: 0;
90
- z-index: 1000;
91
- margin-top: 0.25rem;
92
- }
93
- </style>