CInput
CInput — базовый примитив для построения компонентов ввода. Управляет состоянием фокуса, валидацией, aria-атрибутами и интеграцией с CForm. Сам по себе не рендерит <input> — вместо этого предоставляет данные через слот field, из которых потребитель строит собственное поле.
Когда использовать CInput напрямую?
Для большинства задач используйте CTextField. CInput нужен, когда требуется нестандартное поле: textarea с кастомным оформлением, PIN-input, числовой степпер и другие виджеты, которым нужна валидация и состояние фокуса.
Пример: кастомное поле
Показать код
<template>
<c-input v-model="search">
<template #field="field">
<div class="search-bar" :class="{ 'search-bar--focused': field.focused }">
<svg
class="search-icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
v-bind="field.attrs"
class="search-input"
placeholder="Search anything…"
:value="search"
@input="
(e: any) => {
search = e.target.value
}
"
@focus="field.focus"
@blur="field.blur"
/>
<kbd v-if="!search" class="search-kbd">⌘K</kbd>
<button v-else class="search-clear" @click="search = ''">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
width="14"
height="14"
>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
</template>
</c-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const search = ref('')
</script>Система пресетов
Пресеты — основной способ стилизации компонентов на основе CInput. Вместо условных CSS-классов в каждом шаблоне вы один раз описываете пресет и ссылаетесь на него по имени. Компонент сам применяет нужные классы под текущее состояние.
Каждое значение — массив имён утилитарных классов, поэтому пресеты работают с любым utility-first движком, который вы используете.
Зоны
Плоский пресет (ZonePreset) — это карта зон в списки классов. Зоны маппятся 1:1 на отрисованный DOM:
У самого CInput две зоны:
root.c-inputdetailsВложенные части описываются собственными пресетами компонентов и подставляются в снимок инпута по значению — тот же формат, что у standalone-компонентов:
fieldCFieldPresetroot (.c-field), input, label, prepend, appendfocused filled error disabled readonlymenuCMenuPresetroot (.c-menu)opened closedlistCListPresetroot (.c-list), option (.c-list-item)disabled readonlyТип CInputPreset
Пресет — это набор плоских пресетов по состояниям. base — спокойный вид, каждое состояние — отдельный полный плоский пресет:
type CInputZone = 'root' | 'details'
type CInputState = 'focused' | 'filled' | 'error' | 'disabled' | 'readonly'
// один снимок: свои зоны + вложенные пресеты частей (по значению)
type CInputSnapshot = Partial<Record<CInputZone, string[]>> & {
field?: CFieldPreset
menu?: CMenuPreset
list?: CListPreset
}
// base + опциональные снимки по состояниям — всё опционально
type CInputPreset = Partial<Record<'base' | CInputState, CInputSnapshot>>Составных состояний нет. Вложенность — только по компонентам: вложенный пресет кладут в base, а его состояния компонент резолвит сам.
Одно состояние за раз
Компонент всегда находится в одном текущем состоянии, и применяется пресет именно этого состояния — ничего не складывается и нет никаких приоритетов. Вы просто описываете плоский пресет на каждое состояние, активное применяется (или base, когда поле в покое). Зоны активного состояния подменяют одноимённые зоны base; зона, которую состояние не описывает, берётся из base.
Почему одно состояние, а не стек?
Утилитарные классы — это !important с одинаковой специфичностью, поэтому при стэке конфликтующих классов (например двух bg-*) побеждает порядок в стайлшите, а не намерение. Ровно один комплект классов на зону делает результат предсказуемым.
Адресация состояния напрямую
Каждое состояние — самостоятельный плоский пресет, адресуемый как name.state. input.blue — это весь набор; input.blue.focused — только плоский пресет состояния focused.
Регистрация пресетов
Пресеты регистрируются глобально в createVuelandUI:
import { createVuelandUI } from '@vueland/ui'
import type { CInputPreset } from '@vueland/ui/types'
function makePreset(color: string): CInputPreset {
return {
base: {
field: {
base: { label: [color] },
focused: { label: [color], root: [color] },
filled: { label: [color] },
error: { label: ['text-red'], root: ['text-red'] },
readonly: { label: ['text-grey'] },
// `disabled` притеняется компонентом; состояние нужно только для оверрайда
},
},
error: { details: ['text-red'] },
}
}
const vueland = createVuelandUI({
presets: {
input: {
blue: makePreset('text-blue'),
teal: makePreset('text-teal'),
},
},
})Затем указывайте пресет по имени на любом компоненте на основе CInput:
<c-text-field preset="input.blue" ... />
<c-text-field preset="input.teal" ... />Распределение CInput → CField
Вы пишете один пресет. CInput его резолвит, применяет свои зоны (root, details) и раздаёт набор в поддерево через provide/inject. CField, CMenu и CList берут из base-снимка свои вложенные пресеты (field/menu/list) и резолвят собственные состояния сами. У каждого из них есть и свой проп preset — он перекрывает контекст, поэтому standalone-режим работает без изменений. Ничего не мутируется глобально.
Все состояния наглядно
Пример выше использует preset="input.blue" в шести состояниях:
- Default — зоны
base - Focused — зоны
focusedподменяют одноимённые зоныbase - Filled —
filledподменяетbase(лейбл поднимается, цвет сохраняется) - Error —
errorподменяетbase(красный) - Disabled — взаимодействие заблокировано; компонент притеняет поле
- Readonly — значение видно, но редактировать нельзя
API
Props
modelValueT | T[] | undefined | nullundefinedidstringuid, uid-label, uid-detailslabelstringfield)detailsstringnoDetailsbooleanfalseclearablebooleanfalseclearable в слот fielddisabledbooleanfalsearia-disabledreadonlybooleanfalsearia-readonly, блокирует вводfocusedbooleanfalsedirtybooleanfalseroleCInputRoleuidrulesValidateFn[][]validateOn'input' | 'blur''input'validationValueanyrules вместо modelValue — для полей, где modelValue хранит отображаемый текстpresetstringpresets, переданном в createVuelandUI)Тип CInputRole
type CInputRole = 'combobox' | 'checkbox' | 'radio' | 'listbox''combobox'role="combobox", aria-haspopup="listbox", aria-controls, aria-expanded — для активаторов select / autocomplete'checkbox'aria-labelledby к лейблу'radio'aria-labelledby к лейблу'listbox'uid)Slots
fieldCInputFieldSlotPropsdetailsCInputDetailsSlotPropsПропсы слота field
uidstring<input>attrsRecord<string, unknown>v-bindfocusedbooleanlabelstring | undefinedlabelclearableboolean | undefinedclearabledisabledboolean | undefineddisabledreadonlyboolean | undefinedreadonlydirtyboolean | undefineddirtypresetCInputPreset | undefinedhasErrorbooleanerrorMessagestring | undefinedvalidatingbooleanfocus() => voidblur() => voidreset() => voidvalidate() => Promise<boolean>Пропсы слота details
uidstringerrorMessagestring | undefinedhasErrorbooleanvalidatingbooleandetailsstring | undefineddetailsEvents
update:focusedbooleanCInput — renderless-примитив: он принимает modelValue для валидации и состояния, но не знает, как менять значение кастомного поля. Обновляйте источник данных внутри field-слота.
Expose
validate() => Promise<boolean>reset() => voidfocus() => voiddisabled/readonlyblur() => voidisReadonly() => booleanreadonlyisDisabled() => booleandisabledИнтеграция с CForm
CInput автоматически регистрирует свой метод validate в ближайшем родительском CForm. При вызове form.validate() все зарегистрированные поля проверяются параллельно через Promise.all.
<template>
<c-form>
<template #default="{ validate }">
<c-input v-model="pin" :rules="rules">
<template #field="field">
<input
:id="field.uid"
v-bind="field.attrs"
:value="pin"
@focus="field.focus"
@blur="field.blur"
/>
</template>
</c-input>
<button @click="validate">Проверить</button>
</template>
</c-form>
</template>Автоматические aria-атрибуты
CInput формирует aria-атрибуты и передаёт их в field.attrs. Используйте v-bind="field.attrs" на нативном элементе.
aria-labelledby="{uid}-label"label задан, или kind = checkbox/radioaria-labellabel задан как единственная меткаaria-describedby="{uid}-details"aria-invalid="true"aria-errormessage="{uid}-details"aria-disabled="true"disabled = truearia-readonly="true"readonly = truearia-haspopup="listbox"kind = 'listbox'aria-controls="{uid}-menu"kind = 'listbox'aria-expandedkind = 'listbox' (обновляется при фокусе)CSS-переменные
--c-input-details-heightvar(--c-sys-control-height-sm)--c-input-transition-durationvar(--c-sys-motion-duration-medium)--c-input-primary-colorvar(--c-sys-color-primary)--c-input-error-colorvar(--c-sys-color-error)--c-input-disabled-colorvar(--c-sys-color-disabled)--c-input-readonly-colorvar(--c-sys-color-readonly)Фон поля в состояниях
focused/readonly/disabled/errorотрисовывает компонентCField(--c-field-focused-bg-color,--c-field-readonly-bg-color,--c-field-disabled-bg-color,--c-field-error-bg-color), а неCInput.
CSS-классы состояний
c-input--focusedc-input--has-errordisabled, и не readonly)c-input--disableddisabled = truec-input--readonlyreadonly = truec-input--dirtydirty = truec-input--clearableclearable = true