749 lines
20 KiB
Vue
749 lines
20 KiB
Vue
<template>
|
|
<div class="v4-field-renderer">
|
|
<label
|
|
v-for="field in fields"
|
|
:key="fieldKey(field)"
|
|
class="v4-field"
|
|
:class="{ 'v4-field--readonly': !isEditable(field) }"
|
|
>
|
|
<span class="v4-field__label">
|
|
{{ field.display_name }}
|
|
<sup v-if="field.required">*</sup>
|
|
<em v-if="!isEditable(field)">{{ t('taskV4.field.readonly') }}</em>
|
|
</span>
|
|
|
|
<select
|
|
v-if="isEditable(field) && isSelectField(field)"
|
|
class="v4-field__control"
|
|
:name="fieldKey(field)"
|
|
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
|
:disabled="selectDisabled(field)"
|
|
:value="fieldValue(field)"
|
|
@change="updateField(field, $event)"
|
|
>
|
|
<option value="">{{ t('taskV4.field.selectPlaceholder') }}</option>
|
|
<option
|
|
v-for="option in fieldOptions(field)"
|
|
:key="option.value"
|
|
:value="option.value"
|
|
>
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
|
|
<input
|
|
v-else-if="isEditable(field) && isDateField(field)"
|
|
class="v4-field__control"
|
|
type="date"
|
|
:name="fieldKey(field)"
|
|
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
|
:value="fieldValue(field)"
|
|
@input="updateField(field, $event)"
|
|
>
|
|
|
|
<input
|
|
v-else-if="isEditable(field) && isCheckboxField(field)"
|
|
class="v4-field__control v4-field__control--checkbox"
|
|
type="checkbox"
|
|
:name="fieldKey(field)"
|
|
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
|
:checked="checkboxValue(field)"
|
|
@change="updateCheckboxField(field, $event)"
|
|
>
|
|
|
|
<input
|
|
v-else-if="isEditable(field) && isNumberField(field)"
|
|
class="v4-field__control"
|
|
type="number"
|
|
:name="fieldKey(field)"
|
|
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
|
:value="fieldValue(field)"
|
|
@input="updateField(field, $event)"
|
|
>
|
|
|
|
<textarea
|
|
v-else-if="isEditable(field) && isTextAreaField(field)"
|
|
class="v4-field__control v4-field__control--textarea"
|
|
:name="fieldKey(field)"
|
|
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
|
:value="fieldValue(field)"
|
|
rows="3"
|
|
@input="updateField(field, $event)"
|
|
/>
|
|
|
|
<input
|
|
v-else-if="isEditable(field)"
|
|
class="v4-field__control"
|
|
type="text"
|
|
:name="fieldKey(field)"
|
|
:aria-invalid="fieldError(field) ? 'true' : 'false'"
|
|
:value="fieldValue(field)"
|
|
@input="updateField(field, $event)"
|
|
>
|
|
|
|
<input
|
|
v-else-if="isCheckboxField(field)"
|
|
class="v4-field__control v4-field__control--checkbox"
|
|
type="checkbox"
|
|
:name="fieldKey(field)"
|
|
:checked="checkboxValue(field)"
|
|
disabled
|
|
>
|
|
|
|
<span
|
|
v-else
|
|
class="v4-field__static"
|
|
>
|
|
{{ staticFieldValue(field) }}
|
|
</span>
|
|
|
|
<small
|
|
v-if="field.control_hint"
|
|
class="v4-field__hint"
|
|
>{{ field.control_hint }}</small>
|
|
<small
|
|
v-for="hint in lookupHints(field)"
|
|
:key="hint.key"
|
|
class="v4-field__hint"
|
|
:class="`v4-field__hint--${hint.tone}`"
|
|
>{{ hint.message }}</small>
|
|
<small
|
|
v-for="error in visibleFieldErrors(field)"
|
|
:key="error"
|
|
class="v4-field__error"
|
|
>{{ error }}</small>
|
|
</label>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, reactive, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import {
|
|
fetchReservationV4AccountLookups,
|
|
fetchReservationV4RateCodeLookups,
|
|
fetchReservationV4RoomTypeLookups,
|
|
} from '@/services/reservationService'
|
|
import type {
|
|
ReservationRecord,
|
|
ReservationV4CatalogLookupItem,
|
|
ReservationV4CatalogLookupResult,
|
|
ReservationV4TaskCardFieldResult,
|
|
} from '@/types/reservation'
|
|
import {
|
|
isReservationV4EditableField,
|
|
readV4FieldValue,
|
|
reservationV4LookupKindForOptionsSource,
|
|
reservationV4FieldKey,
|
|
stringifyReservationV4SafeDisplayValue,
|
|
type ReservationV4LookupKind,
|
|
} from '@/utils/reservationV4FieldRules'
|
|
|
|
const props = defineProps<{
|
|
fields: ReservationV4TaskCardFieldResult[]
|
|
modelValue: ReservationRecord
|
|
readOnly: boolean
|
|
validationErrors?: Record<string, string>
|
|
editableKeys?: string[]
|
|
hotelId?: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: ReservationRecord]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
type FieldOption = {
|
|
value: string
|
|
label: string
|
|
item?: ReservationV4CatalogLookupItem
|
|
}
|
|
type LookupHintTone = 'normal' | 'warning' | 'error'
|
|
type LookupHint = {
|
|
key: string
|
|
message: string
|
|
tone: LookupHintTone
|
|
}
|
|
type LookupState = {
|
|
loading: boolean
|
|
error: string
|
|
result: ReservationV4CatalogLookupResult | null
|
|
}
|
|
type ExactLookupState = {
|
|
loading: boolean
|
|
searched: boolean
|
|
item: ReservationV4CatalogLookupItem | null
|
|
}
|
|
|
|
const invalidLookupValues = reactive<Record<string, string>>({})
|
|
const exactLookupStates = reactive<Record<string, ExactLookupState>>({})
|
|
const lookupStates = reactive<Record<ReservationV4LookupKind, LookupState>>({
|
|
ACCOUNT: createLookupState(),
|
|
ROOM_TYPE: createLookupState(),
|
|
RATE_CODE: createLookupState(),
|
|
})
|
|
const lookupRequestVersions: Record<ReservationV4LookupKind, number> = {
|
|
ACCOUNT: 0,
|
|
ROOM_TYPE: 0,
|
|
RATE_CODE: 0,
|
|
}
|
|
const lookupKinds = computed(() => {
|
|
const kinds = props.fields
|
|
.filter((field) => isSelectControl(field) && isEditable(field))
|
|
.map((field) => reservationV4LookupKindForOptionsSource(field.options_source))
|
|
.filter((kind): kind is ReservationV4LookupKind => Boolean(kind))
|
|
return [...new Set(kinds)]
|
|
})
|
|
const lookupKindKey = computed(() => lookupKinds.value.join('|'))
|
|
|
|
watch(
|
|
[lookupKindKey, () => props.hotelId],
|
|
() => {
|
|
resetLookupStates()
|
|
lookupKinds.value.forEach((kind) => {
|
|
void loadLookup(kind)
|
|
})
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
watch(
|
|
[
|
|
() => props.modelValue,
|
|
() => lookupStates.ACCOUNT.result,
|
|
() => lookupStates.ROOM_TYPE.result,
|
|
() => lookupStates.RATE_CODE.result,
|
|
],
|
|
() => {
|
|
clearInvalidLoadedCatalogValues()
|
|
},
|
|
{ deep: true },
|
|
)
|
|
|
|
function fieldKey(field: ReservationV4TaskCardFieldResult): string {
|
|
return reservationV4FieldKey(field)
|
|
}
|
|
|
|
function isEditable(field: ReservationV4TaskCardFieldResult): boolean {
|
|
const key = reservationV4FieldKey(field)
|
|
return !props.readOnly &&
|
|
(!props.editableKeys || props.editableKeys.includes(key)) &&
|
|
isReservationV4EditableField(field)
|
|
}
|
|
|
|
function isSelectField(field: ReservationV4TaskCardFieldResult): boolean {
|
|
return isSelectControl(field) &&
|
|
(Boolean(reservationV4LookupKindForOptionsSource(field.options_source)) || fixedFieldOptions(field).length > 0)
|
|
}
|
|
|
|
function isDateField(field: ReservationV4TaskCardFieldResult): boolean {
|
|
return normalizeControlType(field.control_type) === 'DATE'
|
|
}
|
|
|
|
function isCheckboxField(field: ReservationV4TaskCardFieldResult): boolean {
|
|
return ['CHECKBOX', 'BOOLEAN'].includes(normalizeControlType(field.control_type))
|
|
}
|
|
|
|
function isNumberField(field: ReservationV4TaskCardFieldResult): boolean {
|
|
return normalizeControlType(field.control_type) === 'NUMBER'
|
|
}
|
|
|
|
function isTextAreaField(field: ReservationV4TaskCardFieldResult): boolean {
|
|
return normalizeControlType(field.control_type) === 'TEXTAREA'
|
|
}
|
|
|
|
function fieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
|
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
|
const options = kind
|
|
? (lookupStates[kind].result?.items ?? []).map((item) => ({
|
|
value: item.code,
|
|
label: lookupItemLabel(item),
|
|
item,
|
|
}))
|
|
: fixedFieldOptions(field)
|
|
const exactItem = exactLookupItemForField(field)
|
|
if (exactItem && !options.some((option) => option.value === exactItem.code)) {
|
|
options.push({
|
|
value: exactItem.code,
|
|
label: lookupItemLabel(exactItem),
|
|
item: exactItem,
|
|
})
|
|
}
|
|
return options
|
|
}
|
|
|
|
function selectDisabled(field: ReservationV4TaskCardFieldResult): boolean {
|
|
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
|
if (!kind) {
|
|
return false
|
|
}
|
|
const state = lookupStates[kind]
|
|
return state.loading || Boolean(state.error)
|
|
}
|
|
|
|
function fieldValue(field: ReservationV4TaskCardFieldResult): string | number {
|
|
const value = readV4FieldValue(field, props.modelValue)
|
|
if (!isEditable(field)) {
|
|
return stringifyV4Value(value)
|
|
}
|
|
if (typeof value === 'number') {
|
|
return value
|
|
}
|
|
if (typeof value === 'string') {
|
|
return value
|
|
}
|
|
return stringifyV4Value(value)
|
|
}
|
|
|
|
function staticFieldValue(field: ReservationV4TaskCardFieldResult): string {
|
|
const value = fieldValue(field)
|
|
const matchedOption = fieldOptions(field).find((option) => option.value === value)
|
|
return matchedOption?.label ?? stringifyV4Value(value)
|
|
}
|
|
|
|
function checkboxValue(field: ReservationV4TaskCardFieldResult): boolean {
|
|
const value = readV4FieldValue(field, props.modelValue)
|
|
return value === true || value === 'true' || value === 1 || value === '1'
|
|
}
|
|
|
|
function updateField(field: ReservationV4TaskCardFieldResult, event: Event): void {
|
|
const target = event.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
delete invalidLookupValues[reservationV4FieldKey(field)]
|
|
emit('update:modelValue', {
|
|
...props.modelValue,
|
|
[reservationV4FieldKey(field)]: target.value,
|
|
})
|
|
}
|
|
|
|
function updateCheckboxField(field: ReservationV4TaskCardFieldResult, event: Event): void {
|
|
const target = event.target as HTMLInputElement
|
|
delete invalidLookupValues[reservationV4FieldKey(field)]
|
|
emit('update:modelValue', {
|
|
...props.modelValue,
|
|
[reservationV4FieldKey(field)]: target.checked,
|
|
})
|
|
}
|
|
|
|
function lookupHints(field: ReservationV4TaskCardFieldResult): LookupHint[] {
|
|
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
|
if (!kind) {
|
|
return []
|
|
}
|
|
const state = lookupStates[kind]
|
|
if (state.loading) {
|
|
return [{
|
|
key: `${kind}-loading`,
|
|
message: t('taskV4.lookup.loading'),
|
|
tone: 'normal',
|
|
}]
|
|
}
|
|
if (state.error) {
|
|
return [{
|
|
key: `${kind}-error`,
|
|
message: state.error,
|
|
tone: 'error',
|
|
}]
|
|
}
|
|
const result = state.result
|
|
if (!result) {
|
|
return []
|
|
}
|
|
|
|
const hints: LookupHint[] = []
|
|
if (isExactLookupLoading(field)) {
|
|
hints.push({
|
|
key: `${kind}-exact-loading`,
|
|
message: t('taskV4.lookup.loading'),
|
|
tone: 'normal',
|
|
})
|
|
}
|
|
const selectedItem = selectedLookupItem(field)
|
|
if (kind === 'ACCOUNT' && selectedItem && (selectedItem.market_code || selectedItem.source_code)) {
|
|
hints.push({
|
|
key: `${kind}-derived`,
|
|
message: t('taskV4.lookup.accountDerived', {
|
|
market: selectedItem.market_code ?? '-',
|
|
source: selectedItem.source_code ?? '-',
|
|
}),
|
|
tone: 'normal',
|
|
})
|
|
}
|
|
if (result.stale) {
|
|
hints.push({
|
|
key: `${kind}-stale`,
|
|
message: t('taskV4.lookup.stale'),
|
|
tone: 'warning',
|
|
})
|
|
}
|
|
result.warnings.forEach((warning, index) => {
|
|
hints.push({
|
|
key: `${kind}-warning-${index}`,
|
|
message: t('taskV4.lookup.warning', { warning }),
|
|
tone: 'warning',
|
|
})
|
|
})
|
|
if (!result.items.length) {
|
|
hints.push({
|
|
key: `${kind}-empty`,
|
|
message: t('taskV4.lookup.empty'),
|
|
tone: 'warning',
|
|
})
|
|
}
|
|
if (result.catalog_source || result.catalog_version) {
|
|
hints.push({
|
|
key: `${kind}-meta`,
|
|
message: t('taskV4.lookup.catalogMeta', {
|
|
source: result.catalog_source ?? '-',
|
|
version: result.catalog_version ?? '-',
|
|
}),
|
|
tone: 'normal',
|
|
})
|
|
}
|
|
const invalidValue = invalidLookupValues[reservationV4FieldKey(field)]
|
|
if (invalidValue) {
|
|
hints.push({
|
|
key: `${kind}-invalid-current`,
|
|
message: t('taskV4.lookup.currentNotInCatalog', { value: invalidValue }),
|
|
tone: 'error',
|
|
})
|
|
}
|
|
return hints
|
|
}
|
|
|
|
function visibleFieldErrors(field: ReservationV4TaskCardFieldResult): string[] {
|
|
return [
|
|
...(field.validation_errors ?? []),
|
|
fieldError(field),
|
|
].filter(Boolean)
|
|
}
|
|
|
|
function fieldError(field: ReservationV4TaskCardFieldResult): string {
|
|
return props.validationErrors?.[reservationV4FieldKey(field)] ?? ''
|
|
}
|
|
|
|
function stringifyV4Value(value: unknown): string {
|
|
return stringifyReservationV4SafeDisplayValue(
|
|
value,
|
|
t('taskV4.field.empty'),
|
|
t('taskV4.field.hidden'),
|
|
)
|
|
}
|
|
|
|
function normalizeControlType(value: string | null): string {
|
|
return value?.trim().replace(/[\s-]+/g, '_').toUpperCase() ?? ''
|
|
}
|
|
|
|
function isSelectControl(field: ReservationV4TaskCardFieldResult): boolean {
|
|
return ['SELECT', 'LOOKUP'].includes(normalizeControlType(field.control_type))
|
|
}
|
|
|
|
function fixedFieldOptions(field: ReservationV4TaskCardFieldResult): FieldOption[] {
|
|
const optionsSource = normalizeControlType(field.options_source)
|
|
if (optionsSource !== 'RESERVATION_V4_GROUP_BOOKING_STATUS_FIXED') {
|
|
return []
|
|
}
|
|
return [
|
|
{
|
|
value: 'TEN',
|
|
label: 'TEN-Tentative',
|
|
},
|
|
{
|
|
value: 'DEF',
|
|
label: 'DEF-Definite',
|
|
},
|
|
{
|
|
value: 'INQ',
|
|
label: 'INQ-Inquiry',
|
|
},
|
|
]
|
|
}
|
|
|
|
function lookupItemLabel(item: ReservationV4CatalogLookupItem): string {
|
|
if (item.display_name && item.display_name !== item.code) {
|
|
return `${item.code} - ${item.display_name}`
|
|
}
|
|
return item.code
|
|
}
|
|
|
|
function selectedLookupItem(field: ReservationV4TaskCardFieldResult): ReservationV4CatalogLookupItem | null {
|
|
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
|
const value = String(readV4FieldValue(field, props.modelValue) || '')
|
|
if (!kind || !value) {
|
|
return null
|
|
}
|
|
return lookupStates[kind].result?.items.find((item) => item.code === value)
|
|
?? exactLookupStates[exactLookupKey(kind, value)]?.item
|
|
?? null
|
|
}
|
|
|
|
function createLookupState(): LookupState {
|
|
return {
|
|
loading: false,
|
|
error: '',
|
|
result: null,
|
|
}
|
|
}
|
|
|
|
function resetLookupStates(): void {
|
|
const allKinds: ReservationV4LookupKind[] = ['ACCOUNT', 'ROOM_TYPE', 'RATE_CODE']
|
|
allKinds.forEach((kind) => {
|
|
lookupRequestVersions[kind] += 1
|
|
lookupStates[kind].loading = false
|
|
lookupStates[kind].error = ''
|
|
lookupStates[kind].result = null
|
|
})
|
|
Object.keys(invalidLookupValues).forEach((key) => {
|
|
delete invalidLookupValues[key]
|
|
})
|
|
Object.keys(exactLookupStates).forEach((key) => {
|
|
delete exactLookupStates[key]
|
|
})
|
|
}
|
|
|
|
async function loadLookup(kind: ReservationV4LookupKind): Promise<void> {
|
|
const state = lookupStates[kind]
|
|
const requestVersion = ++lookupRequestVersions[kind]
|
|
state.loading = true
|
|
state.error = ''
|
|
try {
|
|
const result = await lookupFetcher(kind)({
|
|
...(props.hotelId ? { hotel_id: props.hotelId } : {}),
|
|
page_num: 1,
|
|
page_size: 100,
|
|
})
|
|
if (requestVersion === lookupRequestVersions[kind]) {
|
|
state.result = {
|
|
...result,
|
|
items: result.items ?? [],
|
|
warnings: result.warnings ?? [],
|
|
}
|
|
}
|
|
} catch {
|
|
if (requestVersion === lookupRequestVersions[kind]) {
|
|
state.error = t('taskV4.lookup.error')
|
|
}
|
|
} finally {
|
|
if (requestVersion === lookupRequestVersions[kind]) {
|
|
state.loading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
function lookupFetcher(kind: ReservationV4LookupKind) {
|
|
switch (kind) {
|
|
case 'ACCOUNT':
|
|
return fetchReservationV4AccountLookups
|
|
case 'ROOM_TYPE':
|
|
return fetchReservationV4RoomTypeLookups
|
|
case 'RATE_CODE':
|
|
return fetchReservationV4RateCodeLookups
|
|
}
|
|
}
|
|
|
|
function clearInvalidLoadedCatalogValues(): void {
|
|
const nextValues = { ...props.modelValue }
|
|
let changed = false
|
|
props.fields.forEach((field) => {
|
|
if (!isSelectField(field) || !isEditable(field)) {
|
|
return
|
|
}
|
|
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
|
const result = kind ? lookupStates[kind].result : null
|
|
if (!kind || !result) {
|
|
return
|
|
}
|
|
|
|
const key = reservationV4FieldKey(field)
|
|
const currentValue = String(readV4FieldValue(field, nextValues) || '')
|
|
if (!currentValue) {
|
|
return
|
|
}
|
|
if (result.items.some((item) => item.code === currentValue)) {
|
|
delete invalidLookupValues[key]
|
|
return
|
|
}
|
|
if (!isFullyLoadedCatalog(result)) {
|
|
const exactState = exactLookupStates[exactLookupKey(kind, currentValue)]
|
|
if (!exactState) {
|
|
void loadExactLookup(kind, currentValue)
|
|
return
|
|
}
|
|
if (exactState.loading || (exactState.searched && exactState.item)) {
|
|
delete invalidLookupValues[key]
|
|
return
|
|
}
|
|
}
|
|
clearInvalidCatalogValue(field, currentValue, nextValues)
|
|
changed = true
|
|
})
|
|
if (changed) {
|
|
emit('update:modelValue', nextValues)
|
|
}
|
|
}
|
|
|
|
function isFullyLoadedCatalog(result: ReservationV4CatalogLookupResult): boolean {
|
|
return result.items.length >= result.page.total
|
|
}
|
|
|
|
async function loadExactLookup(kind: ReservationV4LookupKind, code: string): Promise<void> {
|
|
const key = exactLookupKey(kind, code)
|
|
exactLookupStates[key] = {
|
|
loading: true,
|
|
searched: false,
|
|
item: null,
|
|
}
|
|
try {
|
|
const result = await lookupFetcher(kind)({
|
|
...(props.hotelId ? { hotel_id: props.hotelId } : {}),
|
|
keyword: code,
|
|
page_num: 1,
|
|
page_size: 20,
|
|
})
|
|
exactLookupStates[key] = {
|
|
loading: false,
|
|
searched: true,
|
|
item: (result.items ?? []).find((item) => item.code === code) ?? null,
|
|
}
|
|
} catch {
|
|
exactLookupStates[key] = {
|
|
loading: false,
|
|
searched: true,
|
|
item: null,
|
|
}
|
|
} finally {
|
|
clearInvalidLoadedCatalogValues()
|
|
}
|
|
}
|
|
|
|
function clearInvalidCatalogValue(
|
|
field: ReservationV4TaskCardFieldResult,
|
|
currentValue: string,
|
|
nextValues: ReservationRecord,
|
|
): void {
|
|
const key = reservationV4FieldKey(field)
|
|
invalidLookupValues[key] = currentValue
|
|
nextValues[key] = ''
|
|
}
|
|
|
|
function exactLookupItemForField(field: ReservationV4TaskCardFieldResult): ReservationV4CatalogLookupItem | null {
|
|
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
|
const value = String(readV4FieldValue(field, props.modelValue) || '')
|
|
if (!kind || !value) {
|
|
return null
|
|
}
|
|
return exactLookupStates[exactLookupKey(kind, value)]?.item ?? null
|
|
}
|
|
|
|
function isExactLookupLoading(field: ReservationV4TaskCardFieldResult): boolean {
|
|
const kind = reservationV4LookupKindForOptionsSource(field.options_source)
|
|
const value = String(readV4FieldValue(field, props.modelValue) || '')
|
|
if (!kind || !value) {
|
|
return false
|
|
}
|
|
return exactLookupStates[exactLookupKey(kind, value)]?.loading ?? false
|
|
}
|
|
|
|
function exactLookupKey(kind: ReservationV4LookupKind, code: string): string {
|
|
return `${props.hotelId ?? ''}:${kind}:${code}`
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.v4-field-renderer {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
.v4-field {
|
|
display: grid;
|
|
gap: 7px;
|
|
}
|
|
|
|
.v4-field__label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
color: var(--th-color-slate-700);
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.v4-field__label sup {
|
|
color: var(--th-color-danger);
|
|
}
|
|
|
|
.v4-field__label em {
|
|
border-radius: 999px;
|
|
background: var(--th-color-slate-100);
|
|
color: var(--th-color-slate-500);
|
|
font-size: 11px;
|
|
font-style: normal;
|
|
padding: 2px 6px;
|
|
}
|
|
|
|
.v4-field__control,
|
|
.v4-field__static {
|
|
min-height: 38px;
|
|
width: 100%;
|
|
border: 1px solid var(--th-color-slate-200);
|
|
border-radius: var(--th-radius-sm);
|
|
background: var(--th-color-white);
|
|
color: var(--th-color-slate-900);
|
|
font: inherit;
|
|
padding: 9px 10px;
|
|
}
|
|
|
|
.v4-field__control--textarea {
|
|
resize: vertical;
|
|
}
|
|
|
|
.v4-field__control--checkbox {
|
|
min-height: 18px;
|
|
width: 18px;
|
|
accent-color: var(--th-color-blue-600);
|
|
box-shadow: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.v4-field__control[aria-invalid='true'] {
|
|
border-color: var(--th-color-danger);
|
|
box-shadow: 0 0 0 3px var(--th-color-danger-bg);
|
|
}
|
|
|
|
.v4-field__static {
|
|
display: block;
|
|
min-height: 38px;
|
|
background: var(--th-color-slate-50);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.v4-field__hint {
|
|
color: var(--th-color-slate-500);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.v4-field__hint--warning {
|
|
color: var(--th-color-warning);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.v4-field__hint--error {
|
|
color: var(--th-color-danger);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.v4-field__error {
|
|
color: var(--th-color-danger);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
@media (max-width: 760px) {
|
|
.v4-field-renderer {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|