我已走完本仓库强制的两层 onboarding（L-core 4 份全文 + 命中触发的 L-ref），并在 `docs/_archive/_design-kickoffs/` 里挖到了**这条需求自己的已批准起手包**，所以下面这稿不是我凭 DS 推的，是照那份契约做的。

## 起手判定

**intent classified as 场景 2.2（已有页面加新 feature）· US-3**。依据：既有产品既有页 + 加第三个枚举项 + 加分隔线；不新增导航项、不动其他页 ⇒ 不是场景 4 重构。视觉真源二元定性：**新增/改动元素走 TVU DS**（用户显式 "用 TVU 设计系统" = M22 design ask，按 `code-conventions` §M22 > M21 优先级 override 掉 M21.2 的 sibling-probe 色值合同），**既有部分照 Keep 契约保留**。

**关键发现（改变了这稿的性质）**：`docs/_archive/_design-kickoffs/v4-1827-ndi-into-ipsource.md` 是这条需求 2026-07-21 已填好的 M49/M50 起手包 —— 产品是 **Config-T Local UI（RPS One 设备本地页，Figma `rJJjWWs51n2iFOlCIC7aYG`）**，radio 行 `7862:1729`，IP Source 页 `1011:7877`。所以四件要改的事、以及全部可见文案，都有**已批准的 Content Contract** 兜着，我逐字沿用，没有自创：`Type of source:` / `HDMI/SDI/USB` / `IP Source` / `NDI` / `Source Name` / `URL: ndi://` / `Apply` / `Merge All IP Source` / `Local input active — no configuration required.`。同时拿到两条产品实况：radio 的第 3 组**本来就有一个隐藏预留组** `7862:1739`（所以"新增"其实是启用它），以及 NDI 表单**刻意不带 per-Tx 开关**（选中 radio 本身即启用）。

## R0 Phase 0 · Element → Component mapping

| Element | Figma component_set / 产品 local 件 | Code path（本稿镜像来源） | 状态 |
|---|---|---|---|
| `Type of source:` 三选一 | `radio` `9f07746f…`（dark theme×status×enable, 8 变体） | `src/components/Radio/Radio.vue` 全套 CSS | ✅ 逐字镜像 |
| 顶部导航页签 / Tx1–4 分页 | `Tab/Item` `2267405386…` + `Tab List` `47efe04ff…` | `src/components/Tab/TabItem.vue`（line + filled） | ✅ 逐字镜像 |
| 选择行↔内容区分隔线 | **库内无 divider 组件**（catalog 2026-08-03 三关键词检索收口） | 1px + `--line-light`，照 catalog 登记的库范式 | ✅ 按范式 |
| 来源卡片（6 张） | 产品 file-local `IP Source Item` `1012:8880`（`Tag=No Source` / `Tag=Video Source`） | 无 code 侧；按 M21 复用 local 语义 + DS token 重建 shell | 🟡 几何未 probe |
| `Apply` / `Merge All IP Source` | `Button/dark M` `12b38c8b…`（产品 local `Button` `59:7772` Tag=First/Second 同位） | `src/components/Button/Button.vue` PALETTE_SPEC + SIZE_GEOMETRY | ✅ 逐字镜像 |
| `To:` 目标接收端下拉 | `select box/filled|line` `ca2ff89d…` | `src/components/Select/Select.vue`（line·M） | ✅ 逐字镜像 |
| NDI `Source Name` 输入 | `input box/filled|line` `1f513914…` | `src/components/Input/Input.vue`（line·M） | ✅ 逐字镜像 |
| NDI `URL: ndi://` 组合框 + 发现列表 | `select box/line` + `Drop down List/Select` `12602e73…` `Type=Radio` | `src/canonical/DropDownListSelect.vue` 面板 CSS | ✅ 镜像（宽度 1 处偏离，见下） |
| 表单行（label 120 + 控件） | `Form Item` `1923:50705`（`Layout=1 line`, `Label Width=120 px`） | `src/components/FormItem/FormItem.vue` | ✅ 逐字镜像 |
| 长源名截断提示 | `Tooltips` `40c9daaf…` | `src/components/Tooltip/Tooltip.vue`（dark·top） | ⚠️ 1 处刻意偏离（见下） |
| 下拉箭头图标 | `icon/Arrow/Dropdown` = registry `navigation/arrow-dropdown` | `src/icons/raw.ts:10` `IconArrowDropdown` **path 逐字复制** | ✅ 库资产 |

## R13 affordance trace（唯一新增的基本语义元素）

```
1. Affordance: directional-vertical (intent: 'NDI 发现列表组合框的下拉触发指示')
2. Vocabulary scan: [navigation/arrow-dropdown (registry, tags: chevron/expand), navigation/arrow-down,
   navigation/arrow-updown, navigation/more] —— 另查到 input/ndi · input/hdmi · input/sdi · others/usb ·
   action/refresh，均**不引入**（Simplicity Contract：契约未要求给 radio 加图标、rescan 属 out-of-scope）
3. Chosen: navigation/arrow-dropdown — DS Select.vue 自己用的就是它，几何一致，跨上下文复用
```

## R-DISCIPLINE checklist

TodoWrite 在本 session 不可用（量具停了写类工具），所以按规则要求物化成清单：**R0 ✅ / R2 ✅（零 hex 字面量）/ R3 ✅ / R7 ✅ / R9 ✅（hover·focus·键盘按 DS 各组件自带规格补）/ R13 ✅ / R14 ✅（全 flex/grid，两处 absolute 均为浮层且带注释）/ R15 ✅ / R16 ✅（产品文案走 `T` const map，零中英混排）/ R19 ✅（选中态全部来自 DS 组件本体，未自创 selected 样式）/ R23 ✅（长源名三件套：ellipsis + `scrollWidth>clientWidth` 检测 + 库 Tooltip 镜像 + `title`/`aria-describedby` 双保险）**。

## 三态怎么看

一屏四帧：**S0 交互原型**（radio 真可点、Tx1–4 切换带 per-Tx 记忆、NDI 发现下拉可展开）+ **S1/S2/S3 三张定态 spec 帧**（照 M16_1 帧名带状态字段）。分隔线、第三个 radio 项都在帧内；delta 标注全部放帧外（M-INTEGRITY §I4：装饰不进产品帧）。M4 只交 dark。

## 必须如实说的边界

1. **宿主几何未 probe（M17，最重要的一条）**：控件宽度、行高、按钮右缘对齐、帧宽（我用了 1280）全部是按 DS token 档位推的，**不是从 `1011:7877` 实测的**。落 Figma / 进产品前必须开那帧实测再对。
2. **顶部导航当前页签名是占位** `Video Source` —— 契约只登记了"不动的其他 5 个页"，没登记本页页签文案。已在 HTML 里标 `data-assumption`。
3. **两处新写文案**（契约未覆盖）：两个 placeholder `Enter source name` / `Select or type source address`，需 owner 拍。
4. **未跑机检、未在浏览器实跑**：`design:kickoff --check`、`audit:no-hardcoded-design-tokens`、`audit:consumer-code` 都需要 Bash，本 session 停用；代码是静态审读产物。⛔ 别当"已验证"。
5. **两处刻意偏离，都带注释**：发现列表面板宽度跟随字段（DS `.dropdown-select` 是写死 240px）；Tooltip 内容改 `white-space: normal`（DS `Tooltip.vue` 同时写了 `white-space:nowrap` + `max-width:320px`，超 320px 会溢出盒子而不是换行 —— 这条像是 DS 自己的缺陷，但 Figma 是真源、可能设计就是单行，请 owner 判一下再决定要不要立 entry）。
6. **radio 承担面板切换**：catalog 的 `radio` §Don't-use-for 写着"切换内容面板用 Tab/Item"。这里仍用 radio，因为它是**既有控件从 2 项扩到 3 项**（Keep 契约点名沿用其 selected/unselected 样式）、语义是 per-Tx 的表单取值而非导航；换成 Tab/Item 就是场景 4 重构，需要 owner 显式发起。

```html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Config-T · Type of source 三选一（+ NDI）· 增量设计 mockup</title>
<style>
/* ============================================================================
   1) TVU DESIGN SYSTEM TOKENS — dark (:root)
   值逐字取自 src/tokens/variables.css。单文件 + 禁 CDN + 只读会话 ⇒ 无法走
   vite.playground.config.ts 的 copyDesignTokensPlugin，此处为受迫内联；
   落进产品仓时应改为 <link> 生成的 tvu-tokens.css，真源只留一份。
   ========================================================================= */
:root{
  /* neutrals */
  --color-white:#ffffff;--color-grey-2:#f8f8f8;--color-grey-3:#f0f0f0;--color-grey-4:#dbdbdb;
  --color-grey-5:#cccccc;--color-grey-6:#9e9e9e;--color-grey-7:#7b7b7b;--color-grey-8:#595959;
  --color-grey-9:#434343;--color-grey-10:#353535;--color-grey-11:#262626;--color-grey-12:#1f1f1f;
  --color-grey-13:#141414;--color-grey-14:#000000;
  /* brand & status */
  --brand:#2fb54e;--brand-hover:#41c760;--brand-match:#0b2b13;--brand-disable:#1a652c;
  --red:#ea4233;--red-hover:#ed5b4e;--orange:#f68512;--blue:#3892f3;
  /* text */
  --text-heading:#ffffff;--text-body:#f8f8f8;--text-2:#cccccc;--text-tips:#9e9e9e;
  --text-placeholder:#7b7b7b;--text-disabled:#595959;--text-primary-btn:#f8f8f8;
  /* icon */
  --icon-active:#f8f8f8;--icon-default:#9e9e9e;--icon-disabled:#595959;--icon-placeholder:#7b7b7b;
  /* surface */
  --bg-layer1:#141414;--bg-layer2:#1f1f1f;--bg-layer3:#262626;--bg-layer4:#353535;
  --bg-topbar:#000000;--bg-grey-btn-hv:#595959;--bg-grey-btn-en:#434343;--bg-grey-btn-dis:#434343;
  /* line */
  --line-border:#595959;--line-deep:#353535;--line-light:#434343;
  /* input / select aliases */
  --input-line-border:var(--color-grey-8);--select-line-border:var(--color-grey-8);
  /* spacing */
  --sp-xxs:4px;--sp-xs:8px;--sp-s:12px;--sp-m:16px;--sp-l:24px;--sp-xl:32px;--sp-xxl:40px;--sp-xxxl:56px;
  /* radius */
  --r-xs:2px;--r-s:4px;--r-m:8px;--r-l:12px;--r-xl:16px;--r-xxl:20px;
  /* z-index */
  --z-tooltip:1000;--z-select-portal:1100;
  /* typography */
  --font-family-en:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,'Fira Sans','Droid Sans','Helvetica Neue',sans-serif;
  --font-family-zh:'PingFang SC','Microsoft YaHei',Helvetica,Arial,sans-serif;
  --font-family-base:var(--font-family-en),var(--font-family-zh);
  --text-style-tips:400 12px/16px var(--font-family-base);
  --text-style-body:400 14px/21px var(--font-family-base);
  --text-style-pop-title:600 16px/24px var(--font-family-base);
  --text-style-headline-2:600 18px/27px var(--font-family-base);
  --font-size-tips:12px;--font-size-body:14px;--line-height-body:21px;
  --font-weight-regular:400;--font-weight-semibold:600;
  /* effect styles */
  --shadow-l2:0 9px 28px 8px #0000000d,0 6px 16px 0 #00000014,0 3px 6px -4px #0000001f;
  --shadow-l3:0 12px 48px 16px #00000008,0 9px 28px 0 #0000000d,0 6px 16px -8px #00000014;
  /* component-variant (theme-stable) */
  --tooltip-bg-dark:var(--color-grey-10);--tooltip-border-dark:var(--color-grey-8);
  --tooltip-text-dark:var(--color-grey-2);--tooltip-max-width:320px;
  --radio-circle-size:16px;--radio-circle-border-width:1.2px;--radio-selected-dot-size:6px;
  --form-item-row-height:32px;--form-item-label-width-120:120px;
  --form-item-root-width-single-line:638px;--control-height-xxs:32px;
  /* derived aliases */
  --input-filled-bg:var(--bg-layer4);--green-bg:var(--brand-match);
  --line-divider-light:var(--line-light);--line-divider-deep:var(--line-deep);
  --radio-default-border:var(--text-tips);--radio-selected-dot:var(--bg-layer1);
  --control-default-border:var(--icon-placeholder);
  /* layout tokens */
  --bp-sm:640px;--bp-md:960px;--bp-lg:1100px;--bp-xl:1280px;
}

/* ============================================================================
   2) DS ELEMENT BASE LAYER — 取自 variables.css 末尾 element base layer
   ========================================================================= */
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0;padding:0}
body{font:var(--text-style-body);font-family:var(--font-family-base);background:var(--bg-layer2);color:var(--text-body)}
h1{font:var(--text-style-headline-2);color:var(--text-body);margin:0}
h2{font:var(--text-style-headline-2);color:var(--text-body);margin:0}
h3{font:var(--text-style-pop-title);color:var(--text-body);margin:0}
label{font:var(--text-style-body);color:var(--text-body)}
a{color:var(--brand)}
button{font:var(--text-style-body);color:inherit;border-radius:var(--r-s)}
input,select{font:var(--text-style-body);color:var(--text-body);background:var(--input-filled-bg);
  border:1px solid var(--line-border);border-radius:var(--r-s);padding:var(--sp-xs) var(--sp-s)}
input::placeholder{color:var(--text-placeholder)}

/* ============================================================================
   3) DS COMPONENT MIRRORS (R3/R15 — 手写 1:1 mirror canonical，非重新设计)
   ========================================================================= */

/* --- Button —— src/components/Button/Button.vue (SIZE_GEOMETRY.M + PALETTE_SPEC) --- */
.tvu-btn{
  --btn-bg:transparent;--btn-color:var(--text-2);--btn-border:transparent;--btn-opacity:1;
  --btn-height:32px;--btn-font-size:14px;--btn-line-height:21px;--btn-gap:0px;
  --btn-padding-inline:16px;--btn-border-width:1px;--btn-radius-square:4px;
  display:inline-flex;align-items:center;justify-content:center;gap:var(--btn-gap);min-width:0;
  padding-inline:var(--btn-padding-inline);height:var(--btn-height);
  border:var(--btn-border-width) solid var(--btn-border);background:var(--btn-bg);color:var(--btn-color);
  opacity:var(--btn-opacity);font-family:var(--font-family-base);font-size:var(--btn-font-size);
  font-weight:400;line-height:var(--btn-line-height);cursor:pointer;
  transition:background .15s,color .15s,border-color .15s,box-shadow .15s,opacity .15s;
  white-space:nowrap;outline:none;user-select:none;border-radius:var(--btn-radius-square)
}
.tvu-btn:hover:not(:disabled){background:var(--btn-bg-hover);color:var(--btn-color-hover);border-color:var(--btn-border-hover)}
.tvu-btn:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
.tvu-btn:disabled{cursor:not-allowed}
/* filling / green —— 主确认动作 */
.tvu-btn--fill-green{--btn-bg:var(--brand);--btn-color:var(--text-primary-btn);--btn-border:transparent;
  --btn-bg-hover:var(--brand-hover);--btn-color-hover:var(--text-primary-btn);--btn-border-hover:transparent}
/* ghost / gray 1 —— 次级动作（borderWidth 1.2px 同 Button.vue computed） */
.tvu-btn--ghost-gray{--btn-bg:transparent;--btn-color:var(--text-2);--btn-border:var(--bg-grey-btn-en);
  --btn-bg-hover:var(--bg-grey-btn-en);--btn-color-hover:var(--text-2);--btn-border-hover:var(--bg-grey-btn-en);
  --btn-border-width:1.2px}

/* --- Radio —— src/components/Radio/Radio.vue（含 Figma-first 显式状态映射） --- */
.tvu-radio{display:inline-flex;align-items:center;gap:var(--sp-xs);height:21px;cursor:pointer;
  font:var(--text-style-body);color:var(--text-body);user-select:none}
.tvu-radio:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:var(--r-xs)}
.tvu-radio__circle{flex-shrink:0;width:var(--radio-circle-size);height:var(--radio-circle-size);
  border-radius:50%;border:var(--radio-circle-border-width) solid var(--radio-default-border);
  background:transparent;display:flex;align-items:center;justify-content:center;
  transition:border-color .15s,background .15s,box-shadow .15s}
.tvu-radio__dot{width:var(--radio-selected-dot-size);height:var(--radio-selected-dot-size);
  border-radius:50%;background:var(--radio-selected-dot)}
.tvu-radio[data-figma-status="off"][data-figma-enable="yes"] .tvu-radio__circle{
  background:transparent;border-color:var(--radio-default-border);box-shadow:none}
.tvu-radio[data-figma-status="on"][data-figma-enable="yes"] .tvu-radio__circle{
  background:var(--brand);border-color:var(--brand);box-shadow:none}

/* --- Tab/Item —— src/components/Tab/TabItem.vue (line + filled, active-green) --- */
.tvu-tab-item{display:inline-flex;align-items:center;gap:var(--sp-xs);height:32px;padding:0 var(--sp-m);
  font:var(--text-style-body);color:var(--text-tips);cursor:pointer;white-space:nowrap;user-select:none;
  transition:color .15s,background .15s,border-color .15s;background:none;border:0}
.tvu-tab-item:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
.tvu-tab-item--line{border-bottom:1px solid transparent;margin-bottom:-1px}
.tvu-tab-item--line.is-active-green{color:var(--brand);border-bottom-color:var(--brand);font-weight:700}
.tvu-tab-item--filled{border-radius:var(--r-s)}
.tvu-tab-item--filled.is-normal{background:var(--bg-layer3);color:var(--text-2)}
.tvu-tab-item--filled.is-active-green{background:var(--brand);color:var(--text-primary-btn)}

/* --- Input（line · M） —— src/components/Input/Input.vue --- */
.tvu-input{display:block;width:100%;padding:var(--sp-xs) var(--sp-s);background:transparent;
  border:1px solid var(--input-line-border);border-radius:var(--r-s);color:var(--text-body);
  font-size:14px;font-family:var(--font-family-base);outline:none;transition:border-color .15s}
.tvu-input::placeholder{color:var(--text-placeholder)}
.tvu-input:hover:not(:disabled),.tvu-input:focus:not(:disabled){border-color:var(--brand)}
.tvu-input--m{height:32px}

/* --- Select（line · M） —— src/components/Select/Select.vue
       注：line 边框用 --line-light、而 Input line 用 --input-line-border(grey-8)，
       这个差异是 DS 两个源文件的现状，逐字沿用、非本稿选择。 --- */
.tvu-select-wrap{position:relative;display:inline-block;width:100%}
.tvu-select{display:block;width:100%;background:transparent;border:1px solid var(--line-light);
  border-radius:var(--r-s);color:var(--text-body);font-size:13px;font-family:var(--font-family-base);
  outline:none;appearance:none;-webkit-appearance:none;cursor:pointer;height:32px;
  padding:0 36px 0 var(--sp-s);transition:border-color .15s}
.tvu-select:hover,.tvu-select:focus{border-color:var(--brand)}
.tvu-select option{background:var(--bg-layer2);color:var(--text-body)}
/* overlay: arrow pinned inside the field, no layout semantics（R14 例外，已注明） */
.tvu-select-arrow{position:absolute;right:12px;top:50%;transform:translateY(-50%);pointer-events:none;
  color:var(--text-tips);display:flex;align-items:center}
.tvu-select-arrow svg{display:block}

/* --- Drop down List/Select —— src/canonical/DropDownListSelect.vue
       偏离 1 处：DS 面板写死 width:240px，本稿改为跟随触发字段宽（组合框惯例）。 --- */
.tvu-dropdown-select{padding:var(--sp-xs) 0;border-radius:var(--r-s);background:var(--bg-layer4);
  box-shadow:var(--shadow-l2);font-family:var(--font-family-base);min-width:240px}
.tvu-dropdown-item{display:flex;align-items:center;gap:var(--sp-xs);min-height:32px;
  padding:0 var(--sp-xs) 0 var(--sp-s);color:var(--text-body);cursor:pointer}
.tvu-dropdown-item:hover{background:var(--green-bg)}
.tvu-dropdown-item.is-checked{color:var(--brand)}
.tvu-dropdown-item__label{font-size:14px;line-height:21px}

/* --- Tooltip（dark · top） —— src/components/Tooltip/Tooltip.vue
       偏离 1 处：DS 同时写 white-space:nowrap + max-width:320px，超长会溢出盒子；
       本组件在此处的唯一职责就是显示被截断的长源名 ⇒ 改 normal 换行。 --- */
.tvu-tooltip-wrap{position:relative;display:inline-flex;isolation:isolate;min-width:0}
.tvu-tooltip-box{position:absolute;z-index:var(--z-tooltip);display:none;white-space:normal;
  max-width:var(--tooltip-max-width);padding:var(--sp-xxs) var(--sp-xs);border-radius:var(--r-s);
  background:var(--tooltip-bg-dark);border:1px solid var(--tooltip-border-dark);
  color:var(--tooltip-text-dark);font:var(--text-style-tips);pointer-events:none;box-shadow:var(--shadow-l3)}
.tvu-tooltip-box--top{bottom:calc(100% + 6px);left:0}
.tvu-tooltip-wrap[data-truncated="true"]:hover>.tvu-tooltip-box,
.tvu-tooltip-wrap[data-truncated="true"]:focus-within>.tvu-tooltip-box{display:block}
.tvu-tooltip-arrow{position:absolute;width:8.5px;height:8.5px;background:var(--tooltip-bg-dark);
  transform:rotate(45deg);bottom:-5px;left:var(--sp-s);
  border-right:1px solid var(--tooltip-border-dark);border-bottom:1px solid var(--tooltip-border-dark)}

/* --- Form Item（Layout=1 line · Label Width=120 px · Theme=Dark） --- */
.tvu-form-item{color:var(--text-body);--fi-label:var(--text-tips);
  width:min(100%,var(--form-item-root-width-single-line))}
.tvu-form-item__main{display:flex;gap:var(--sp-xs);align-items:flex-start}
.tvu-form-item__label{display:inline-flex;align-items:center;gap:var(--sp-xxs);min-height:var(--form-item-row-height);
  width:var(--form-item-label-width-120);padding:var(--sp-xxs) 0;font:var(--text-style-body);
  color:var(--fi-label);flex-shrink:0}
.tvu-form-item__content{display:flex;flex:1 1 auto;min-width:0;flex-direction:column;gap:var(--sp-xxs)}

/* --- R23 截断三件套之 (a) --- */
.cell--ellipsis{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;display:block}

/* ============================================================================
   4) PRODUCT LAYER — Config-T Local UI 页面装配（token-only，全 flex/grid）
      ⚠️ 几何取值来自 DS token 档位，宿主帧 1011:7877 未 probe（见交付说明）
   ========================================================================= */
.cfg-frame{width:var(--bp-xl);max-width:100%;background:var(--bg-layer1);border:1px solid var(--line-deep);
  border-radius:var(--r-m);overflow:hidden}
.cfg-frame--inert{pointer-events:none}
.cfg-topbar{height:var(--sp-xxxl);background:var(--bg-topbar);display:flex;align-items:center;
  gap:var(--sp-l);padding:0 var(--sp-l)}
.cfg-topbar__brand{font:var(--text-style-pop-title);color:var(--text-heading);white-space:nowrap}
.cfg-nav{display:flex;align-items:center;gap:var(--sp-xxs);overflow-x:auto}
.cfg-header{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-m);
  padding:var(--sp-m) var(--sp-l) 0}
.cfg-tx{display:flex;align-items:center;gap:var(--sp-xxs)}
.cfg-tx__hint{font:var(--text-style-tips);color:var(--text-tips);margin-right:var(--sp-xs)}
.cfg-main{padding:var(--sp-m) var(--sp-l) var(--sp-l)}
.cfg-typerow{display:flex;align-items:center;gap:var(--sp-l);flex-wrap:wrap;min-height:var(--control-height-xxs)}
.cfg-typerow__label{font:var(--text-style-body);color:var(--text-tips);white-space:nowrap}
.cfg-typerow__opts{display:flex;align-items:center;gap:var(--sp-xl);flex-wrap:wrap}
/* NEW · 选择行与内容区之间的视觉分隔（库内无 divider 组件 ⇒ 1px + Line token） */
.cfg-divider{height:1px;background:var(--line-light);margin:var(--sp-m) 0 0}
.cfg-body{padding-top:var(--sp-m);min-height:340px}

/* 内容区 A · HDMI/SDI/USB —— 静态被动提示（无控件、无动作） */
.cfg-hint{min-height:300px;display:flex;align-items:center;justify-content:center;text-align:center;
  font:var(--text-style-body);color:var(--text-tips);padding:var(--sp-l)}

/* 内容区 B · IP Source —— 既有 6 卡网格（baseline，本期不改内部） */
.src-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--sp-m)}
.src-card{background:var(--bg-layer3);border-radius:var(--r-m);padding:var(--sp-s);
  display:flex;flex-direction:column;gap:var(--sp-xs);min-width:0}
.src-card__pv{aspect-ratio:16/9;border-radius:var(--r-s);background:var(--bg-layer1);
  display:flex;align-items:center;justify-content:center;font:var(--text-style-tips);color:var(--text-placeholder)}
.src-card--has-source .src-card__pv{
  background:linear-gradient(135deg,var(--bg-layer4) 0%,var(--bg-layer2) 60%,var(--bg-layer1) 100%)}
.src-card__ip{font:var(--text-style-body);color:var(--text-body);font-variant-numeric:tabular-nums}
.src-card--no-source .src-card__ip{color:var(--text-placeholder)}
.src-card__row{display:flex;align-items:center;gap:var(--sp-xs);min-width:0}
.src-card__row-label{font:var(--text-style-body);color:var(--text-tips);white-space:nowrap}
.src-card__act{display:flex;justify-content:flex-end;margin-top:var(--sp-xxs)}
.src-foot{display:flex;justify-content:flex-end;margin-top:var(--sp-m)}

/* 内容区 C · NDI —— 新增表单（无 per-Tx 开关：选中 radio 即启用） */
.ndi-form{display:flex;flex-direction:column;gap:var(--sp-m)}
.ndi-form__act{display:flex;gap:var(--sp-xs);
  padding-left:calc(var(--form-item-label-width-120) + var(--sp-xs));
  width:min(100%,var(--form-item-root-width-single-line))}
.cbx{position:relative;width:100%}
.cbx__field{display:flex;align-items:center;width:100%}
/* overlay: combobox 发现列表浮层，anchored 到字段，无 layout 语义（R14 例外，已注明） */
.cbx__panel{position:absolute;top:calc(100% + var(--sp-xxs));left:0;width:100%;
  z-index:var(--z-select-portal);display:none}
.cbx.is-open .cbx__panel{display:block}
.cbx__toggle{position:absolute;right:0;top:0;height:32px;width:36px;display:flex;align-items:center;
  justify-content:center;background:none;border:0;color:var(--text-tips);cursor:pointer;padding:0}
.cbx__toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
.cbx__toggle svg{display:block}
.cbx .tvu-input{padding-right:36px}

/* ============================================================================
   5) ANNOTATION LAYER — 画布 / 帧标签 / 交付说明（不进产品帧，M-INTEGRITY §I4）
   ========================================================================= */
.doc{max-width:1360px;margin:0 auto;padding:var(--sp-l) var(--sp-m) var(--sp-xxl);
  display:flex;flex-direction:column;gap:var(--sp-xl)}
.doc__head{display:flex;flex-direction:column;gap:var(--sp-xs);border-left:var(--sp-xxs) solid var(--brand);
  padding-left:var(--sp-s)}
.doc__meta{font:var(--text-style-tips);color:var(--text-tips)}
.legend{display:flex;gap:var(--sp-s);flex-wrap:wrap;margin-top:var(--sp-xxs)}
.chip{display:inline-flex;align-items:center;gap:var(--sp-xxs);height:24px;padding:0 var(--sp-xs);
  border-radius:var(--r-s);font:var(--text-style-tips);border:1px solid var(--line-light);color:var(--text-2)}
.chip--add{border-color:var(--brand);color:var(--brand)}
.chip--change{border-color:var(--blue);color:var(--blue)}
.block{display:flex;flex-direction:column;gap:var(--sp-xs)}
.block__cap{font:var(--text-style-pop-title);color:var(--text-body)}
.block__notes{margin:0;padding-left:var(--sp-m);font:var(--text-style-tips);color:var(--text-tips)}
.block__notes li{margin:2px 0}
.doc__foot{border-top:1px solid var(--line-deep);padding-top:var(--sp-m);
  font:var(--text-style-tips);color:var(--text-tips);display:flex;flex-direction:column;gap:var(--sp-xs)}
.doc__foot h3{font:var(--text-style-body);color:var(--text-2)}
.doc__foot ol,.doc__foot ul{margin:0;padding-left:var(--sp-m)}
.doc__foot code{font-family:monospace;font-size:var(--font-size-tips);color:var(--text-2)}

@media (max-width:960px){
  .src-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
  .cfg-frame{width:100%}
}
@media (max-width:640px){
  .src-grid{grid-template-columns:minmax(0,1fr)}
  .cfg-header{flex-direction:column;align-items:flex-start}
  .ndi-form__act{padding-left:0}
  .tvu-form-item__main{flex-direction:column;gap:0}
  .tvu-form-item__label{width:auto}
}
</style>
</head>
<body>
<main class="doc">

  <header class="doc__head">
    <h1>Config-T Local UI · <span lang="en">Type of source</span> 二选一 → 三选一（新增 <span lang="en">NDI</span>）</h1>
    <p class="doc__meta">
      US-3 / 场景 2.2 增量设计 · 基线 = 已批准起手包 <code>v4-1827-ndi-into-ipsource</code>
      （Figma <code>rJJjWWs51n2iFOlCIC7aYG</code>，radio 行 <code>7862:1729</code>，IP Source 页 <code>1011:7877</code>）
      · dark only（domain-tvu M4）· 组件与 token 全部镜像 TVU DS
    </p>
    <div class="legend">
      <span class="chip">既有 Keep</span>
      <span class="chip chip--add">本期新增 Add</span>
      <span class="chip chip--change">本期修改 Change</span>
      <span class="chip">标注只在帧外，产品帧内无装饰</span>
    </div>
  </header>

  <!-- ── S0 交互原型 ─────────────────────────────────────────────────────── -->
  <section class="block">
    <div class="block__cap">S0 · 交互原型（可点）</div>
    <ul class="block__notes">
      <li>点三个 radio 切换下方内容区；点 Tx1–4 切换传输通道，每个 Tx 各自记住自己的来源类型（per-Tx memory）。</li>
      <li>NDI 的 <span lang="en">URL: ndi://</span> 是组合框：点箭头展开发现列表 = Discovery 模式，直接键入 = Manual 模式。</li>
      <li>发现列表里最后一条是超长源名，hover / focus 看截断 + tooltip（R23 三件套）。</li>
    </ul>
    <div id="frame-live"></div>
  </section>

  <!-- ── S1 / S2 / S3 定态 spec 帧 ───────────────────────────────────────── -->
  <section class="block">
    <div class="block__cap">S1 · <span lang="en">HDMI/SDI/USB</span> 选中 — 本地输入（默认态）</div>
    <ul class="block__notes">
      <li>内容区为静态被动提示，无控件、无动作 —— 只为避免空白区被读成“页面坏了”。</li>
    </ul>
    <div id="frame-s1"></div>
  </section>

  <section class="block">
    <div class="block__cap">S2 · <span lang="en">IP Source</span> 选中 — 既有 6 卡网格（baseline 原样保留）</div>
    <ul class="block__notes">
      <li>卡片内部（预览 / IP / <span lang="en">To:</span> 目标接收端 / <span lang="en">Apply</span>）与
        <span lang="en">Merge All IP Source</span> 本期不改。</li>
      <li>第 6 张卡是产品既有组件 <code>IP Source Item</code> 的 <code>Tag=No Source</code> 变体。</li>
    </ul>
    <div id="frame-s2"></div>
  </section>

  <section class="block">
    <div class="block__cap">S3 · <span lang="en">NDI</span> 选中 — 新增 NDI 表单（本期 delta）</div>
    <ul class="block__notes">
      <li>字段集 = <span lang="en">Source Name</span> + <span lang="en">URL: ndi://</span> 组合框 +
        <span lang="en">Apply</span>；<strong>刻意不带 per-Tx 启用开关</strong>：选中 radio 本身就等于“该 Tx 用 NDI”。</li>
      <li>NDI 不是导航项 —— 顶部导航页签数与既有一致，没有新增页签。</li>
    </ul>
    <div id="frame-s3"></div>
  </section>

  <footer class="doc__foot">
    <h3>Known deviations / Open scope items（handoff 追踪段，非契约段）</h3>
    <ol>
      <li><strong>宿主几何未 probe（M17，最高优先）</strong>：控件宽度 / 行高 / 按钮右缘对齐 / 帧宽（此稿 1280）
        取自 DS token 档位，<strong>不是</strong>从 <code>1011:7877</code> 实测。落 Figma 或进产品前必须开那帧实测比对。</li>
      <li>顶部导航<strong>当前页签文案是占位</strong> <span lang="en">Video Source</span>（已在 DOM 标
        <code>data-assumption</code>）—— 契约只登记了“不动的其他页”，未登记本页页签名。</li>
      <li>两处<strong>新写文案</strong>待 owner 拍：<span lang="en">Enter source name</span> /
        <span lang="en">Select or type source address</span>（Content Contract 未覆盖 placeholder）。</li>
      <li>刻意偏离 DS 两处（代码内已注明）：发现列表面板宽度跟随字段（DS 写死 240px）；tooltip 内容改
        <code>white-space:normal</code>（DS 同时有 <code>nowrap</code> + <code>max-width:320px</code>，超长会溢出盒子）。</li>
      <li>out-of-scope（沿用已批准 spec §6 backlog，本稿刻意不画）：source-empty / scanning / discovery-failed /
        connecting / Apply 成功·失败 / 超长源名校验 / 4-Tx 绑定总览。</li>
      <li>未跑机检、未在浏览器实跑（本会话 Bash / 写文件停用）：<code>design:kickoff --check</code>、
        <code>audit:no-hardcoded-design-tokens</code>、<code>audit:consumer-code</code> 均待补跑。</li>
    </ol>
    <p><span lang="en">UI primitives used:</span> Radio · Tab/Item(line·filled) · Button/dark M(filling green · ghost gray 1) ·
      Input(line·M) · Select(line·M) · Drop down List/Select · Tooltip(dark) · Form Item(1 line·120px) ·
      icon <code>navigation/arrow-dropdown</code>。</p>
  </footer>

</main>

<script>
(function () {
  'use strict';

  /* R16 — 产品 UI 字符串走 const map，禁 inline 双语；文案逐字取自已批准 Content Contract。
     标 (new) 者为本期新写、待 owner 拍。 */
  var T = {
    brand: 'RPS One',
    navEncoder: 'Encoder',
    navVideoSource: 'Video Source',          /* (assumption) 当前页签名未 probe */
    navConfigLive: 'Config for live',
    navMediaMind: 'MediaMind',
    navReturnVideo: 'Return Video',
    navVoip: 'VoIP·IFB',
    pageTitle: 'Video Source',               /* (assumption) 同上 */
    txHint: 'Transmitter',
    typeOfSource: 'Type of source:',
    optLocal: 'HDMI/SDI/USB',
    optIp: 'IP Source',
    optNdi: 'NDI',
    localHint: 'Local input active — no configuration required.',
    noSource: 'No Source',
    to: 'To:',
    apply: 'Apply',
    mergeAll: 'Merge All IP Source',
    sourceName: 'Source Name',
    ndiUrl: 'URL: ndi://',
    phName: 'Enter source name',             /* (new) */
    phUrl: 'Select or type source address'   /* (new) */
  };

  var TYPES = [
    { id: 'local', label: T.optLocal },
    { id: 'ip', label: T.optIp },
    { id: 'ndi', label: T.optNdi }            /* 启用既有隐藏预留组 7862:1739 */
  ];
  var TXS = ['Tx1', 'Tx2', 'Tx3', 'Tx4'];
  var NAV = [T.navEncoder, T.navVideoSource, T.navConfigLive, T.navMediaMind, T.navReturnVideo, T.navVoip];

  var CARDS = [
    { ip: '192.168.1.24:5000', to: 'Transmitter 1', hasSource: true },
    { ip: '192.168.1.31:5004', to: 'Transmitter 2', hasSource: true },
    { ip: '10.10.4.87:6000', to: 'Transmitter 3', hasSource: true },
    { ip: '10.10.4.90:6002', to: 'Transmitter 4', hasSource: true },
    { ip: '172.16.8.5:5010', to: 'Transmitter 1', hasSource: true },
    { ip: '—', to: 'Transmitter 2', hasSource: false }
  ];
  var TX_OPTS = ['Transmitter 1', 'Transmitter 2', 'Transmitter 3', 'Transmitter 4'];

  var NDI_SOURCES = [
    'STUDIO-A (CAM 1)',
    'STUDIO-A (CAM 2)',
    'TVU-PACK-01 (Program)',
    'OB-VAN-NORTH (CAM 7 — Backup Program Feed, Gate 3, East Stand)'
  ];

  /* icon/Arrow/Dropdown = registry navigation/arrow-dropdown；path 逐字复制 src/icons/raw.ts:10 */
  var ICON_ARROW_DROPDOWN =
    '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">' +
    '<path d="M3.05146 6.20128C3.27161 5.95374 3.65086 5.93145 3.89853 6.15148L7.86707 9.67727C7.94284 9.74458 8.05698 9.74458 8.13274 9.67727L12.1013 6.15148C12.349 5.93145 12.7282 5.95374 12.9484 6.20128C13.1685 6.44883 13.1462 6.82788 12.8985 7.04791L8.39841 11.046C8.17112 11.2479 7.8287 11.2479 7.60141 11.046L3.10129 7.04791C2.85362 6.82788 2.83131 6.44883 3.05146 6.20128Z" fill="currentColor"/></svg>';

  var uid = 0;
  function nid(p) { uid += 1; return p + '-' + uid; }
  function esc(s) {
    return String(s).replace(/[&<>"']/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
    });
  }
  function tabi(interactive) { return interactive ? '0' : '-1'; }

  /* ---------- 顶部导航（不新增页签：NDI 不是导航项） ---------- */
  function navHTML(interactive) {
    return NAV.map(function (label) {
      var active = label === T.navVideoSource;
      return '<button type="button" role="tab" aria-selected="' + active + '" tabindex="' + tabi(interactive) +
        '" class="tvu-tab-item tvu-tab-item--line ' + (active ? 'is-active-green' : 'is-normal') + '"' +
        (active ? ' data-assumption="page-tab-label-not-probed"' : '') +
        ' data-figma-component="TabItem" data-figma-type="Line"' +
        (active ? ' data-figma-property1="Active" data-figma-property2="Green"' : ' data-figma-property1="Normal"') +
        '>' + esc(label) + '</button>';
    }).join('');
  }

  /* ---------- Tx1–4 分页（既有，右上角） ---------- */
  function txHTML(activeTx, interactive) {
    return TXS.map(function (tx) {
      var on = tx === activeTx;
      return '<button type="button" role="tab" aria-selected="' + on + '" tabindex="' + tabi(interactive) +
        '" class="tvu-tab-item tvu-tab-item--filled ' + (on ? 'is-active-green' : 'is-normal') +
        '" data-act="pick-tx" data-tx="' + tx + '"' +
        ' data-figma-component="TabItem" data-figma-type="Filled" data-figma-property2="Green"' +
        ' data-figma-property1="' + (on ? 'Active' : 'Normal') + '">' + esc(tx) + '</button>';
    }).join('');
  }

  /* ---------- Type of source 三选一 ---------- */
  function radiosHTML(selected, interactive) {
    return TYPES.map(function (t) {
      var on = t.id === selected;
      return '<label class="tvu-radio" role="radio" aria-checked="' + on + '" tabindex="' + tabi(interactive) +
        '" data-act="pick-type" data-type="' + t.id + '"' +
        ' data-figma-component="radio" data-figma-dark-theme="on" data-figma-enable="yes"' +
        ' data-figma-status="' + (on ? 'on' : 'off') + '">' +
        '<span class="tvu-radio__circle">' + (on ? '<span class="tvu-radio__dot"></span>' : '') + '</span>' +
        '<span>' + esc(t.label) + '</span></label>';
    }).join('');
  }

  /* ---------- 内容区 A · 本地输入 ---------- */
  function panelLocal() {
    return '<div class="cfg-hint" lang="en">' + esc(T.localHint) + '</div>';
  }

  /* ---------- 内容区 B · IP Source 6 卡网格（baseline） ---------- */
  function selectHTML(value, options, interactive, aria) {
    var opts = options.map(function (o) {
      return '<option value="' + esc(o) + '"' + (o === value ? ' selected' : '') + '>' + esc(o) + '</option>';
    }).join('');
    return '<span class="tvu-select-wrap">' +
      '<select class="tvu-select" tabindex="' + tabi(interactive) + '" aria-label="' + esc(aria) + '"' +
      ' data-figma-component="SelectBoxLine" data-figma-size="M" data-figma-status="Value">' + opts + '</select>' +
      '<span class="tvu-select-arrow">' + ICON_ARROW_DROPDOWN + '</span></span>';
  }

  function panelIp(interactive) {
    var cards = CARDS.map(function (c, i) {
      return '<article class="src-card ' + (c.hasSource ? 'src-card--has-source' : 'src-card--no-source') +
        '" data-figma-component="IP Source Item" data-figma-tag="' +
        (c.hasSource ? 'Video Source' : 'No Source') + '">' +
        '<div class="src-card__pv">' + (c.hasSource ? '' : '<span lang="en">' + esc(T.noSource) + '</span>') + '</div>' +
        '<div class="src-card__ip" lang="en">' + esc(c.ip) + '</div>' +
        '<div class="src-card__row"><span class="src-card__row-label" lang="en">' + esc(T.to) + '</span>' +
        selectHTML(c.to, TX_OPTS, interactive, T.to + ' ' + T.txHint + ' ' + (i + 1)) + '</div>' +
        '<div class="src-card__act"><button type="button" class="tvu-btn tvu-btn--fill-green" tabindex="' +
        tabi(interactive) + '" data-figma-component="Button" data-figma-size="M" data-figma-style="filling"' +
        ' data-figma-color="green" lang="en">' + esc(T.apply) + '</button></div>' +
        '</article>';
    }).join('');
    return '<div class="src-grid">' + cards + '</div>' +
      '<div class="src-foot"><button type="button" class="tvu-btn tvu-btn--ghost-gray" tabindex="' +
      tabi(interactive) + '" data-figma-component="Button" data-figma-size="M" data-figma-style="ghost"' +
      ' data-figma-color="gray 1" lang="en">' + esc(T.mergeAll) + '</button></div>';
  }

  /* ---------- 内容区 C · NDI 表单（新增） ---------- */
  function formRow(label, controlHTML) {
    return '<div class="tvu-form-item" data-figma-component="FormItem" data-figma-layout="1 line"' +
      ' data-figma-label-width="120 px" data-figma-theme="Dark">' +
      '<div class="tvu-form-item__main">' +
      '<label class="tvu-form-item__label" lang="en">' + esc(label) + '</label>' +
      '<div class="tvu-form-item__content">' + controlHTML + '</div></div></div>';
  }

  function panelNdi(state, interactive) {
    var listId = nid('ndi-list');
    var items = NDI_SOURCES.map(function (s) {
      var tipId = nid('tip');
      var checked = s === state.ndiUrl;
      return '<div class="tvu-dropdown-item' + (checked ? ' is-checked' : '') + '" role="option"' +
        ' aria-selected="' + checked + '" data-act="pick-ndi" data-value="' + esc(s) + '">' +
        '<span class="tvu-tooltip-wrap" data-tip>' +
        '<span class="tvu-dropdown-item__label cell--ellipsis" aria-describedby="' + tipId + '" lang="en">' +
        esc(s) + '</span>' +
        '<span class="tvu-tooltip-box tvu-tooltip-box--top" role="tooltip" id="' + tipId + '" lang="en">' +
        esc(s) + '<span class="tvu-tooltip-arrow"></span></span>' +
        '</span></div>';
    }).join('');

    var nameCtrl = '<input class="tvu-input tvu-input--m" type="text" lang="en" tabindex="' + tabi(interactive) +
      '" data-field="ndiName" value="' + esc(state.ndiName || '') + '" placeholder="' + esc(T.phName) +
      '" aria-label="' + esc(T.sourceName) + '"' +
      ' data-figma-component="InputBoxLine" data-figma-size="M">';

    var urlCtrl = '<div class="cbx" data-cbx>' +
      '<div class="cbx__field">' +
      '<input class="tvu-input tvu-input--m" type="text" lang="en" role="combobox" aria-expanded="false"' +
      ' aria-controls="' + listId + '" aria-autocomplete="list" tabindex="' + tabi(interactive) + '"' +
      ' data-field="ndiUrl" value="' + esc(state.ndiUrl || '') + '" placeholder="' + esc(T.phUrl) +
      '" aria-label="' + esc(T.ndiUrl) + '" data-figma-component="SelectBoxLine" data-figma-ux="editable">' +
      '<button type="button" class="cbx__toggle" data-act="cbx-toggle" tabindex="' + tabi(interactive) +
      '" aria-label="Show discovered sources">' + ICON_ARROW_DROPDOWN + '</button>' +
      '</div>' +
      '<div class="cbx__panel"><div class="tvu-dropdown-select" role="listbox" id="' + listId + '"' +
      ' data-figma-component="DropDownListSelect" data-figma-type="Radio" data-figma-dark-theme="on">' +
      items + '</div></div></div>';

    return '<div class="ndi-form">' +
      formRow(T.sourceName, nameCtrl) +
      formRow(T.ndiUrl, urlCtrl) +
      '<div class="ndi-form__act"><button type="button" class="tvu-btn tvu-btn--fill-green" tabindex="' +
      tabi(interactive) + '" data-figma-component="Button" data-figma-size="M" data-figma-style="filling"' +
      ' data-figma-color="green" lang="en">' + esc(T.apply) + '</button></div></div>';
  }

  function panelHTML(type, state, interactive) {
    if (type === 'local') return panelLocal();
    if (type === 'ip') return panelIp(interactive);
    return panelNdi(state, interactive);
  }

  /* ---------- 整帧 ---------- */
  function frameHTML(state, interactive) {
    return '<div class="cfg-frame' + (interactive ? '' : ' cfg-frame--inert') + '" lang="en">' +
      '<div class="cfg-topbar"><span class="cfg-topbar__brand">' + esc(T.brand) + '</span>' +
      /* 顶部导航：不新增页签（NDI 不是导航项） */
      '<nav class="cfg-nav" role="tablist" aria-label="Config-T sections">' + navHTML(interactive) + '</nav></div>' +
      '<div class="cfg-header"><h2>' + esc(T.pageTitle) + '</h2>' +
      '<div class="cfg-tx" role="tablist" aria-label="Transmitter"><span class="cfg-tx__hint">' +
      esc(T.txHint) + '</span>' + txHTML(state.tx, interactive) + '</div></div>' +
      '<div class="cfg-main">' +
      '<div class="cfg-typerow"><span class="cfg-typerow__label">' + esc(T.typeOfSource) + '</span>' +
      '<div class="cfg-typerow__opts" role="radiogroup" aria-label="' + esc(T.typeOfSource) + '">' +
      radiosHTML(state.byTx[state.tx], interactive) + '</div></div>' +
      '<div class="cfg-divider" role="separator"></div>' +
      '<div class="cfg-body">' + panelHTML(state.byTx[state.tx], state, interactive) + '</div>' +
      '</div></div>';
  }

  /* ---------- R23 (b) 截断检测：只在真截断时才让 tooltip 生效 ---------- */
  function measureTruncation(scope) {
    var wraps = scope.querySelectorAll('[data-tip]');
    for (var i = 0; i < wraps.length; i++) {
      var label = wraps[i].querySelector('.cell--ellipsis');
      if (!label) continue;
      var truncated = label.scrollWidth > label.clientWidth;
      if (truncated) {
        wraps[i].setAttribute('data-truncated', 'true');
        label.setAttribute('title', label.textContent); /* no-JS / 触屏 fallback */
      } else {
        wraps[i].removeAttribute('data-truncated');
        label.removeAttribute('title');
      }
    }
  }

  /* ---------- 定态 spec 帧 ---------- */
  function mountStatic(id, type) {
    var state = {
      tx: 'Tx1',
      byTx: { Tx1: type, Tx2: type, Tx3: type, Tx4: type },
      ndiName: type === 'ndi' ? 'Studio A — Camera 1' : '',
      ndiUrl: type === 'ndi' ? 'STUDIO-A (CAM 1)' : ''
    };
    var host = document.getElementById(id);
    if (host) host.innerHTML = frameHTML(state, false);
  }

  /* ---------- 交互原型 ---------- */
  var live = {
    tx: 'Tx1',
    byTx: { Tx1: 'ip', Tx2: 'ndi', Tx3: 'local', Tx4: 'ip' }, /* per-Tx memory */
    ndiName: '',
    ndiUrl: ''
  };
  var liveHost = document.getElementById('frame-live');

  function repaintLive() {
    liveHost.innerHTML = frameHTML(live, true);
  }

  function closeAllCbx(scope) {
    var boxes = scope.querySelectorAll('.cbx.is-open');
    for (var i = 0; i < boxes.length; i++) {
      boxes[i].classList.remove('is-open');
      var input = boxes[i].querySelector('[role="combobox"]');
      if (input) input.setAttribute('aria-expanded', 'false');
    }
  }

  if (liveHost) {
    repaintLive();

    liveHost.addEventListener('click', function (e) {
      var el = e.target.closest ? e.target.closest('[data-act]') : null;

      if (el && el.getAttribute('data-act') === 'pick-type') {
        e.preventDefault();
        live.byTx[live.tx] = el.getAttribute('data-type');
        repaintLive();
        return;
      }
      if (el && el.getAttribute('data-act') === 'pick-tx') {
        live.tx = el.getAttribute('data-tx');
        repaintLive();
        return;
      }
      if (el && el.getAttribute('data-act') === 'cbx-toggle') {
        var cbx = el.closest('[data-cbx]');
        var wasOpen = cbx.classList.contains('is-open');
        closeAllCbx(liveHost);
        if (!wasOpen) {
          cbx.classList.add('is-open');
          cbx.querySelector('[role="combobox"]').setAttribute('aria-expanded', 'true');
          measureTruncation(cbx); /* 面板可见后才量得到 scrollWidth */
        }
        return;
      }
      if (el && el.getAttribute('data-act') === 'pick-ndi') {
        var value = el.getAttribute('data-value');
        var box = el.closest('[data-cbx]');
        live.ndiUrl = value;
        var field = box.querySelector('[data-field="ndiUrl"]');
        field.value = value;
        closeAllCbx(liveHost);
        field.focus();
        return;
      }
      if (!e.target.closest('[data-cbx]')) closeAllCbx(liveHost);
    });

    liveHost.addEventListener('input', function (e) {
      var f = e.target.getAttribute && e.target.getAttribute('data-field');
      if (f === 'ndiName') live.ndiName = e.target.value;
      if (f === 'ndiUrl') live.ndiUrl = e.target.value; /* Manual 模式 */
    });

    liveHost.addEventListener('keydown', function (e) {
      var act = e.target.closest ? e.target.closest('[data-act]') : null;
      if (act && (e.key === ' ' || e.key === 'Enter')) {
        var a = act.getAttribute('data-act');
        if (a === 'pick-type' || a === 'pick-ndi') { e.preventDefault(); act.click(); return; }
      }
      if (e.key === 'Escape') { closeAllCbx(liveHost); return; }
      if (e.key === 'ArrowDown' && e.target.getAttribute('role') === 'combobox') {
        e.preventDefault();
        var toggle = e.target.closest('[data-cbx]').querySelector('[data-act="cbx-toggle"]');
        if (toggle) toggle.click();
      }
    });

    document.addEventListener('click', function (e) {
      if (!e.target.closest('#frame-live')) closeAllCbx(liveHost);
    });
  }

  mountStatic('frame-s1', 'local');
  mountStatic('frame-s2', 'ip');
  mountStatic('frame-s3', 'ndi');
})();
</script>
</body>
</html>
```

**下一步建议（按可靠性排序）**：① 开 `1011:7877` + `7862:1729` 实测宿主几何与当前页签文案，回填这稿的三处假设；② 补跑 `pnpm design:kickoff --check docs/internal/_design-kickoffs/v4-1827-ndi-into-ipsource.md` + `audit:no-hardcoded-design-tokens` + `audit:consumer-code`（本会话跑不了）；③ 请 owner 拍两条 placeholder 文案，以及 `Tooltip.vue` 的 `nowrap` + `max-width` 是否为 Figma 真源意图。