Input.vue
3.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<script setup>
/**
* Input —— 受控单行输入框(v-model)。与业务无关,只认 props。
*
* @typedef {'sm'|'md'|'lg'} InputSize
* @typedef {'text'|'password'|'number'|'email'|'search'|'url'|'date'|'datetime-local'} InputType
*/
defineProps({
/** 受控值。 */
modelValue: { type: [String, Number], default: '' },
/** @type {import('vue').PropType<InputType>} 原生 input 类型。 */
type: { type: String, default: 'text' },
/** 占位文案。 */
placeholder: { type: String, default: '' },
/** 禁用态:阻断输入且降透明度。 */
disabled: { type: Boolean, default: false },
/** 非法态:描边转 danger 色,并置 aria-invalid。 */
invalid: { type: Boolean, default: false },
/** @type {import('vue').PropType<InputSize>} 尺寸。 */
size: { type: String, default: 'md' },
})
const emit = defineEmits(['update:modelValue'])
/**
* 转发输入值。
* @param {Event} e
* @returns {void}
*/
function onInput(e) {
emit('update:modelValue', /** @type {HTMLInputElement} */ (e.target).value)
}
</script>
<template>
<input
class="drama-input"
:class="[`drama-input--${size}`, { 'is-invalid': invalid }]"
:type="type"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
:aria-invalid="invalid ? 'true' : 'false'"
@input="onInput"
>
</template>
<style scoped>
/*
* 底色跟所在卡片一致(surface),只留一圈发丝描边 —— 控件不是比卡片更深的一块「凹陷」。
* 用 surface-alt 填充 + border-strong 描边(填充和描边双重强调)时,一排筛选读起来像一格格禁用的死块,
* 值反而是里面最不显眼的东西。层次交给交互态:hover 描边加深,focus 转主色 + 光晕。
* Select / Textarea / SegmentedControl 同此,一排控件才是一套。
*/
.drama-input {
box-sizing: border-box;
width: 100%;
border: 1px solid var(--drama-color-border);
border-radius: var(--drama-radius-sm);
background-color: var(--drama-color-surface);
color: var(--drama-color-text);
font-family: var(--drama-font-family-body);
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
}
.drama-input::placeholder {
color: var(--drama-color-text-muted);
}
.drama-input:hover:not(:disabled):not(:focus) {
border-color: var(--drama-color-border-strong);
}
.drama-input:focus {
border-color: var(--drama-color-primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--drama-color-primary) 18%, transparent);
}
.drama-input:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.drama-input.is-invalid {
border-color: var(--drama-color-danger);
}
.drama-input.is-invalid:focus {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--drama-color-danger) 18%, transparent);
}
/* 高度取 control token,不再由 padding 推 —— 同尺寸的 Select / SegmentedControl / Button 与它等高。 */
.drama-input--sm {
height: var(--drama-control-height-sm);
padding: 0 var(--drama-space-md);
font-size: var(--drama-font-size-sm);
}
.drama-input--md {
height: var(--drama-control-height-md);
padding: 0 var(--drama-space-md);
font-size: var(--drama-font-size-md);
}
.drama-input--lg {
height: var(--drama-control-height-lg);
padding: 0 var(--drama-space-lg);
font-size: var(--drama-font-size-lg);
}
</style>