# Marquee 跑马灯

循环无缝滚动展示长内容的纯展示组件，内容经 slot 无缝循环；支持悬停暂停、`prefers-reduced-motion` 静态降级。滚动速度为像素/秒真实速度（组件测量内容宽推导时长），内容不足一屏时自动填充；`max-loops` 播完派发 `oas-finish`（detail `{ loops }`）。

::: warning 破坏性变更：speed 语义由「秒」改为「像素/秒」
旧版 `speed` 表示单次循环时长（秒）：`speed="8"` 即 8 秒滚完一整轮——内容越宽，视觉速度越快，不同容器下同一取值的实际速度飘忽。
现 `speed` 表示真实滚动速度（像素/秒，默认 48）：组件经 ResizeObserver 测量内容宽度后推导动画时长（时长 = 内容宽 ÷ 速度），内容/容器宽度不同，视觉速度依然恒定。

迁移指引：旧代码里的小数值（如 `speed="8"`）在新语义下是 8px/s（极慢），请按目标速度重设（常用 24–120）。时长语义如需保留，可用 CSS 变量 `--oas-marquee-duration` 直接覆写（测量写入后仍会覆盖，不推荐）。
:::

## 基础用法

<DemoBlock title="默认循环滚动（48px/s）">
  <oas-marquee style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    OAS-UI 组件库 · Web Components 无框架依赖 · TypeScript 类型完备 · 无障碍可达 ·
  </oas-marquee>
</DemoBlock>

## 速度控制

<DemoBlock title="speed=96 快速 / speed=24 慢速（像素/秒）">
  <oas-marquee speed="96" style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    快速滚动：96 像素/秒
  </oas-marquee>
  <oas-marquee speed="24" style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0; margin-top: var(--oas-space-3);">
    慢速滚动：24 像素/秒
  </oas-marquee>
</DemoBlock>

同为 `speed="48"`，200px 的短内容与 2000px 的长内容视觉速度完全一致（旧「时长」语义下相差 10 倍）——这就是速度改用语义的原因。

## auto-fill 自动填充

内容比容器短时，仅复制一组会在平移过半后露出右侧留白（断 seam）。组件测量容器与内容宽度后，把克隆组自动填充到足够份数，平移一整组内容的过程中视口始终有内容，无缝不断 seam。

<DemoBlock title="auto-fill：短内容在宽容器中无缝循环">
  <oas-marquee style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    ✦ 短公告
  </oas-marquee>
</DemoBlock>

- 克隆份数有上限（50 份）：极短内容 + 极宽容器时宁可留白也不让 DOM 无限膨胀；图片墙等重内容场景建议内容本身至少一屏宽。
- 填充份数随容器尺寸变化自动重算（ResizeObserver）。

## 垂直滚动

`orientation="vertical"` 切换为垂直滚动（公告长文场景）。**容器必须固定高**（overflow 裁切才有意义），高度由宿主样式给出。

<DemoBlock title="orientation=vertical：垂直滚动（容器固定高 96px）">
  <oas-marquee orientation="vertical" style="height: 96px; border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2);">
    <div>第一条公告：新版本发布</div>
    <div>第二条公告：跑马灯支持像素/秒真实速度</div>
    <div>第三条公告：内容不足一屏自动填充</div>
    <div>第四条公告：动态追加内容滚动不跳变</div>
  </oas-marquee>
</DemoBlock>

垂直跑马灯是连续流动语义；需要「逐条滚动停顿」请用 `oas-carousel` 的垂直轮播。

## 反向滚动

<DemoBlock title="reverse：反向滚动（双行对开）">
  <oas-marquee style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    正向滚动：自左向右排出 · OAS-UI · Web Components ·
  </oas-marquee>
  <oas-marquee reverse style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0; margin-top: var(--oas-space-3);">
    反向滚动：自右向左排出 · OAS-UI · Web Components ·
  </oas-marquee>
</DemoBlock>

## 多行交错

组件本身保持单行连续滚动语义；多行跑马灯由多个 `oas-marquee` 以 `reverse` 交错组合实现（无需额外属性，垂直堆叠即多行）：

<DemoBlock title="多行交错：正 / 反 / 正 三行组合">
  <oas-marquee style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    第一行 · 正向 · OAS-UI · Web Components · 框架无关的 UI 库 ·
  </oas-marquee>
  <oas-marquee reverse style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0; margin-top: var(--oas-space-2);">
    第二行 · 反向 · OAS-UI · Web Components · 框架无关的 UI 库 ·
  </oas-marquee>
  <oas-marquee style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0; margin-top: var(--oas-space-2);">
    第三行 · 正向 · OAS-UI · Web Components · 框架无关的 UI 库 ·
  </oas-marquee>
</DemoBlock>

## 边缘渐隐

`fade-edges` 开启容器两端渐隐（mask-image 遮罩，默认关——渐隐会软化边缘、改变存量外观，按需开启）。渐隐宽度走 CSS 变量 `--oas-marquee-fade-size`（默认 24px）。

<DemoBlock title="fade-edges：logo 墙边缘渐隐">
  <oas-marquee fade-edges style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-3) 0;">
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#0b6cff"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">A</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#16a34a"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">B</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#d97706"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">C</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#dc2626"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">D</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#7c3aed"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">E</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#0891b2"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">F</text></svg>
  </oas-marquee>
</DemoBlock>

## 悬停暂停

<DemoBlock title="pause-on-hover：鼠标悬停/聚焦时暂停">
  <oas-marquee pause-on-hover style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    将鼠标移入本行，滚动暂停；移出后继续。
  </oas-marquee>
</DemoBlock>

## 内容动态更新

宿主向 slot 追加/修改内容时，组件重建克隆组但保持动画相位（记录当前相位、以负 `animation-delay` 续跑），用户看到的滚动位置不跳变。下方实例每 3 秒追加一条实时公告：

<DemoBlock title="动态追加内容：滚动不跳变">
  <oas-marquee id="mq-live" pause-on-hover style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    系统运行正常 ·
  </oas-marquee>
</DemoBlock>

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  // whenDefined 守卫：元素升级后 slotchange 才会驱动克隆组重建与相位保持
  customElements.whenDefined('oas-marquee').then(() => {
    const mq = document.querySelector('#mq-live')
    if (mq) {
      let n = 0
      setInterval(() => {
        n += 1
        const span = document.createElement('span')
        span.textContent = ` 实时公告 ${n} 号 ·`
        mq.appendChild(span)
      }, 3000)
    }

    // max-loops：播完反馈
    const loops = document.querySelector('#mq-loops')
    const status = document.querySelector('#mq-loops-status')
    loops?.addEventListener('oas-finish', () => {
      if (status) status.textContent = 'oas-finish 已派发（loops: 3）——动画定格在末帧，不再滚动'
    })
  })
})
</script>

## 元素内容

<DemoBlock title="slot 支持任意元素组合">
  <oas-marquee speed="60" style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    <oas-tag>新增</oas-tag>
    <span style="margin: 0 var(--oas-space-3);">新版本已发布</span>
    <oas-tag type="success">推荐</oas-tag>
    <span style="margin-left: var(--oas-space-3);">构建于 Web Components 标准之上</span>
  </oas-marquee>
</DemoBlock>

## 图片 / logo 墙

内容不局限于文字——slot 放图片或 logo 即成主流的"品牌墙"无缝滚动。

<DemoBlock title="logo 墙（无缝循环 + 边缘渐隐）">
  <oas-marquee fade-edges style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-3) 0;">
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#0b6cff"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">A</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#16a34a"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">B</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#d97706"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">C</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#dc2626"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">D</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#7c3aed"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">E</text></svg>
    <svg viewBox="0 0 40 40" width="36" height="36" style="margin: 0 var(--oas-space-4); vertical-align: middle;"><rect width="40" height="40" rx="10" fill="#0891b2"/><text x="20" y="26" font-size="18" text-anchor="middle" fill="#fff" font-family="sans-serif">F</text></svg>
  </oas-marquee>
</DemoBlock>

<DemoBlock title="图片滚动（img）">
  <oas-marquee speed="36" style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    <img src="https://picsum.photos/seed/isui-mq-1/120/60" alt="图 1" style="height: 44px; border-radius: var(--oas-radius-sm); margin: 0 var(--oas-space-3); vertical-align: middle;">
    <img src="https://picsum.photos/seed/isui-mq-2/120/60" alt="图 2" style="height: 44px; border-radius: var(--oas-radius-sm); margin: 0 var(--oas-space-3); vertical-align: middle;">
    <img src="https://picsum.photos/seed/isui-mq-3/120/60" alt="图 3" style="height: 44px; border-radius: var(--oas-radius-sm); margin: 0 var(--oas-space-3); vertical-align: middle;">
    <img src="https://picsum.photos/seed/isui-mq-4/120/60" alt="图 4" style="height: 44px; border-radius: var(--oas-radius-sm); margin: 0 var(--oas-space-3); vertical-align: middle;">
  </oas-marquee>
</DemoBlock>

## 限定循环次数（max-loops）

`max-loops` 限定循环轮数（正整数）：播完 N 轮后动画定格在末帧（`animation-fill-mode: forwards`），并派发一次 `oas-finish`（`detail: { loops }`）。未设置或取值非法/非整数时无限循环。轮数 = 位移距离（一份内容宽）被滚过的次数，与内容宽度无关。

`prefers-reduced-motion` 下动画整体关闭、静态展示——没有「播完」，也就不派发 `oas-finish`。

<DemoBlock title="max-loops=3：播 3 轮后定格并派发 oas-finish">
  <oas-marquee id="mq-loops" max-loops="3" speed="160" style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    本条公告循环播放 3 次后停止 ·
  </oas-marquee>
  <p id="mq-loops-status" style="margin-top: var(--oas-space-2); font-size: var(--oas-font-size-sm); color: var(--oas-color-text-secondary)">播放中…</p>
</DemoBlock>

## RTL（右到左）

容器处于 RTL 书写方向（宿主或祖先 `dir="rtl"`、`oas-config-provider direction` 注入均生效）时，平移方向自动镜像：内容**向右**流动、新内容从左缘进入（与 LTR 的「向左流、右缘进入」镜像对称），无缝循环与 auto-fill 填充不受影响。`reverse` 与镜像正交——RTL 下 `reverse` 即向左流动。纵向滚动与书写方向无关。

<DemoBlock title="dir=rtl：平移方向镜像（内容向右流动）">
  <oas-marquee dir="rtl" style="border: 1px solid var(--oas-color-border); border-radius: var(--oas-radius-md); padding: var(--oas-space-2) 0;">
    从左向右流动 · RTL 镜像 · OAS-UI · Web Components ·
  </oas-marquee>
</DemoBlock>

## API

### oas-marquee

#### 属性

| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| `dir` | 书写方向（RTL 判定消费）：RTL 下水平滚动平移方向镜像 | — | — |
| `fade-edges` | 布尔，存在时容器两端 mask-image 渐隐（默认关）；渐隐宽度走 `--oas-marquee-fade-size` | — | — |
| `max-loops` | 限定循环轮数（正整数）：播完定格末帧并派发一次 oas-finish；缺失/非法为无限循环 | `string` | — |
| `orientation` | 滚动方向：`horizontal`（默认）/ `vertical`（垂直滚动，容器需固定高） | — | — |
| `pause-on-hover` | 布尔，存在时悬停/聚焦暂停动画（animation-play-state: paused） | — | — |
| `reverse` | 布尔，存在时反向滚动 | `boolean` | — |
| `speed` | 滚动速度（像素/秒，默认 48）；经测量内容宽推导动画时长（时长=距离/速度），非法/非正数回退默认 | `string` | — |

#### 事件

| 事件 | 说明 |
| --- | --- |
| `oas-finish` | max-loops 播完定格时派发一次，`detail: { loops }` |

#### 插槽

| 名称 | 说明 |
| --- | --- |
| 默认 | 滚动内容 |

#### CSS 变量

| CSS 变量 | 默认值 |
| --- | --- |
| `--oas-marquee-duration` | `20s` |
| `--oas-marquee-fade-size` | `24px` |
| `--oas-marquee-shift` | `50%` |

- 系统开启「减少动态效果」（`prefers-reduced-motion: reduce`）时动画关闭、静态展示。
- 复制内容组带 `aria-hidden`，屏幕阅读器不重复朗读。
- 无其他事件，纯展示；仅 `max-loops` 播完派发一次 `oas-finish`。
