Files
codex-proxy-rs/frontend/src/components/base/BaseTextarea.vue
T
0a09c1572f feat(accounts): add notes and refine account interactions (#100)
Persist optional account notes across creation, editing, imports, and credential refreshes. Show notes in compact account rows with overflow-only popovers.

Support vertically resizable textareas, bounded select menus with custom scrollbars, and quota updates without table loading.

Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-09-16 01:36:34 +08:00

84 lines
2.7 KiB
Vue

<script setup lang="ts">
import { computed, inject, useAttrs } from 'vue'
import { formFieldKey } from './BaseForm/context'
type TextareaSize = 'sm' | 'md' | 'lg'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
placeholder?: string
disabled?: boolean
size?: TextareaSize
rows?: number
resize?: 'none' | 'both' | 'horizontal' | 'vertical'
}>(),
{
placeholder: '',
disabled: false,
size: 'md',
rows: 5,
resize: 'vertical',
},
)
const model = defineModel<string>({ default: '' })
const attrs = useAttrs()
const field = inject(formFieldKey, null)
const controlId = computed(() => typeof attrs.id === 'string' ? attrs.id : field?.controlId.value)
const invalid = computed(() => Boolean(
field?.invalid.value || attrs['aria-invalid'] === true || attrs['aria-invalid'] === 'true',
))
const describedBy = computed(() => [
typeof attrs['aria-describedby'] === 'string' ? attrs['aria-describedby'] : undefined,
field?.describedBy.value,
].filter(Boolean).join(' ') || undefined)
const rootAttrs = computed(() => ({ class: attrs.class, style: attrs.style }))
const controlAttrs = computed(() => Object.fromEntries(
Object.entries(attrs).filter(([key]) => ![
'class',
'style',
'id',
'aria-describedby',
'aria-invalid',
'aria-required',
].includes(key)),
))
const sizeClasses: Record<TextareaSize, string> = {
sm: 'px-3 py-2.5 text-xs',
md: 'px-3.5 py-3 text-cp',
lg: 'px-4 py-3.5 text-cp-lg',
}
const textareaClasses = computed(() => [
'cp-scrollbar w-full rounded-cp border-0 bg-[var(--cp-input-bg)] text-cp-text shadow-cp-input outline-none transition-[background-color,box-shadow,color] duration-160 placeholder:text-cp-text-quaternary motion-reduce:transition-none',
'hover:bg-[var(--cp-input-hover-bg)] hover:shadow-cp-input-hover focus:bg-(--cp-input-active-bg) focus:shadow-cp-input-active',
'disabled:cursor-not-allowed disabled:bg-cp-bg-container-disabled disabled:text-cp-text-disabled disabled:shadow-none',
'leading-[1.55] font-emphasis',
sizeClasses[props.size],
invalid.value ? 'bg-(--cp-input-error-active-bg) shadow-cp-input-error-active' : undefined,
])
</script>
<template>
<div v-bind="rootAttrs" class="min-w-0">
<textarea
v-bind="controlAttrs"
:id="controlId"
v-model="model"
:class="textareaClasses"
:style="{ resize: disabled ? 'none' : resize }"
:rows="rows"
:placeholder="placeholder"
:disabled="disabled"
:required="field?.required.value || undefined"
:aria-describedby="describedBy"
:aria-invalid="invalid || undefined"
:aria-required="field?.required.value || undefined"
/>
</div>
</template>