2525 * 棘轮因**新文件**当场红;且播放/暂停控制在 WAI-ARIA 的惯用做法本就是**换可访问名**而非按下态
2626 * (按下态适用于静音一类**模式**开关)。琥珀徽标 = R4.5 逐字点名的「暂停徽标」,也是 T29
2727 * 「琥珀退去」要作用的那个元素。
28+ * @ai -context T29 追加(#3 相变凝固 · R5.3 / R4.5):本件挂 `usePhaseFreeze(phase)` —— 相位经
29+ * `LiveBarHost` 注入(**可选**槽,缺省 `"capture"` = 本件既有渲染,冻结进度 0)。三处新结构:
30+ * ① 受控 prop `phase`(唯一新槽;`LiveBarProps` 的既有五项一字未动);
31+ * ② **墨度层** `data-testid="live-ink"`(唯一 GSAP 目标 = 「琥珀退去」的载体;R4.5 点名的 LIVE 仪表与
32+ * 暂停徽标都在它里面 —— 徽标是条件渲染,单独作载体在未暂停时会落空);
33+ * ③ `kind="warn"`:LIVE 仪表本身走 `--ed-due` 族(R4.5 逐字「采集态 LIVE 仪表/暂停徽标的 `--ed-due`
34+ * 族元素」)—— 退去只改**墨度**、不改色值。⚠️ 缺省相位下 hook **不建时间线**(相位未变)⇒ 本件
35+ * 既有六条判据零改动。
2836 */
2937import { useEffect , useState } from "react" ;
3038import type { ReactElement } from "react" ;
@@ -33,6 +41,8 @@ import { Button, StatusLine, Text } from "../ui/primitives";
3341import { Icon } from "../ui/icons" ;
3442import { fmtMs } from "../utils/fmt" ;
3543import Waveform from "../components/Waveform" ;
44+ import { usePhaseFreeze } from "./usePhaseFreeze" ;
45+ import type { ShellPhase } from "./shellPhase" ;
3646
3747/**
3848 * 采集态顶栏高度:**消费 T17 落真的新 token**(R4.1)。兜底 58px 与真源同值
@@ -57,6 +67,8 @@ export interface LiveBarProps {
5767 readonly onTogglePause : ( ) => void ;
5868 readonly onMark : ( ) => void ;
5969 readonly onStop : ( ) => void ;
70+ /** 当前相位(T29 追加的**可选**槽;缺省 `"capture"` = 本件既有渲染:冻结进度 0、无编排动效)。 */
71+ readonly phase ?: ShellPhase ;
6072}
6173
6274/**
@@ -71,8 +83,11 @@ export default function LiveBar({
7183 onTogglePause,
7284 onMark,
7385 onStop,
86+ phase = "capture" ,
7487} : LiveBarProps ) : ReactElement {
7588 const [ level , setLevel ] = useState < AudioLevelPayload > ( { rms : 0 , clipping : false } ) ;
89+ // T29:相变凝固的编排(波形按 `barsFreeze` 收束成直线;墨度层由 hook 的 GSAP 目标退到常态档)
90+ const { inkRef, barsFreeze } = usePhaseFreeze ( phase ) ;
7691
7792 useEffect ( ( ) => {
7893 const unlisten = listen < AudioLevelPayload > ( "live:audio-level" , ( e ) => {
@@ -100,39 +115,55 @@ export default function LiveBar({
100115 overflow : "hidden" ,
101116 } }
102117 >
103- { /* 语义(`role="status"`)与基类由 `StatusLine` 原语承载;仪表本体走它的 `action` 纯插槽
104- (该槽**不额外包 DOM、不加类** —— 见 `StatusLine.tsx` 边界④)。 */ }
105- < StatusLine
106- kind = "info"
107- testId = "live-meter"
108- action = {
109- < >
110- < Waveform rms = { level . rms } clipping = { level . clipping } testId = "live-waveform" />
111- < Text as = "span" size = { 4 } font = "mono" tone = "ink-2" testId = "live-elapsed" >
112- { fmtMs ( elapsedMs ) }
113- </ Text >
114- { paused ? (
115- < Text as = "span" size = { 5 } tone = "due" testId = "live-paused-badge" >
116- 已暂停
117- </ Text >
118- ) : null }
119- < Button variant = "secondary" size = "sm" testId = "live-pause" onClick = { onTogglePause }
120- icon = { < Icon name = { paused ? "play" : "pause" } size = { 16 } /> } >
121- { paused ? LIVE_PAUSE_LABELS . paused : LIVE_PAUSE_LABELS . running }
122- </ Button >
123- < Button variant = "ghost" size = "sm" testId = "live-mark" onClick = { onMark }
124- icon = { < Icon name = "check" size = { 16 } /> } >
125- 标记
126- </ Button >
127- < Button variant = "ghost" size = "sm" testId = "live-stop" onClick = { onStop }
128- icon = { < Icon name = "stop" size = { 16 } /> } >
129- 停止
130- </ Button >
131- </ >
132- }
118+ { /* 墨度层(T29「琥珀退去」的载体 · 唯一 GSAP 目标):**新建**的 div,**不声明任何 transition**
119+ (类规则或行内 transition 都会与逐帧写入打架:GSAP 每帧写、CSS 再插值 ⇒ 值永远追不上)。
120+ 它的 `opacity` 由 `usePhaseFreeze` 从 1 退到常态档;采集态(freeze 0)恒为 1 ⇒ 零观感变化。 */ }
121+ < div
122+ ref = { inkRef }
123+ data-testid = "live-ink"
124+ style = { { flex : "1 1 auto" , minWidth : 0 , display : "block" } }
133125 >
134- LIVE
135- </ StatusLine >
126+ { /* 语义(`role="status"`)与基类由 `StatusLine` 原语承载;仪表本体走它的 `action` 纯插槽
127+ (该槽**不额外包 DOM、不加类** —— 见 `StatusLine.tsx` 边界④)。
128+ `kind="warn"` = R4.5 逐字「采集态 LIVE 仪表…的 `--ed-due` 族」:琥珀是**采集态的身份墨**,
129+ 退去只退墨度(色值仍逐字是 `--ed-due`,见 `motion.css` 的相位块侧锁)。 */ }
130+ < StatusLine
131+ kind = "warn"
132+ testId = "live-meter"
133+ action = {
134+ < >
135+ < Waveform
136+ rms = { level . rms }
137+ clipping = { level . clipping }
138+ testId = "live-waveform"
139+ freeze = { barsFreeze }
140+ />
141+ < Text as = "span" size = { 4 } font = "mono" tone = "ink-2" testId = "live-elapsed" >
142+ { fmtMs ( elapsedMs ) }
143+ </ Text >
144+ { paused ? (
145+ < Text as = "span" size = { 5 } tone = "due" testId = "live-paused-badge" >
146+ 已暂停
147+ </ Text >
148+ ) : null }
149+ < Button variant = "secondary" size = "sm" testId = "live-pause" onClick = { onTogglePause }
150+ icon = { < Icon name = { paused ? "play" : "pause" } size = { 16 } /> } >
151+ { paused ? LIVE_PAUSE_LABELS . paused : LIVE_PAUSE_LABELS . running }
152+ </ Button >
153+ < Button variant = "ghost" size = "sm" testId = "live-mark" onClick = { onMark }
154+ icon = { < Icon name = "check" size = { 16 } /> } >
155+ 标记
156+ </ Button >
157+ < Button variant = "ghost" size = "sm" testId = "live-stop" onClick = { onStop }
158+ icon = { < Icon name = "stop" size = { 16 } /> } >
159+ 停止
160+ </ Button >
161+ </ >
162+ }
163+ >
164+ LIVE
165+ </ StatusLine >
166+ </ div >
136167 </ div >
137168 ) ;
138169}
0 commit comments