# Input 输入框

原生 `<input>` 增强的基础输入组件。

## 基础用法

<DemoBlock title="基础用法">
  <oas-input placeholder="请输入内容" style="width: 240px"></oas-input>
</DemoBlock>

## 无障碍名称（label）

<DemoBlock title="label（可访问名称）">
  <oas-input id="input-label-set" label="登录邮箱" placeholder="name@example.com" style="width: 240px"></oas-input>
  <oas-input id="input-label" placeholder="无 label，回退占位文本" style="width: 240px"></oas-input>
  <span id="input-label-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 260px"></span>
</DemoBlock>

`label` 作为输入框的可访问名称（`aria-label`）来源，读屏朗读该名称。未设置 `label` 时依次回退 `placeholder` → 内置文案「输入框」；设置后（如「登录邮箱」）覆盖回退链。

## 原生表单与 label 关联

<DemoBlock title="form-associated：label for / FormData / reset">
  <form id="input-form-demo" style="width: 100%; display: flex; flex-direction: column; gap: var(--oas-space-3); align-items: flex-start">
    <label for="input-form-username" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm)">姓名（点击本行文字 → 聚焦输入框）</label>
    <oas-input id="input-form-username" name="username" value="初始值" style="width: 240px"></oas-input>
    <div style="display: flex; gap: var(--oas-space-2)">
      <oas-button id="input-form-submit" size="small" type="button">读取 FormData</oas-button>
      <oas-button id="input-form-reset" size="small" type="button">form.reset()</oas-button>
    </div>
    <span id="input-form-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm)"></span>
  </form>
</DemoBlock>

组件是 **form-associated** 自定义元素（`formAssociated: true`）：`<label for>` 原生关联生效（点击 label 聚焦到 shadow 内真实 input，读屏朗读 label 文本）；放进原生 `<form>` 后经标准 `FormData` 收集（有 `name` 才提交）、`form.reset()` 回到 `value` 初始值、父级 `fieldset[disabled]` 联动禁用。与 `oas-form` 的 `collectFields` 机制并行可用。

程序性读/写当前值走公开 `value` property（对齐原生 `input.value`）：`el.value` 读取——parser/mask 感知的提交值语义（与 `oas-input`/`oas-change`/FormData 口径一致）；`el.value = x` 写入——写受控 `value` 属性并即时回写内部控件，不派发事件。**受控赋值即生效语义**：即使传入值与当前属性值相同也会强制回写内部控件；Vue 等框架对自定义元素的 `:value` 绑定在每次 patch 会无条件重写，受控绑定下请以事件（`oas-input`/`oas-change`）同步外部状态，避免无关重渲染抹掉用户未提交的输入。

## 类型

<DemoBlock title="type">
  <oas-input type="password" placeholder="密码" style="width: 240px"></oas-input>
  <oas-input type="number" placeholder="数字" style="width: 240px"></oas-input>
  <oas-input type="email" placeholder="邮箱" style="width: 240px"></oas-input>
</DemoBlock>

`type` 透传原生 input 类型，支持 `text` / `password` / `number` / `email` 等。

## 密码可见切换

<DemoBlock title="show-password">
  <oas-input type="password" show-password placeholder="密码" value="oasui123" style="width: 240px"></oas-input>
</DemoBlock>

`show-password` 在 `type="password"` 时于输入框右侧渲染眼睛按钮，点击在明文/密文间切换；按钮带 `aria-label`（locale 文案）与 `aria-pressed`，聚焦时有焦点环。

## 可清空

<DemoBlock title="可清空（clearable）">
  <oas-input clearable value="有内容时可一键清空" style="width: 240px"></oas-input>
</DemoBlock>

存在内容且设置 `clearable` 时显示清除按钮，点击后清空并聚焦，派发 `oas-clear`。

## 加载态

<DemoBlock title="loading 加载态（不禁用输入）">
  <oas-input id="input-loading" loading clearable value="远程校验中…" style="width: 240px"></oas-input>
  <oas-button id="btn-input-loading" size="small">切换 loading</oas-button>
  <span id="input-loading-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 160px"></span>
</DemoBlock>

设置 `loading` 后输入框行尾显示 spinner 并同步 `aria-busy="true"`：**输入不被禁用**（用户可继续编辑，适合远程校验/联想请求场景）；与 `clearable` 共存时 spinner 优先、清除按钮让位，退出 `loading` 后恢复。

## 禁用与只读

<DemoBlock title="disabled / readonly">
  <oas-input disabled placeholder="禁用" style="width: 240px"></oas-input>
  <oas-input readonly value="只读内容" style="width: 240px"></oas-input>
</DemoBlock>

## 前后缀（addon）

<DemoBlock title="addon-before / addon-after">
  <oas-input addon-before="http://" placeholder="域名" style="width: 240px"></oas-input>
  <oas-input addon-after="元" placeholder="金额" style="width: 240px"></oas-input>
  <oas-input addon-before="¥" addon-after="/人" placeholder="单价" style="width: 240px"></oas-input>
</DemoBlock>

`addon-before` / `addon-after` 为输入框外侧的 addon 文案块（如单位、域名），分别走独立的 `::part(prepend)` / `::part(append)`，禁用时 addon 灰化。

## 图标

<DemoBlock title="prefix-icon / suffix-icon">
  <oas-input prefix-icon="search" placeholder="搜索" style="width: 240px"></oas-input>
  <oas-input suffix-icon="eye" placeholder="密码" type="password" style="width: 240px"></oas-input>
</DemoBlock>

`prefix-icon` / `suffix-icon` 接受图标名（`@oas-ui/icons` 的 内置图标名），内联渲染 SVG 装饰图标。

## 内嵌前后缀与清空并存

<DemoBlock title="prefix / suffix + clearable">
  <oas-input prefix-text="$" suffix-text=".00" clearable value="1280" style="width: 240px"></oas-input>
  <oas-input suffix-icon="chevron-down" clearable value="可清空带图标" style="width: 240px"></oas-input>
</DemoBlock>

`prefix-text` / `suffix-text` 为输入框内部文案，与 `clearable`、图标、addon 可并存不冲突。`prefix` / `suffix` 为兼容别名（`attribute` 与 `property` 两条通道均可写入，React / Vue 绑定也生效）——**规范名是 `prefix-text` / `suffix-text`**。

## 自定义清除图标（clear-icon 插槽）

<DemoBlock title="clear-icon 插槽">
  <oas-input id="input-clear-icon-default" clearable value="默认清除图标" style="width: 220px"></oas-input>
  <oas-input id="input-clear-icon-custom" clearable value="自定义清除图标" style="width: 220px">
    <oas-icon slot="clear-icon" name="close-circle"></oas-icon>
  </oas-input>
</DemoBlock>

`clearable` 的清除按钮默认渲染内置关闭图标；需要个性化时用 `slot="clear-icon"` 分发自定义图标（slot 有内容时原生替换内置 fallback，图标尺寸由分发内容自定）。

## 字数统计

<DemoBlock title="show-count + maxlength">
  <oas-input show-count maxlength="10" placeholder="最多输入 10 个字" style="width: 240px"></oas-input>
  <oas-input show-count value="无长度限制" style="width: 240px"></oas-input>
</DemoBlock>

`show-count` 在输入框右下角显示字数统计：设置 `maxlength` 时显示 `当前长度/maxlength`，未设置时仅显示当前长度；`maxlength` 同时透传原生 input 限制输入长度。超过限制时计数数字变 danger 色。

## 提示文案（hint）

<DemoBlock title="hint 静态提示（与校验错误独立）">
  <oas-input hint="手机号仅用于登录验证，不会对外展示" placeholder="手机号" style="width: 260px"></oas-input>
  <oas-input id="input-hint-error" hint="格式：YYYY-MM-DD" value="2024/01/15" style="width: 220px"></oas-input>
  <oas-button id="btn-input-hint-error" size="small">切换校验错误</oas-button>
  <span id="input-hint-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 240px"></span>
</DemoBlock>

`hint` 在输入框下方常驻一段静态提示文案（字号 `--oas-font-size-sm`、次要色），并通过 `aria-describedby` 关联到内层 input（读屏朗读提示内容）。它与校验错误（`status` / `aria-invalid`）**相互独立**：错误态出现时提示仍保留。点击按钮切换 `status="error"` 可看到错误边框与提示文案共存。

## 回车提交事件

<DemoBlock title="oas-enter">
  <oas-input id="input-enter" placeholder="输入后按回车" style="width: 240px"></oas-input>
  <span id="enter-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 160px"></span>
</DemoBlock>

输入时按 Enter（非输入法组合上屏）派发 `oas-enter`，`detail: { value }`。

## 事件

<DemoBlock title="输入与清除事件">
  <oas-input id="input-event" clearable placeholder="输入或点击清除" style="width: 240px"></oas-input>
  <span id="input-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 160px"></span>
</DemoBlock>

监听 `oas-input`（输入中）与 `oas-clear`（清除）：

<script setup>
import { onMounted } from 'vue'
onMounted(() => {
  const el = document.getElementById('input-event')
  const out = document.getElementById('input-output')
  el?.addEventListener('oas-input', (e) => {
    out.textContent = `oas-input: ${e.detail.value}`
  })
  el?.addEventListener('oas-clear', () => {
    out.textContent = 'oas-clear'
  })
  const enter = document.getElementById('input-enter')
  const enterOut = document.getElementById('enter-output')
  enter?.addEventListener('oas-enter', (e) => {
    enterOut.textContent = `oas-enter: ${e.detail.value}`
  })

  // label（可访问名称）demo：等组件升级后读取内层 input 的 aria-label
  const labelSet = document.getElementById('input-label-set')
  const labelFallback = document.getElementById('input-label')
  const labelOut = document.getElementById('input-label-output')
  const readLabel = () => {
    const a = labelSet?.shadowRoot?.querySelector('input')?.getAttribute('aria-label')
    const b = labelFallback?.shadowRoot?.querySelector('input')?.getAttribute('aria-label')
    if (a !== undefined && b !== undefined) {
      labelOut.textContent = `aria-label：设置「${a}」 / 回退「${b}」`
    } else {
      setTimeout(readLabel, 60)
    }
  }
  readLabel()

  // focus / blur / change + select() 方法
  const fcb = document.getElementById('input-fcb')
  const fcbOut = document.getElementById('input-fcb-output')
  fcb?.addEventListener('oas-focus', () => (fcbOut.textContent = 'oas-focus'))
  fcb?.addEventListener('oas-blur', () => (fcbOut.textContent = 'oas-blur'))
  fcb?.addEventListener('oas-change', (e) => (fcbOut.textContent = `oas-change: ${e.detail.value}`))
  document.getElementById('btn-input-select')?.addEventListener('click', () => {
    fcb?.focus()
    fcb?.select()
    fcbOut.textContent = '已调用 select()（内容全选）'
  })

  // addon slot 搜索组合
  document.getElementById('input-search-btn')?.addEventListener('click', () => {
    const el = document.getElementById('input-search-output'); if (el) el.textContent = '触发搜索'
  })

  // 块级 addon demo：block-end 内按钮清空输入（公开 value setter 即时回写，可见反馈）
  document.getElementById('input-block-clear')?.addEventListener('click', () => {
    const host = document.getElementById('input-block-addon')
    if (host) host.value = ''
    const out = document.getElementById('input-block-output')
    if (out) out.textContent = '已清空地址（block-end 按钮生效）'
  })

  // loading 加载态 demo：切换 loading 属性并同步输出
  const loadingEl = document.getElementById('input-loading')
  const loadingOut = document.getElementById('input-loading-output')
  document.getElementById('btn-input-loading')?.addEventListener('click', () => {
    if (!loadingEl) return
    const next = loadingEl.hasAttribute('loading')
    if (next) loadingEl.removeAttribute('loading')
    else loadingEl.setAttribute('loading', '')
    if (loadingOut) loadingOut.textContent = next ? 'loading 已退出（clear 恢复）' : 'loading 中（clear 让位）'
  })

  // allow-over-max 超限 validate 反馈
  const over = document.getElementById('input-overmax')
  const overOut = document.getElementById('input-overmax-output')
  over?.addEventListener('oas-validate', (e) => {
    overOut.textContent = e.detail.error
      ? `oas-validate: ${e.detail.error}`
      : 'oas-validate: 已回到限内'
  })

  // formatter / parser（仅 property 通道）：千分位
  const fmt = document.getElementById('input-format')
  const fmtOut = document.getElementById('input-format-output')
  if (fmt) {
    fmt.formatter = (v) => v.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
    fmt.parser = (v) => v.replace(/,/g, '')
    fmt.addEventListener('oas-input', (e) => {
      fmtOut.textContent = `原始值: ${e.detail.value}`
    })
  }

  // mask 输入掩码：默认提交显示值 vs mask-raw 提交原始序列（事件值实时回显）
  const maskEl = document.getElementById('input-mask')
  const maskRawEl = document.getElementById('input-mask-raw')
  const maskOut = document.getElementById('input-mask-output')
  maskEl?.addEventListener('oas-input', (e) => {
    if (maskOut) maskOut.textContent = `默认（显示值提交）: ${e.detail.value || '—'}`
  })
  maskRawEl?.addEventListener('oas-input', (e) => {
    if (maskOut) maskOut.textContent = `mask-raw（原始序列提交）: ${e.detail.value || '—'}`
  })

  // form-associated：原生 label 关联 + FormData 读取 + reset
  const faForm = document.getElementById('input-form-demo')
  const faOut = document.getElementById('input-form-output')
  document.getElementById('input-form-submit')?.addEventListener('click', () => {
    if (!faForm) return
    const fd = new FormData(faForm)
    faOut.textContent = `FormData: username=${JSON.stringify(fd.get('username'))}`
  })
  document.getElementById('input-form-reset')?.addEventListener('click', () => {
    faForm?.reset()
    if (faOut) faOut.textContent = 'form.reset() 已执行，值回到初始值'
  })

  // hint 静态提示 demo：切换校验错误，提示文案与 aria-describedby 独立保留
  const hintEl = document.getElementById('input-hint-error')
  const hintOut = document.getElementById('input-hint-output')
  const readHint = () => {
    if (!hintEl) return
    const inner = hintEl.shadowRoot?.querySelector('input')
    const describedBy = inner?.getAttribute('aria-describedby') ?? null
    if (hintOut) {
      hintOut.textContent = `${hintEl.hasAttribute('status') ? '校验错误态' : '正常态'}；aria-describedby=${describedBy}`
    }
  }
  document.getElementById('btn-input-hint-error')?.addEventListener('click', () => {
    if (!hintEl) return
    if (hintEl.hasAttribute('status')) hintEl.removeAttribute('status')
    else hintEl.setAttribute('status', 'error')
    readHint()
  })
  if (hintEl) setTimeout(readHint, 60)

  // min / max / step（number 类型）透传 demo：读回内层原生属性
  const rangeEl = document.getElementById('input-number-range')
  const rangeOut = document.getElementById('input-number-range-output')
  const readRange = () => {
    const inner = rangeEl?.shadowRoot?.querySelector('input')
    if (!inner) {
      setTimeout(readRange, 60)
      return
    }
    if (rangeOut) {
      rangeOut.textContent = `原生透传：min=${inner.getAttribute('min')} max=${inner.getAttribute('max')} step=${inner.getAttribute('step')}`
    }
  }
  if (rangeEl) readRange()
})
</script>

## 复杂前后缀（slot 分发）

<DemoBlock title="slot=prefix / slot=suffix">
  <oas-input placeholder="搜索用户名" style="width: 240px">
    <oas-icon slot="prefix" name="search"></oas-icon>
  </oas-input>
  <oas-input placeholder="手机号" style="width: 240px">
    <span slot="suffix">📱</span>
  </oas-input>
  <oas-input placeholder="金额" value="1280" prefix-text="¥" suffix-text=".00" style="width: 240px"></oas-input>
</DemoBlock>

简单文本用 `prefix-text` / `suffix-text` 属性；复杂内容（图标/按钮/徽标等）用同名 `slot="prefix"` / `slot="suffix"` 分发，slot 有内容时原生替换属性文本。`prefix` / `suffix` 为兼容别名（规范名 `prefix-text` / `suffix-text`），attribute 与 property（React / Vue 绑定）两条通道均可用。

## 尺寸

<DemoBlock title="size 尺寸档位">
  <oas-input size="small" placeholder="小号" style="width: 200px"></oas-input>
  <oas-input placeholder="中号（默认）" style="width: 200px"></oas-input>
  <oas-input size="large" placeholder="大号" style="width: 200px"></oas-input>
</DemoBlock>

`size` 支持 `small` / `medium`（默认）/ `large` 三档，高度对齐全局尺寸 token（`--oas-control-height-sm/md/lg`，24/32/40px），字号同步缩放，内嵌前后缀、addon、清除/眼睛按钮同档联动。未显式设置时就近继承 `oas-config-provider` 的 `size` 注入（全局表单密度）。

## 形态

<DemoBlock title="variant 形态">
  <oas-input placeholder="outlined（默认）" style="width: 200px"></oas-input>
  <oas-input variant="filled" placeholder="filled 填充" style="width: 200px"></oas-input>
  <oas-input variant="borderless" placeholder="borderless 无框" style="width: 200px"></oas-input>
</DemoBlock>

`variant` 支持 `outlined`（默认描边）/ `filled`（填充底色、聚焦才出边框）/ `borderless`（无框无底色，用于纯文本嵌入场景）。

## 校验态

<DemoBlock title="status 校验态">
  <oas-input status="error" value="校验未通过" style="width: 200px"></oas-input>
  <oas-input status="warning" value="有警告" style="width: 200px"></oas-input>
  <oas-input status="success" value="校验通过" style="width: 200px"></oas-input>
</DemoBlock>

`status` 支持 `error` / `warning` / `success` 三态：边框与焦点环切换语义色。`error` 会同步内层 input 的 `aria-invalid="true"`（读屏可感知「无效」）；`warning` / `success` 仅视觉提示、不标无效语义。表单校验（`oas-form`）写宿主 `aria-invalid` 的既有通道仍然生效，与 `status` 可叠加（error 优先）。

## 原生属性透传

<DemoBlock title="原生属性透传白名单">
  <oas-input name="username" autocomplete="username" required placeholder="用户名（必填）" style="width: 240px"></oas-input>
  <oas-input name="code" inputmode="numeric" maxlength="6" placeholder="验证码（数字键盘）" style="width: 240px"></oas-input>
  <oas-input name="nick" minlength="2" spellcheck="false" enterkeyhint="done" placeholder="昵称（至少 2 字）" style="width: 240px"></oas-input>
  <oas-input id="input-number-range" type="number" min="0" max="10" step="2" placeholder="数字（0-10，步长 2）" style="width: 240px"></oas-input>
  <span id="input-number-range-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 220px"></span>
</DemoBlock>

以下原生属性镜像到 shadow 内原生 input（宿主移除后同步解除）：`name` / `autocomplete` / `autofocus` / `inputmode` / `minlength` / `min` / `max` / `step` / `required` / `spellcheck` / `enterkeyhint` / `pattern`。可用于原生表单语义、自动填充与移动端键盘优化（`inputmode` / `enterkeyhint`）；其中 `min` / `max` / `step` 供 `type="number"` 使用（其余类型浏览器原生忽略）。`autofocus` 由组件在挂载后转发聚焦一次（原生 autofocus 不穿透 shadow DOM），页面加载即可聚焦输入框。

## 焦点与提交事件

<DemoBlock title="oas-focus / oas-blur / oas-change + select()">
  <oas-input id="input-fcb" placeholder="聚焦、输入后失焦试试" style="width: 220px"></oas-input>
  <oas-button id="btn-input-select" size="small">全选</oas-button>
  <span id="input-fcb-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 180px"></span>
</DemoBlock>

- `oas-focus` / `oas-blur`：内层 input 聚焦/失焦时派发，`detail: { value }` 携带当前值。
- `oas-change`：**提交语义**——失焦或按 Enter 时值相对上次提交发生变化才派发（输入中不派发，与 `oas-input` 区分）。
- `select()` 方法：全选输入框内容（委托原生 input.select）；`blur()` 同理委托失焦。

## addon 复杂内容（slot 分发）

<DemoBlock title="slot=prepend / slot=append（文本 / 图标 / 按钮）">
  <oas-input placeholder="输入网站名称" style="width: 360px">
    <span slot="prepend">http://</span>
    <span slot="append">.com</span>
  </oas-input>
  <oas-input placeholder="搜索关键词" style="width: 300px">
    <oas-icon slot="prefix" name="search"></oas-icon>
    <oas-button slot="append" id="input-search-btn" type="primary">搜索</oas-button>
  </oas-input>
  <span id="input-search-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 100px"></span>
</DemoBlock>

`addon-before` / `addon-after` 属性只接文案；复杂内容用 `slot="prepend"` / `slot="append"` 分发。纯文本、图标渲染在灰底 addon 面上（圆角合并/边框去重与 addon 属性一致，文本双通道并存——slot 有内容时原生替换属性 fallback）。分发**自包含控件**（`oas-button` / `oas-button-group` / `oas-compact`）时，addon 会自动退化为**贴合容器**：去掉文本 addon 的灰底、描边与左右内边距，高度与输入框同档对齐，相邻边 -1px 压边合并为单线，并把圆角合并协议（`--oas-button-group-radius`）穿透给控件，使其外角与输入框外框拼成连续圆角。**选择器等自带边框/底色的复合控件仍不建议放进 addon 槽**：跨 Shadow 边界无法安全去除其内部样式（其下拉层与触发器共享颜色 token，透明化会连带下拉菜单），与输入框拼接复合控件请用 `oas-compact` 紧凑容器：

<DemoBlock title="select + input 组合（oas-compact 紧凑拼接）">
  <oas-compact style="width: 420px">
    <oas-select placeholder="协议" options='[{"label":"http://","value":"http"},{"label":"https://","value":"https"}]' style="width: 120px"></oas-select>
    <oas-input placeholder="输入网站名称"></oas-input>
    <oas-button type="primary">访问</oas-button>
  </oas-compact>
</DemoBlock>

## 块级 addon（block-start / block-end）

<DemoBlock title="slot=block-start / slot=block-end（输入框上/下方整行块）">
  <oas-input id="input-block-addon" placeholder="收件地址" show-count maxlength="20" style="width: 320px">
    <span slot="block-start">📦 配送到：北京市海淀区</span>
    <oas-button slot="block-end" size="small" variant="text" id="input-block-clear">清空地址</oas-button>
  </oas-input>
  <span id="input-block-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 160px"></span>
</DemoBlock>

`slot="block-start"` / `slot="block-end"` 在输入框**上方 / 下方**各渲染一个整行块级 addon：与 `prepend` / `append`（水平拼接在输入框两侧）不同维度，块级行独立占位、不参与边框拼接，可放提示文案、动作按钮、链接等任意内容。行容器字号走 `--oas-font-size-sm`、次要色（禁用时灰化），经 `::part(block-start)` / `::part(block-end)` 可深度定制。与 `show-count`（outside 计数同在输入框下方）并存时，计数自动挂进 `block-end` 行内靠右显示，不重叠。

## 字数统计位置与字素计数

<DemoBlock title="count-position + emoji 字素计数">
  <oas-input show-count count-position="inside" maxlength="10" placeholder="计数在框内" style="width: 220px"></oas-input>
  <oas-input show-count maxlength="10" value="👍👨‍👩‍👧你" placeholder="emoji 按字素计数" style="width: 220px"></oas-input>
</DemoBlock>

`show-count` 计数按**字素（grapheme）**统计（内建 `Intl.Segmenter`，无该 API 时按码点兜底）——emoji、ZWJ 组合字符（如家庭 emoji）计 1，避免 JS `length` 把一个 emoji 算作多个。`count-position="inside"` 把计数移到输入区内右侧（默认 `outside` 右下角）。注意：`maxlength` 的原生截断仍按码元口径，与统计口径分离（需要超限场景见下方 `allow-over-max`）。

## 超限不截断

<DemoBlock title="allow-over-max + oas-validate">
  <oas-input id="input-overmax" allow-over-max show-count maxlength="5" placeholder="超过 5 字可继续输入" style="width: 240px"></oas-input>
  <span id="input-overmax-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 220px"></span>
</DemoBlock>

默认 `maxlength` 透传原生硬截断；设置 `allow-over-max` 后不再截断，超限可继续输入，计数标红（danger），并在越界状态翻转时派发 `oas-validate`（`detail.error` 为 `exceed-maximum`，回到限内为 `null`），宿主可据此提示而不打断输入。

## 显示格式化（formatter / parser）

<DemoBlock title="formatter / parser（千分位，property 通道）">
  <oas-input id="input-format" placeholder="输入数字自动加千分位" style="width: 260px"></oas-input>
  <span id="input-format-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 140px"></span>
</DemoBlock>

`formatter` / `parser` **仅 JS property 通道**（`el.formatter = fn`）——Web Components 的 attribute 无法传函数，无对应 HTML 属性。显示值 = `formatter(原始值)`，`oas-input` / `oas-change` 等事件 `detail.value` 携带 `parser(显示值)` 解析后的原始值；移除（置 `null`）恢复原始显示。输入时光标按格式化前后长度差近似保持（卡号分段等场景够用）。

## 输入掩码（mask）

<DemoBlock title="mask 输入掩码（###-####）与 mask-raw 原始值">
  <oas-input id="input-mask" mask="###-####" placeholder="编号（如 1234567）" style="width: 200px"></oas-input>
  <oas-input id="input-mask-raw" mask="###-####" mask-raw placeholder="同掩码 + mask-raw" style="width: 200px"></oas-input>
  <span id="input-mask-output" style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); min-width: 240px"></span>
</DemoBlock>

`mask` 定义输入掩码模板：`#` 数字、`A` 字母、`*` 字母数字，其余字符为字面量（如 `###-####` 的 `-`）。键入自动跳过字面量、非法字符被过滤、粘贴只收合法位、退格删到字面量即停（字面量由模板管理，不会被删穿），光标始终保持在正确位置。默认事件与表单提交值携带**显示值**（含字面量）；设置 `mask-raw` 后改为**去格式化的原始字符序列**（FormData 提交值同口径）。`mask` 与 `formatter`/`parser` 互斥，同设时 mask 优先并告警；`maxlength` 在 mask 模式下不透传（避免原生截断破坏掩码）。（`show-count` 计数口径随提交通道：缺省按显示值（含字面量），`mask-raw` 时按原始序列）

## 清除按钮显隐

<DemoBlock title="show-clear-on">
  <oas-input clearable show-clear-on="focus" value="聚焦时才显示清除" style="width: 220px"></oas-input>
  <oas-input clearable show-clear-on="hover" value="悬浮或聚焦时显示" style="width: 220px"></oas-input>
</DemoBlock>

`show-clear-on` 控制清除按钮显隐策略：默认 `always`（有值即常显，现状不变）；`focus` 仅聚焦时显示；`hover` 悬浮或聚焦时显示（降低密集表单的视觉噪声）。

## 宽度自适应

<DemoBlock title="auto-width">
  <oas-input auto-width auto-width-min="120" value="短文本" placeholder="输入内容试试" style="margin-inline-end: 24px"></oas-input>
  <oas-input auto-width auto-width-min="120" auto-width-max="320" value="宽度随内容变化的输入框" placeholder="auto-width"></oas-input>
</DemoBlock>

`auto-width` 开启后宽度随内容自适应（mirror 测宽技术：与输入框同字体/内边距的隐藏测量元素，空值回落占位文本宽度）；`auto-width-min` / `auto-width-max`（px 数值）钳制宽度范围，`min` 缺省 72px、`max` 缺省 100%。行内编辑、标签输入等场景常用。

## 范围输入（组合写法）

<DemoBlock title="两个输入框 + 分隔符（范围输入）">
  <div style="display: inline-flex; align-items: center; gap: var(--oas-space-2)">
    <oas-input placeholder="最低价" inputmode="numeric" style="width: 120px"></oas-input>
    <span style="color: var(--oas-color-text-secondary)">~</span>
    <oas-input placeholder="最高价" inputmode="numeric" style="width: 120px"></oas-input>
  </div>
</DemoBlock>

范围输入用两个 `oas-input` + 分隔符布局即可（组件不提供 pair 双输入 API）：两个输入框各自的 `oas-change` 提交值，宿主组合成区间。需要紧凑圆角拼接时外层套 `oas-compact` 容器。

## API

### oas-input

#### 属性

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| `addon-after` | 后置 addon 文案块 | — | — |
| `addon-before` | 前置 addon 文案块 | — | — |
| `allow-over-max` | 超限不截断：maxlength 不再透传原生（可继续输入），计数标红并派发 oas-validate | `boolean` | — |
| `auto-width` | 宽度随内容自适应（mirror 测宽；空值回落 placeholder 宽），常配 auto-width-min/max 钳制 | `boolean` | — |
| `auto-width-max` | auto-width 最大宽度 px 或百分比（默认 100%） | — | — |
| `auto-width-min` | auto-width 最小宽度 px（默认 72） | — | — |
| `autocomplete` | 透传内层原生 input 的自动填充提示（密码管理器/浏览器自动填充） | — | — |
| `autofocus` | 挂载后自动聚焦内层输入框（原生 autofocus 不穿透 shadow，组件转发） | `boolean` | — |
| `clearable` | 可清空 | `boolean` | — |
| `count-position` | 字数计数位置：`outside`（默认，框外）/ `inside`（输入区内右侧） | `string` | — |
| `disabled` | 禁用 | `boolean` | — |
| `enterkeyhint` | 透传内层原生 input 的移动虚拟键盘回车键形态（如 search/send/go） | — | — |
| `hint` | 输入框下方常驻静态提示文案（aria-describedby 关联，独立于校验错误） | `string` | — |
| `inputmode` | 透传内层原生 input 的移动虚拟键盘类型 | — | — |
| `label` | 可访问名称（`aria-label` 来源，未设时回退 `placeholder` → 内置文案「输入框」） | — | — |
| `loading` | 加载态：行尾 spinner + aria-busy（不禁用输入）；与 clearable 共存时优先显示 | `boolean` | — |
| `mask` | 输入掩码模板：`#` 数字 / `A` 字母 / `*` 字母数字，其余字符为字面量（如 `###-####`）；键入自动跳字面量、粘贴只收合法位、退格删到字面量停；与 formatter/parser 互斥（mask 优先） | `string` | — |
| `mask-raw` | 提交值/FormData 为去格式化的原始字符序列；缺省提交显示值 | `boolean` | — |
| `max` | 透传内层原生 input 的最大值 | — | — |
| `maxlength` | 最大输入长度（透传原生 maxlength） | `string` | — |
| `min` | 透传内层原生 input 的最小值（number 类型生效） | — | — |
| `minlength` | 透传内层原生 input 的最小长度约束 | — | — |
| `name` | 原生表单字段名（form-associated：FormData 提交的键） | — | — |
| `pattern` | 透传内层原生 input 的正则校验模式 | — | — |
| `placeholder` | 占位提示 | `string` | — |
| `prefix-icon` | 前置图标名 | `string` | — |
| `prefix-text` | 内嵌前置文案；`prefix` 为兼容别名（attribute 与 property 通道均可用） | `string` | — |
| `readonly` | 只读 | `boolean` | — |
| `required` | 必填标记（透传原生 input；驱动原生校验链 valueMissing） | `boolean` | — |
| `show-clear-on` | 清除按钮显隐：`always`（默认常显）/ `hover` / `focus` | `string` | — |
| `show-count` | 显示字数统计（右下角，超限标 danger） | `boolean` | — |
| `show-password` | 密码可见切换（`type="password"` 时渲染眼睛按钮） | `boolean` | — |
| `size` | 尺寸档位 `small` / `medium`（默认）/ `large`：高度与字号联动 | `string` | `medium` |
| `spellcheck` | 透传内层原生 input 的拼写检查开关 | — | — |
| `status` | 校验态：`error` / `warning` / `success`；error 同步内层 aria-invalid | `string` | — |
| `step` | 透传内层原生 input 的步长 | — | — |
| `suffix-icon` | 后置图标名 | `string` | — |
| `suffix-text` | 内嵌后置文案；`suffix` 为兼容别名（attribute 与 property 通道均可用） | `string` | — |
| `type` | 原生 input 类型 | `string` | `text` |
| `value` | 值（受控） | `string` | — |
| `variant` | 形态：`outlined`（默认描边）/ `filled`（填充底色）/ `borderless`（无框） | `string` | — |

#### Property（JS property；是否写回 attribute 见各行说明）

| Property | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| `value` | 当前值：parser/mask 感知的提交值语义（`el.value` 读；`el.value = x` 写受控 `value` 并即时回写内部控件，不派发事件） | `string` | — |

#### 事件

| 事件 | 说明 |
| --- | --- |
| `oas-blur` | 失焦时派发，`detail: { value }` |
| `oas-change` | 提交语义：失焦/Enter 且值相对上次提交变化时派发，`detail: { value }` |
| `oas-clear` | 点击清除，`detail: { originalEvent }` |
| `oas-enter` | 按 Enter（非输入法组合），`detail: { value }` |
| `oas-focus` | 聚焦时派发，`detail: { value }` |
| `oas-input` | 输入中，`detail: { value }` |
| `oas-validate` | allow-over-max 越界状态翻转时派发，`detail: { error: "exceed-maximum" \| null }` |

#### 插槽

| 名称 | 说明 |
| --- | --- |
| `append` | 后置 addon 区（可嵌 select/按钮等任意内容） |
| `block-end` | 输入框下方整行块级 addon（动作行/计数行，任意内容；outside 字数计数并存时自动挂入本行） |
| `block-start` | 输入框上方整行块级 addon（提示行/说明行，任意内容） |
| `clear-icon` | 自定义清除按钮图标（插槽有内容即替换内置） |
| `prefix` | 内嵌前置内容（图标/按钮等复杂内容，分发时优先于 `prefix` 属性文本）；简单文本用 `prefix` 属性即可 |
| `prepend` | 前置 addon 区（可嵌 select/按钮等任意内容） |
| `suffix` | 内嵌后置内容（图标/按钮等复杂内容，分发时优先于 `suffix` 属性文本）；简单文本用 `suffix` 属性即可 |

#### CSS 变量

| CSS 变量 | 默认值 |
| --- | --- |
| `--oas-button-group-radius` | `var(--oas-radius-md)` |
| `--oas-input-auto-max` | `100%` |
| `--oas-input-auto-min` | `72px` |
| `--oas-input-measured` | `100%` |
