@cut-workbench/workbench 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lab.js","names":["_Fragment","_Fragment","_Fragment"],"sources":["../src/features/lab/components/LabCard.tsx","../src/features/lab/hooks/use-lab-action.ts","../src/features/lab/components/shared.tsx","../src/features/lab/components/ConcatenateLab.tsx","../src/features/lab/components/RemuxLab.tsx","../src/features/lab/components/ChromakeyLab.tsx","../src/features/lab/components/SubtitleLab.tsx","../src/features/lab/components/TrackSplitLab.tsx","../src/features/lab/components/ThumbnailLab.tsx","../src/features/lab/components/RecorderLab.tsx","../src/features/lab/components/CustomClipLab.tsx","../src/features/lab/components/LabPage.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\nimport { FlaskConical } from 'lucide-react'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { cn } from '@/lib/cn'\n\ninterface LabCardProps {\n title: string\n description: string\n apis: string[]\n children: ReactNode\n unavailable?: string | null\n className?: string\n}\n\n/** Lab 卡片外壳:标题 + 用到的 API + (不可用时的原因) */\nexport function LabCard({\n title,\n description,\n apis,\n children,\n unavailable,\n className,\n}: LabCardProps) {\n return (\n <Card className={cn('flex flex-col', className)}>\n <CardHeader className=\"gap-1\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex size-6 items-center justify-center rounded bg-primary/10 text-primary\">\n <FlaskConical className=\"size-3.5\" />\n </div>\n <CardTitle className=\"text-sm\">{title}</CardTitle>\n </div>\n <CardDescription className=\"text-xs\">{description}</CardDescription>\n <div className=\"flex flex-wrap gap-1 pt-1\">\n {apis.map((api) => (\n <Badge\n key={api}\n variant=\"outline\"\n className=\"font-mono text-[10px] font-normal text-muted-foreground\"\n >\n {api}\n </Badge>\n ))}\n </div>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-2.5\">\n {unavailable != null ? (\n <div className=\"rounded-md border border-dashed border-warning/40 bg-warning/5 p-3 text-xs text-warning\">\n {unavailable}\n </div>\n ) : (\n children\n )}\n </CardContent>\n </Card>\n )\n}\n","import { useCallback, useRef } from 'react'\nimport { useState } from 'react'\nimport { useWorkbench } from '@/context'\nimport { notify } from '@/lib/notify'\n\n/** 生成可下载链接(Lab 通用) */\nexport function useDownload(): (blob: Blob, filename: string) => void {\n const urlsRef = useRef<string[]>([])\n return useCallback((blob: Blob, filename: string) => {\n const url = URL.createObjectURL(blob)\n urlsRef.current.push(url)\n const link = document.createElement('a')\n link.href = url\n link.download = filename\n document.body.appendChild(link)\n link.click()\n link.remove()\n window.setTimeout(() => URL.revokeObjectURL(url), 15_000)\n }, [])\n}\n\n/** 带 busy 状态的异步执行器:统一错误 toast,禁止静默吞异常 */\nexport function useLabAction() {\n const { bridge } = useWorkbench()\n const [busy, setBusy] = useState<string | null>(null)\n const run = useCallback(\n async <T,>(label: string, task: () => Promise<T>): Promise<T | null> => {\n setBusy(label)\n try {\n return await task()\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n console.error(err)\n notify(bridge, { level: 'error', title: '操作失败', description: message })\n return null\n } finally {\n setBusy(null)\n }\n },\n [bridge],\n )\n return { busy, run }\n}\n","import { Loader2 } from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\nimport { useDownload } from '../hooks/use-lab-action'\n\n/**\n * Lab 共享的可视组件。\n *\n * 只导出组件(React Fast Refresh 要求单文件只导出组件);\n * 工具函数与 hook 放在 `../hooks/use-lab-action`。\n */\n\n/** 文件选择按钮 */\nexport function FilePickButton({\n label,\n accept,\n multiple,\n onFiles,\n disabled,\n}: {\n label: string\n accept: string\n multiple?: boolean\n onFiles: (files: File[]) => void\n disabled?: boolean\n}) {\n const inputRef = useRef<HTMLInputElement | null>(null)\n return (\n <>\n <input\n ref={inputRef}\n type=\"file\"\n accept={accept}\n multiple={multiple}\n className=\"hidden\"\n onChange={(e) => {\n const files = Array.from(e.target.files ?? [])\n if (files.length > 0) onFiles(files)\n e.target.value = ''\n }}\n />\n <button\n type=\"button\"\n disabled={disabled}\n onClick={() => inputRef.current?.click()}\n className=\"flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed px-3 py-3.5 text-xs text-muted-foreground transition-colors hover:border-primary/50 hover:text-foreground disabled:pointer-events-none disabled:opacity-50\"\n >\n <span className=\"max-w-full truncate\">{label}</span>\n </button>\n </>\n )\n}\n\n/** 结果预览:视频 + 下载 */\nexport function ResultPreview({ blob, filename }: { blob: Blob; filename: string }) {\n const download = useDownload()\n const [url] = useState(() => URL.createObjectURL(blob))\n useEffect(() => () => URL.revokeObjectURL(url), [url])\n\n return (\n <div className=\"space-y-1.5\">\n <video\n src={url}\n controls\n className=\"w-full rounded-md border bg-stage-bg\"\n style={{ maxHeight: 200 }}\n />\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"min-w-0 flex-1 truncate text-[10px] text-muted-foreground\">\n {(blob.size / 1024 / 1024).toFixed(2)} MB\n </span>\n <button\n type=\"button\"\n onClick={() => download(blob, filename)}\n className=\"rounded-md bg-primary px-2 py-1 text-[11px] text-primary-foreground hover:bg-primary/90\"\n >\n 下载\n </button>\n </div>\n </div>\n )\n}\n\n/** 忙碌提示 */\nexport function Busy({ label }: { label: string }) {\n return (\n <div className=\"flex items-center gap-2 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground\">\n <Loader2 className=\"size-3 animate-spin\" />\n {label}\n </div>\n )\n}\n\n/** 简易滑杆 */\nexport function SliderRow({\n label,\n value,\n min,\n max,\n step,\n format,\n onChange,\n}: {\n label: string\n value: number\n min: number\n max: number\n step: number\n format?: (value: number) => string\n onChange: (value: number) => void\n}) {\n return (\n <div className=\"space-y-1\">\n <div className=\"flex justify-between text-[10px] text-muted-foreground\">\n <span>{label}</span>\n <span className=\"font-mono tabular-nums\">{format ? format(value) : value.toFixed(2)}</span>\n </div>\n <input\n type=\"range\"\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={(e) => onChange(Number(e.target.value))}\n className=\"h-1 w-full cursor-pointer appearance-none rounded-full bg-muted accent-primary\"\n />\n </div>\n )\n}\n","import { useState } from 'react'\nimport { AlertTriangle } from 'lucide-react'\nimport { concatMp4, readMp4Info, type Mp4Info } from '@cut-workbench/core/lab'\nimport { formatDuration, formatResolution } from '@/lib/format'\nimport { LabCard } from './LabCard'\nimport { useLabAction } from '../hooks/use-lab-action'\nimport { Busy, FilePickButton, ResultPreview } from './shared'\n\n/**\n * Lab 1:无损拼接。\n *\n * 用 `fastConcatMP4` 直接拼接多个 MP4 的样本,不重编码。\n * 前提是编码 / 分辨率 / 采样率完全一致;不一致时本 Lab 会先预检并给出具体差异。\n */\nexport function ConcatenateLab() {\n const [files, setFiles] = useState<File[]>([])\n const [infos, setInfos] = useState<Mp4Info[]>([])\n const [result, setResult] = useState<Blob | null>(null)\n const { busy, run } = useLabAction()\n\n const handleFiles = async (picked: File[]) => {\n setFiles(picked)\n setResult(null)\n setInfos([])\n const detected = await run('正在读取文件信息…', async () => {\n const list: Mp4Info[] = []\n for (const file of picked) list.push(await readMp4Info(file))\n return list\n })\n if (detected != null) setInfos(detected)\n }\n\n const handleConcat = async () => {\n const output = await run('正在无损拼接…', () => concatMp4(files))\n if (output != null) {\n setResult(output.blob)\n setInfos(output.infos)\n }\n }\n\n const first = infos[0]\n const mismatch =\n first != null &&\n infos.some(\n (info) =>\n info.width !== first.width ||\n info.height !== first.height ||\n info.codec !== first.codec ||\n info.audioSampleRate !== first.audioSampleRate,\n )\n\n return (\n <LabCard\n title=\"无损拼接\"\n description=\"直接把多个 MP4 的样本拼在一起,不重编码,速度极快。要求规格完全一致。\"\n apis={['fastConcatMP4', 'MP4Clip.getFileHeaderBinData']}\n >\n <FilePickButton\n label={files.length > 0 ? `已选 ${files.length} 个文件(重新选择)` : '选择 2 个以上 MP4'}\n accept=\"video/mp4\"\n multiple\n onFiles={(picked) => void handleFiles(picked)}\n disabled={busy != null}\n />\n\n {files.length > 0 && (\n <ul className=\"space-y-1 text-[10px] text-muted-foreground\">\n {files.map((file, index) => {\n const info = infos[index]\n return (\n <li key={`${file.name}-${index}`} className=\"flex justify-between gap-2\">\n <span className=\"truncate\">{file.name}</span>\n <span className=\"shrink-0 font-mono\">\n {info != null\n ? `${formatResolution(info.width, info.height)} · ${formatDuration(info.durationUs)} · ${info.codec}`\n : '检测中…'}\n </span>\n </li>\n )\n })}\n </ul>\n )}\n\n {mismatch && (\n <div className=\"flex items-start gap-1.5 rounded-md bg-warning/10 p-2 text-[10px] text-warning\">\n <AlertTriangle className=\"mt-0.5 size-3 shrink-0\" />\n 检测到规格不一致,拼接会失败并给出具体原因(fastConcatMP4 的硬性要求)。\n </div>\n )}\n\n <button\n type=\"button\"\n disabled={files.length < 2 || busy != null}\n onClick={() => void handleConcat()}\n className=\"rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50\"\n >\n 开始拼接\n </button>\n\n {busy != null && <Busy label={busy} />}\n {result != null && <ResultPreview blob={result} filename=\"concat.mp4\" />}\n </LabCard>\n )\n}\n","import { useState } from 'react'\r\nimport { mixAudioIntoMp4 } from '@cut-workbench/core/lab'\r\nimport { LabCard } from './LabCard'\r\nimport { useLabAction } from '../hooks/use-lab-action'\r\nimport { Busy, FilePickButton, ResultPreview, SliderRow } from './shared'\r\n\r\n/**\r\n * Lab 2:配音。\r\n *\r\n * `mixinMP4AndAudio` 只重编码音频轨,视频轨原样复制 —— 因此速度快、画质无损。\r\n */\r\nexport function RemuxLab() {\r\n const [video, setVideo] = useState<File | null>(null)\r\n const [audio, setAudio] = useState<File | null>(null)\r\n const [volume, setVolume] = useState(1)\r\n const [loop, setLoop] = useState(true)\r\n const [result, setResult] = useState<Blob | null>(null)\r\n const { busy, run } = useLabAction()\r\n\r\n const handleMix = async () => {\r\n if (video == null || audio == null) return\r\n const output = await run('正在混合音频…', () =>\r\n mixAudioIntoMp4(video, audio, volume, loop),\r\n )\r\n if (output != null) setResult(output)\r\n }\r\n\r\n return (\r\n <LabCard\r\n title=\"视频配音\"\r\n description=\"把音轨混入 MP4:只重编码音频,视频轨不动,画质零损失。\"\r\n apis={['mixinMP4AndAudio']}\r\n >\r\n <FilePickButton\r\n label={video == null ? '选择视频(MP4)' : video.name}\r\n accept=\"video/mp4\"\r\n onFiles={(files) => {\r\n setVideo(files[0] ?? null)\r\n setResult(null)\r\n }}\r\n disabled={busy != null}\r\n />\r\n <FilePickButton\r\n label={audio == null ? '选择音频(MP3/WAV/M4A)' : audio.name}\r\n accept=\"audio/*\"\r\n onFiles={(files) => {\r\n setAudio(files[0] ?? null)\r\n setResult(null)\r\n }}\r\n disabled={busy != null}\r\n />\r\n\r\n <SliderRow\r\n label=\"音量\"\r\n value={volume}\r\n min={0}\r\n max={2}\r\n step={0.05}\r\n format={(v) => `${Math.round(v * 100)}%`}\r\n onChange={setVolume}\r\n />\r\n <label className=\"flex items-center gap-2 text-[11px] text-muted-foreground\">\r\n <input\r\n type=\"checkbox\"\r\n checked={loop}\r\n onChange={(e) => setLoop(e.target.checked)}\r\n className=\"accent-primary\"\r\n />\r\n 音频短于视频时循环填充\r\n </label>\r\n\r\n <button\r\n type=\"button\"\r\n disabled={video == null || audio == null || busy != null}\r\n onClick={() => void handleMix()}\r\n className=\"rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50\"\r\n >\r\n 开始混合\r\n </button>\r\n\r\n {busy != null && <Busy label={busy} />}\r\n {result != null && <ResultPreview blob={result} filename=\"mixin.mp4\" />}\r\n </LabCard>\r\n )\r\n}\r\n","import { useCallback, useEffect, useRef, useState } from 'react'\nimport { isWebGL2Available, renderChromakeyFrame, createMp4Clip, type Mp4ClipHandle } from '@cut-workbench/core/lab'\nimport { LabCard } from './LabCard'\nimport { useLabAction } from '../hooks/use-lab-action'\nimport { useWorkbench } from '@/context'\nimport { notify } from '@/lib/notify'\nimport { Busy, FilePickButton, SliderRow } from './shared'\n\n/**\n * Lab 3:绿幕抠像。\n *\n * `createChromakey` 返回一个函数,把 VideoFrame 转成去背后的 ImageBitmap。\n * 配合 `MP4Clip.tickInterceptor` 即可对整段视频生效(导出时同样有效)。\n *\n * ⚠️ 依赖 WebGL2;不支持时本卡片隐藏操作、仅显示原因。\n */\n/**\n * 抠像结果的兜底底色:读取主题令牌,而不是写死 `#101018`。\n *\n * 抠像后的 VideoFrame 带透明通道,需要一层底色才能看清;用 `--cw-stage-bg`\n * 让 Lab 预览区与工作台舞台保持一致,宿主换主题时一起变。\n * 取不到时退回深色(纯逻辑环境 / 未包 `.cw-root`)。\n */\nfunction readStageBackground(): string {\n if (typeof document === 'undefined') return '#101018'\n const root = document.querySelector('.cw-root')\n if (root == null) return '#101018'\n const value = getComputedStyle(root).getPropertyValue('--cw-stage-bg').trim()\n return value.length > 0 ? value : '#101018'\n}\n\nexport function ChromakeyLab() {\n const { bridge } = useWorkbench()\n // WebGL2 探测是纯函数,可安全在 useState 初始化中执行\n const [webgl2] = useState(isWebGL2Available)\n const [file, setFile] = useState<File | null>(null)\n const [similarity, setSimilarity] = useState(0.4)\n const [smoothness, setSmoothness] = useState(0.1)\n const [spill, setSpill] = useState(0.1)\n const [timeUs, setTimeUs] = useState(0)\n const [durationUs, setDurationUs] = useState(0)\n const canvasRef = useRef<HTMLCanvasElement | null>(null)\n const clipRef = useRef<Mp4ClipHandle | null>(null)\n const { busy, run } = useLabAction()\n\n useEffect(() => {\n return () => {\n clipRef.current?.destroy()\n clipRef.current = null\n }\n }, [])\n\n const render = useCallback(async () => {\n const clip = clipRef.current\n const canvas = canvasRef.current\n if (clip == null || canvas == null) return\n await renderChromakeyFrame(\n clip,\n timeUs,\n { similarity, smoothness, spill, keyColor: [0, 255, 0] },\n canvas,\n // 底色由主题令牌驱动:抠像结果画在透明画布上,用舞台色兜底,\n // 这样宿主换主题时 Lab 预览区也跟着变\n readStageBackground(),\n )\n }, [similarity, smoothness, spill, timeUs])\n\n const handleFile = async (files: File[]) => {\n const picked = files[0]\n if (picked == null) return\n const result = await run('正在解析视频…', async () => {\n clipRef.current?.destroy()\n const created = await createMp4Clip(picked)\n return created\n })\n if (result == null) return\n clipRef.current = result.clip\n setFile(picked)\n setDurationUs(result.meta.duration)\n setTimeUs(Math.min(result.meta.duration / 2, 1_000_000))\n notify(bridge, { level: 'success', title: '已载入视频,拖动参数查看抠像效果' })\n }\n\n useEffect(() => {\n if (clipRef.current == null || busy != null) return\n void render()\n }, [busy, render])\n\n return (\n <LabCard\n title=\"绿幕抠像\"\n description=\"用 WebGL2 实时扣除背景色,通过 tickInterceptor 作用到整段视频。\"\n apis={['createChromakey', 'MP4Clip.tickInterceptor']}\n unavailable={webgl2 ? null : '当前设备不支持 WebGL2,createChromakey 无法运行。'}\n >\n <FilePickButton\n label={file?.name ?? '选择绿幕视频(MP4)'}\n accept=\"video/mp4\"\n onFiles={(files) => void handleFile(files)}\n disabled={busy != null}\n />\n\n <div className=\"relative overflow-hidden rounded-md border bg-stage-bg\">\n <canvas ref={canvasRef} className=\"block w-full\" style={{ maxHeight: 180 }} />\n {file == null && (\n <div className=\"pointer-events-none absolute inset-0 flex h-24 items-center justify-center text-[10px] text-muted-foreground\">\n 选择视频后显示抠像结果\n </div>\n )}\n </div>\n\n <SliderRow\n label=\"相似度 similarity\"\n value={similarity}\n min={0}\n max={1}\n step={0.01}\n format={(v) => v.toFixed(2)}\n onChange={setSimilarity}\n />\n <SliderRow\n label=\"平滑度 smoothness\"\n value={smoothness}\n min={0}\n max={1}\n step={0.01}\n format={(v) => v.toFixed(2)}\n onChange={setSmoothness}\n />\n <SliderRow\n label=\"饱和度 spill\"\n value={spill}\n min={0}\n max={1}\n step={0.01}\n format={(v) => v.toFixed(2)}\n onChange={setSpill}\n />\n {durationUs > 0 && (\n <SliderRow\n label=\"取帧时间\"\n value={timeUs}\n min={0}\n max={Math.max(1, durationUs - 1)}\n step={100_000}\n format={(v) => `${(v / 1e6).toFixed(2)}s`}\n onChange={setTimeUs}\n />\n )}\n {busy != null && <Busy label={busy} />}\n <p className=\"text-[10px] leading-relaxed text-muted-foreground\">\n keyColor 固定为纯绿 (0,255,0)。实际项目中可读取首帧像素自动取色。\n </p>\n </LabCard>\n )\n}\n","import { useCallback, useEffect, useRef, useState } from 'react'\nimport { Play, Square } from 'lucide-react'\nimport {\n burnSubtitles,\n createSubtitlePreviewClip,\n renderSubtitleFrame,\n type SubtitleClipHandle,\n} from '@cut-workbench/core/lab'\nimport { parseSrt } from '@cut-workbench/core'\nimport { LabCard } from './LabCard'\nimport { useLabAction } from '../hooks/use-lab-action'\nimport { Busy, ResultPreview, SliderRow } from './shared'\n\nconst SAMPLE_SRT = `1\n00:00:00,000 --> 00:00:02,000\n这是第一句字幕\n\n2\n00:00:02,000 --> 00:00:04,000\n第二句会带描边\n\n3\n00:00:04,000 --> 00:00:06,000\nEmbedSubtitlesClip 烧录进画面\n`\n\n/**\n * Lab 4:字幕烧录。\n *\n * `EmbedSubtitlesClip` 本身就是一个 IClip:tick 返回带字幕的 VideoFrame。\n * 因此既能放进 Combinator 导出,也能直接逐帧预览。\n */\nexport function SubtitleLab() {\n const [srt, setSrt] = useState(SAMPLE_SRT)\n const [timeUs, setTimeUs] = useState(0)\n const [playing, setPlaying] = useState(false)\n const [result, setResult] = useState<Blob | null>(null)\n const { busy, run } = useLabAction()\n const canvasRef = useRef<HTMLCanvasElement | null>(null)\n const clipRef = useRef<SubtitleClipHandle | null>(null)\n const rafRef = useRef<number | null>(null)\n\n const cues = parseSrt(srt)\n const durationUs = cues.length > 0 ? cues[cues.length - 1]!.endMs * 1000 : 0\n\n // 重新创建字幕 clip\n useEffect(() => {\n clipRef.current?.destroy()\n if (cues.length === 0) {\n clipRef.current = null\n return\n }\n try {\n clipRef.current = createSubtitlePreviewClip(srt, 640, 360)\n } catch (err) {\n console.warn('[SubtitleLab] create failed', err)\n clipRef.current = null\n }\n return () => {\n clipRef.current?.destroy()\n clipRef.current = null\n }\n }, [cues.length, srt])\n\n const draw = useCallback(async () => {\n const clip = clipRef.current\n const canvas = canvasRef.current\n if (clip == null || canvas == null) return\n await renderSubtitleFrame(clip, timeUs, canvas)\n }, [timeUs])\n\n useEffect(() => {\n void draw()\n }, [draw])\n\n // 播放循环(简单 rAF 递增时间)\n useEffect(() => {\n if (!playing) return\n let last = performance.now()\n let current = timeUs\n const tick = () => {\n const now = performance.now()\n current += (now - last) * 1000\n last = now\n if (durationUs > 0 && current >= durationUs) {\n setTimeUs(durationUs)\n setPlaying(false)\n return\n }\n setTimeUs(current)\n rafRef.current = requestAnimationFrame(tick)\n }\n rafRef.current = requestAnimationFrame(tick)\n return () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current)\n }\n // 只在开始播放时启动\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [playing, durationUs])\n\n const handleBurn = async () => {\n const width = 1280\n const height = 720\n const output = await run('正在烧录字幕…', () =>\n burnSubtitles(srt, width, height, durationUs),\n )\n if (output != null) setResult(output)\n }\n\n return (\n <LabCard\n title=\"字幕烧录\"\n description=\"SRT 解析后逐帧绘制到画面,最终编码进视频流(硬字幕)。\"\n apis={['EmbedSubtitlesClip', 'Combinator']}\n >\n <textarea\n value={srt}\n onChange={(e) => setSrt(e.target.value)}\n rows={5}\n className=\"w-full rounded-md border bg-transparent p-2 font-mono text-[10px] outline-none focus-visible:border-ring\"\n placeholder=\"粘贴 SRT 内容\"\n />\n\n <div className=\"overflow-hidden rounded-md border bg-stage-bg\">\n <canvas\n ref={canvasRef}\n width={640}\n height={360}\n className=\"block w-full\"\n style={{ maxHeight: 180 }}\n />\n </div>\n\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setPlaying((p) => !p)}\n disabled={cues.length === 0}\n className=\"flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-[11px] disabled:opacity-50\"\n >\n {playing ? <Square className=\"size-3\" /> : <Play className=\"size-3\" />}\n {playing ? '停止' : '预览'}\n </button>\n <span className=\"text-[10px] text-muted-foreground\">\n {cues.length} 条 · {(timeUs / 1e6).toFixed(2)}s / {(durationUs / 1e6).toFixed(2)}s\n </span>\n </div>\n\n <SliderRow\n label=\"时间\"\n value={timeUs}\n min={0}\n max={Math.max(1, durationUs)}\n step={10_000}\n format={(v) => `${(v / 1e6).toFixed(2)}s`}\n onChange={(v) => {\n setPlaying(false)\n setTimeUs(v)\n }}\n />\n\n <button\n type=\"button\"\n disabled={cues.length === 0 || busy != null}\n onClick={() => void handleBurn()}\n className=\"rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50\"\n >\n 烧录并导出 MP4\n </button>\n\n {busy != null && <Busy label={busy} />}\n {result != null && <ResultPreview blob={result} filename=\"subtitles.mp4\" />}\n </LabCard>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\nimport { Split } from 'lucide-react'\nimport { splitMp4Tracks, type TrackSplitResult } from '@cut-workbench/core/lab'\nimport { formatDuration } from '@/lib/format'\nimport { LabCard } from './LabCard'\nimport { useLabAction } from '../hooks/use-lab-action'\nimport { Busy, FilePickButton } from './shared'\n\n/**\n * Lab 5:轨道拆分。\n *\n * `splitTrack()` 从一个 MP4Clip 派生出「纯视频」与「纯音频」两个 clip,\n * 它们与源共享样本表,因此内存开销很小。\n */\nexport function TrackSplitLab() {\n const [file, setFile] = useState<File | null>(null)\n const [result, setResult] = useState<TrackSplitResult | null>(null)\n const { busy, run } = useLabAction()\n const cleanupRef = useRef<(() => void) | null>(null)\n\n useEffect(() => {\n return () => cleanupRef.current?.()\n }, [])\n\n const handleSplit = async () => {\n if (file == null) return\n cleanupRef.current?.()\n const output = await run('正在拆分轨道…', () => splitMp4Tracks(file))\n setResult(output)\n cleanupRef.current = () => {\n output?.videoClip?.destroy()\n output?.audioClip?.destroy()\n }\n }\n\n return (\n <LabCard\n title=\"轨道拆分\"\n description=\"把一个 MP4 拆成纯视频轨与纯音频轨两个独立 IClip,用于分别编辑。\"\n apis={['MP4Clip.splitTrack']}\n >\n <FilePickButton\n label={file?.name ?? '选择 MP4'}\n accept=\"video/mp4\"\n onFiles={(files) => {\n setFile(files[0] ?? null)\n setResult(null)\n }}\n disabled={busy != null}\n />\n\n <button\n type=\"button\"\n disabled={file == null || busy != null}\n onClick={() => void handleSplit()}\n className=\"flex items-center justify-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50\"\n >\n <Split className=\"size-3\" />\n 拆分轨道\n </button>\n\n {busy != null && <Busy label={busy} />}\n\n {result != null && (\n <div className=\"grid gap-1.5 text-[10px]\">\n <TrackResult\n label=\"视频轨\"\n present={result.videoMeta != null}\n detail={\n result.videoMeta != null\n ? `${result.videoMeta.width}×${result.videoMeta.height} · ${formatDuration(result.videoMeta.duration)}`\n : '源文件无视频轨'\n }\n />\n <TrackResult\n label=\"音频轨\"\n present={result.audioMeta != null}\n detail={\n result.audioMeta != null\n ? `48kHz 双声道 · ${formatDuration(result.audioMeta.duration)}`\n : '源文件无音频轨'\n }\n />\n <p className=\"pt-1 leading-relaxed text-muted-foreground\">\n 拆分出的 clip 与源共享样本表,可分别创建 Sprite 放到不同轨道;两者在时间轴上对齐即等于原片。\n </p>\n </div>\n )}\n </LabCard>\n )\n}\n\nfunction TrackResult({\n label,\n present,\n detail,\n}: {\n label: string\n present: boolean\n detail: string\n}) {\n return (\n <div className=\"flex items-center justify-between gap-2 rounded-md border px-2 py-1.5\">\n <span className={present ? 'text-foreground' : 'text-muted-foreground'}>{label}</span>\n <span className={present ? 'font-mono text-success' : 'text-muted-foreground'}>\n {present ? detail : detail}\n </span>\n </div>\n )\n}\n","import { useEffect, useState } from 'react'\r\nimport { extractKeyframes } from '@cut-workbench/core/lab'\r\nimport { formatTimecode } from '@cut-workbench/core'\r\nimport { LabCard } from './LabCard'\r\nimport { useLabAction } from '../hooks/use-lab-action'\r\nimport { Busy, FilePickButton } from './shared'\r\n\r\ninterface FrameItem {\r\n ts: number\r\n url: string\r\n size: number\r\n}\r\n\r\n/**\r\n * Lab 6:缩略图抽帧。\r\n *\r\n * `MP4Clip.thumbnails()` 默认每个关键帧生成一张;也可以传 step 按固定间隔取。\r\n * 这里展示全部关键帧与时间戳。\r\n */\r\nexport function ThumbnailLab() {\r\n const [file, setFile] = useState<File | null>(null)\r\n const [frames, setFrames] = useState<FrameItem[]>([])\r\n const [width, setWidth] = useState(120)\r\n const { busy, run } = useLabAction()\r\n\r\n useEffect(() => {\r\n return () => {\r\n for (const frame of frames) URL.revokeObjectURL(frame.url)\r\n }\r\n }, [frames])\r\n\r\n const handleExtract = async () => {\r\n if (file == null) return\r\n const output = await run('正在抽取关键帧…', () => extractKeyframes(file, width))\r\n if (output != null) setFrames(output)\r\n }\r\n\r\n return (\r\n <LabCard\r\n title=\"缩略图抽帧\"\r\n description=\"按关键帧抽取缩略图,可用于时间轴缩略图带或场景切分预览。\"\r\n apis={['MP4Clip.thumbnails']}\r\n >\r\n <FilePickButton\r\n label={file?.name ?? '选择 MP4'}\r\n accept=\"video/mp4\"\r\n onFiles={(files) => {\r\n setFile(files[0] ?? null)\r\n setFrames([])\r\n }}\r\n disabled={busy != null}\r\n />\r\n\r\n <div className=\"flex items-center gap-2\">\r\n <label className=\"text-[10px] text-muted-foreground\">宽度</label>\r\n <input\r\n type=\"number\"\r\n min={40}\r\n max={480}\r\n step={20}\r\n value={width}\r\n onChange={(e) => setWidth(Number(e.target.value))}\r\n className=\"h-6 w-16 rounded border bg-transparent px-1.5 text-[11px]\"\r\n />\r\n <button\r\n type=\"button\"\r\n disabled={file == null || busy != null}\r\n onClick={() => void handleExtract()}\r\n className=\"ml-auto rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50\"\r\n >\r\n 抽取\r\n </button>\r\n </div>\r\n\r\n {busy != null && <Busy label={busy} />}\r\n\r\n {frames.length > 0 && (\r\n <>\r\n <p className=\"text-[10px] text-muted-foreground\">共 {frames.length} 张关键帧</p>\r\n <div className=\"thin-scrollbar grid max-h-52 grid-cols-3 gap-1.5 overflow-auto\">\r\n {frames.map((frame, index) => (\r\n <figure key={`${frame.ts}-${index}`} className=\"space-y-0.5\">\r\n <img\r\n src={frame.url}\r\n alt=\"\"\r\n className=\"w-full rounded border object-cover\"\r\n loading=\"lazy\"\r\n />\r\n <figcaption className=\"font-mono text-[9px] text-muted-foreground tabular-nums\">\r\n {formatTimecode(frame.ts)}\r\n </figcaption>\r\n </figure>\r\n ))}\r\n </div>\r\n </>\r\n )}\r\n </LabCard>\r\n )\r\n}\r\n","import { useCallback, useEffect, useRef, useState } from 'react'\nimport { Circle, Pause, Play, Square } from 'lucide-react'\nimport {\n AVRecorder,\n createDemoCanvasStream,\n fixRecordedDuration,\n formatRecorderState,\n type RecorderHandle,\n} from '@cut-workbench/core/lab'\nimport { LabCard } from './LabCard'\nimport { useLabAction } from '../hooks/use-lab-action'\nimport { ResultPreview } from './shared'\nimport { useWorkbench } from '@/context'\nimport { notify } from '@/lib/notify'\n\ntype Mode = 'demo' | 'camera'\n\n/**\n * Lab 7:屏幕 / 画布录制。\n *\n * `AVRecorder` 接收任意 MediaStream 并输出 MP4(H.264 + AAC)。\n * 这里提供两种源:\n * - 程序化画布(Canvas.captureStream,无需授权);\n * - 摄像头(getUserMedia,需要用户授权)。\n */\nexport function RecorderLab() {\n const { bridge } = useWorkbench()\n const [mode, setMode] = useState<Mode>('demo')\n const [recording, setRecording] = useState(false)\n const [paused, setPaused] = useState(false)\n const [bytes, setBytes] = useState(0)\n const [state, setState] = useState<string>('inactive')\n const [result, setResult] = useState<Blob | null>(null)\n const [previewStream, setPreviewStream] = useState<MediaStream | null>(null)\n const videoRef = useRef<HTMLVideoElement | null>(null)\n const recorderRef = useRef<RecorderHandle | null>(null)\n const demoRef = useRef<{ stream: MediaStream; stop: () => void } | null>(null)\n const cameraRef = useRef<MediaStream | null>(null)\n const { run } = useLabAction()\n\n // 预览流挂到 <video>\n useEffect(() => {\n const video = videoRef.current\n if (video == null) return\n video.srcObject = previewStream\n if (previewStream != null) void video.play().catch(() => undefined)\n return () => {\n if (video.srcObject === previewStream) video.srcObject = null\n }\n }, [previewStream])\n\n const cleanup = useCallback(() => {\n demoRef.current?.stop()\n demoRef.current = null\n cameraRef.current?.getTracks().forEach((t) => t.stop())\n cameraRef.current = null\n recorderRef.current?.dispose()\n recorderRef.current = null\n setPreviewStream(null)\n }, [])\n\n useEffect(() => cleanup, [cleanup])\n\n const acquireStream = async (): Promise<MediaStream | null> => {\n cleanup()\n if (mode === 'demo') {\n const demo = createDemoCanvasStream(1280, 720)\n demoRef.current = demo\n setPreviewStream(demo.stream)\n return demo.stream\n }\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })\n cameraRef.current = stream\n setPreviewStream(stream)\n return stream\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n notify(bridge, { level: 'error', title: '无法访问摄像头', description: message })\n return null\n }\n }\n\n const handleStart = async () => {\n const stream = await acquireStream()\n if (stream == null) return\n setBytes(0)\n setResult(null)\n const recorder = createRecorderAdapter(stream, (total) => setBytes(total))\n recorderRef.current = recorder\n recorder.start()\n setRecording(true)\n setPaused(false)\n setState(recorder.state())\n }\n\n const handlePauseToggle = () => {\n const recorder = recorderRef.current\n if (recorder == null) return\n if (paused) {\n recorder.resume()\n setPaused(false)\n } else {\n recorder.pause()\n setPaused(true)\n }\n setState(recorder.state())\n }\n\n const handleStop = async () => {\n const recorder = recorderRef.current\n if (recorder == null) return\n const output = await run('正在封装 MP4…', async () => {\n const raw = await recorder.stop()\n // AVRecorder 输出的是 fMP4,moov 时长为 0,需修复后播放器才显示正确总长\n return fixRecordedDuration(raw)\n })\n setRecording(false)\n setPaused(false)\n setState(recorder.state())\n recorderRef.current = null\n if (output != null) {\n setResult(output)\n notify(bridge, {\n level: 'success',\n title: `录制完成 · ${(output.size / 1024 / 1024).toFixed(2)} MB`,\n })\n }\n }\n\n return (\n <LabCard\n title=\"屏幕 / 画布录制\"\n description=\"用 AVRecorder 把 MediaStream 实时编码为 MP4。可录制程序化画布或摄像头。\"\n apis={['AVCanvas.captureStream', 'AVRecorder.start/pause/resume/stop']}\n >\n <div className=\"flex gap-1.5\">\n {(['demo', 'camera'] as Mode[]).map((item) => (\n <button\n key={item}\n type=\"button\"\n disabled={recording}\n onClick={() => setMode(item)}\n className={`flex-1 rounded-md border px-2 py-1 text-[11px] disabled:opacity-50 ${\n mode === item ? 'border-primary bg-primary/10 text-primary' : 'text-muted-foreground'\n }`}\n >\n {item === 'demo' ? '动态画布' : '摄像头'}\n </button>\n ))}\n </div>\n\n <div className=\"overflow-hidden rounded-md border bg-stage-bg\">\n <video\n ref={videoRef}\n muted\n playsInline\n className=\"block w-full\"\n style={{ maxHeight: 170 }}\n />\n </div>\n\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {!recording ? (\n <button\n type=\"button\"\n onClick={() => void handleStart()}\n className=\"flex items-center gap-1 rounded-md bg-destructive px-2.5 py-1 text-[11px] text-destructive-foreground\"\n >\n <Circle className=\"size-3 fill-current\" />\n 开始录制\n </button>\n ) : (\n <>\n <button\n type=\"button\"\n onClick={handlePauseToggle}\n className=\"flex items-center gap-1 rounded-md bg-secondary px-2.5 py-1 text-[11px]\"\n >\n {paused ? <Play className=\"size-3\" /> : <Pause className=\"size-3\" />}\n {paused ? '继续' : '暂停'}\n </button>\n <button\n type=\"button\"\n onClick={() => void handleStop()}\n className=\"flex items-center gap-1 rounded-md bg-primary px-2.5 py-1 text-[11px] text-primary-foreground\"\n >\n <Square className=\"size-3\" />\n 停止\n </button>\n </>\n )}\n <span className=\"ml-auto font-mono text-[10px] text-muted-foreground tabular-nums\">\n {(bytes / 1024 / 1024).toFixed(2)} MB · {formatRecorderState(state)}\n </span>\n </div>\n\n {result != null && <ResultPreview blob={result} filename=\"recording.mp4\" />}\n </LabCard>\n )\n}\n\n/** 适配 RecorderHandle(隔离 AVRecorder 细节) */\nfunction createRecorderAdapter(\n stream: MediaStream,\n onChunk: (bytes: number) => void,\n): RecorderHandle {\n const recorder = new AVRecorder(stream, { expectFPS: 30 })\n const chunks: BlobPart[] = []\n let total = 0\n\n return {\n start: () => {\n const readable = recorder.start(300)\n void (async () => {\n const reader = readable.getReader()\n for (;;) {\n try {\n const { done, value } = await reader.read()\n if (done) break\n if (value != null) {\n chunks.push(new Uint8Array(value))\n total += value.byteLength\n onChunk(total)\n }\n } catch {\n break\n }\n }\n })()\n },\n pause: () => recorder.pause(),\n resume: () => recorder.resume(),\n stop: async () => {\n await recorder.stop()\n return new Blob(chunks, { type: 'video/mp4' })\n },\n state: () => recorder.state,\n dispose: () => {\n stream.getTracks().forEach((t) => t.stop())\n },\n }\n}\n","import { useCallback, useEffect, useRef, useState } from 'react'\nimport { Play, Square } from 'lucide-react'\nimport {\n FrameCounterClip,\n exportCustomClip,\n renderClipFrameToCanvas,\n type CustomClipHandle,\n} from '@cut-workbench/core/lab'\nimport { formatTimecode } from '@cut-workbench/core'\nimport { LabCard } from './LabCard'\nimport { useLabAction } from '../hooks/use-lab-action'\nimport { Busy, ResultPreview, SliderRow } from './shared'\n\n/**\n * Lab 8:自定义 IClip。\n *\n * `FrameCounterClip` 是本项目实现的 IClip:不依赖任何媒体文件,\n * 逐帧用 OffscreenCanvas 画出帧号 / 进度条 / 色相变化。\n *\n * 它证明 IClip 接口(tick / ready / meta / clone / split / destroy)可以被任意扩展,\n * 且能直接被 Combinator 编码成 MP4。\n */\nexport function CustomClipLab() {\n const [durationSec, setDurationSec] = useState(5)\n const [timeUs, setTimeUs] = useState(0)\n const [playing, setPlaying] = useState(false)\n const [result, setResult] = useState<Blob | null>(null)\n const canvasRef = useRef<HTMLCanvasElement | null>(null)\n const clipRef = useRef<CustomClipHandle | null>(null)\n const rafRef = useRef<number | null>(null)\n const { busy, run } = useLabAction()\n\n const durationUs = Math.round(durationSec * 1_000_000)\n\n // 重建 clip(时长变化时)\n useEffect(() => {\n clipRef.current?.destroy()\n try {\n clipRef.current = new FrameCounterClip(640, 360, durationUs, 30)\n } catch (err) {\n console.warn('[CustomClipLab] create failed', err)\n clipRef.current = null\n }\n return () => {\n clipRef.current?.destroy()\n clipRef.current = null\n }\n }, [durationUs])\n\n const draw = useCallback(async () => {\n const clip = clipRef.current\n const canvas = canvasRef.current\n if (clip == null || canvas == null) return\n await renderClipFrameToCanvas(clip, timeUs, canvas)\n }, [timeUs])\n\n useEffect(() => {\n void draw()\n }, [draw])\n\n // 播放\n useEffect(() => {\n if (!playing) return\n let last = performance.now()\n let current = timeUs\n const tick = () => {\n const now = performance.now()\n current += (now - last) * 1000\n last = now\n if (current >= durationUs) {\n setTimeUs(durationUs - 1)\n setPlaying(false)\n return\n }\n setTimeUs(current)\n rafRef.current = requestAnimationFrame(tick)\n }\n rafRef.current = requestAnimationFrame(tick)\n return () => {\n if (rafRef.current != null) cancelAnimationFrame(rafRef.current)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [playing, durationUs])\n\n const handleExport = async () => {\n const clip = clipRef.current\n if (clip == null) return\n const output = await run('正在编码自定义 Clip…', () => exportCustomClip(clip, durationUs))\n if (output != null) setResult(output)\n }\n\n return (\n <LabCard\n title=\"自定义 IClip\"\n description=\"纯程序化渲染的 Clip(帧计数器),不依赖任何媒体文件,证明接口可扩展。\"\n apis={['IClip.tick', 'IClip.clone', 'IClip.split', 'Combinator']}\n >\n <div className=\"overflow-hidden rounded-md border bg-stage-bg\">\n <canvas\n ref={canvasRef}\n width={640}\n height={360}\n className=\"block w-full\"\n style={{ maxHeight: 180 }}\n />\n </div>\n\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n if (!playing && timeUs >= durationUs - 1) setTimeUs(0)\n setPlaying((p) => !p)\n }}\n className=\"flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-[11px]\"\n >\n {playing ? <Square className=\"size-3\" /> : <Play className=\"size-3\" />}\n {playing ? '停止' : '预览'}\n </button>\n <span className=\"font-mono text-[10px] text-muted-foreground tabular-nums\">\n {formatTimecode(timeUs)} / {formatTimecode(durationUs)}\n </span>\n </div>\n\n <SliderRow\n label=\"时间\"\n value={timeUs}\n min={0}\n max={Math.max(1, durationUs - 1)}\n step={10_000}\n format={(v) => `${(v / 1e6).toFixed(2)}s`}\n onChange={(v) => {\n setPlaying(false)\n setTimeUs(v)\n }}\n />\n <SliderRow\n label=\"时长\"\n value={durationSec}\n min={1}\n max={20}\n step={0.5}\n format={(v) => `${v.toFixed(1)}s`}\n onChange={(v) => {\n setPlaying(false)\n setDurationSec(v)\n setTimeUs(0)\n }}\n />\n\n <button\n type=\"button\"\n disabled={busy != null}\n onClick={() => void handleExport()}\n className=\"rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50\"\n >\n 编码为 MP4\n </button>\n\n {busy != null && <Busy label={busy} />}\n {result != null && <ResultPreview blob={result} filename=\"custom-clip.mp4\" />}\n </LabCard>\n )\n}\n","import { FlaskConical } from 'lucide-react'\r\nimport { ConcatenateLab } from './ConcatenateLab'\r\nimport { RemuxLab } from './RemuxLab'\r\nimport { ChromakeyLab } from './ChromakeyLab'\r\nimport { SubtitleLab } from './SubtitleLab'\r\nimport { TrackSplitLab } from './TrackSplitLab'\r\nimport { ThumbnailLab } from './ThumbnailLab'\r\nimport { RecorderLab } from './RecorderLab'\r\nimport { CustomClipLab } from './CustomClipLab'\r\n\r\n/**\r\n * 能力实验室:每个 WebAV 高级 API 一个可独立运行的卡片。\r\n *\r\n * 与主编辑器共享同一套能力探测结果,因此这里不再重复检测。\r\n */\r\nexport function LabPage() {\r\n return (\r\n <div className=\"mx-auto max-w-7xl space-y-4 p-4\">\r\n <header className=\"space-y-1\">\r\n <div className=\"flex items-center gap-2\">\r\n <FlaskConical className=\"size-4 text-primary\" />\r\n <h1 className=\"text-sm font-semibold\">能力实验室</h1>\r\n </div>\r\n <p className=\"text-xs text-muted-foreground\">\r\n 覆盖 av-cliper / av-canvas / av-recorder 的进阶 API。每个卡片可独立运行,失败时给出可读原因。\r\n </p>\r\n </header>\r\n\r\n <div className=\"grid gap-3 md:grid-cols-2 xl:grid-cols-3\">\r\n <ConcatenateLab />\r\n <RemuxLab />\r\n <ChromakeyLab />\r\n <SubtitleLab />\r\n <TrackSplitLab />\r\n <ThumbnailLab />\r\n <RecorderLab />\r\n <CustomClipLab />\r\n </div>\r\n\r\n <footer className=\"border-t pt-3 text-[10px] leading-relaxed text-muted-foreground\">\r\n 说明:所有输出均为 MP4(H.264 + AAC 48kHz 双声道)。WebAV 生成的是 fMP4 分片,\r\n 部分播放器需「修复时长」;浏览器本地处理,文件不会上传。\r\n </footer>\r\n </div>\r\n )\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 通知统一出口。
3
+ *
4
+ * 为什么需要这一层:`sonner` 的 toast 队列是**模块级全局单例**,每个挂载的
5
+ * `<Toaster>` 都会渲染队列里的全部消息。宿主一旦自己也挂了 Toaster(常见于
6
+ * 已有的设计系统),同一条消息就会被渲染两次 —— 一条是宿主的样式,一条是
7
+ * 工作台的样式,肉眼看到就是「两个不同样式的相同提示」。
8
+ *
9
+ * 因此所有提示都必须走这里,由 `HostBridge.notify` 决定最终去向:
10
+ * 1. 宿主实现了 `notify` → 完全交给宿主(系统通知 / 自己的 toast / 日志),
11
+ * 工作台不再碰 sonner,也就不会重复;
12
+ * 2. 宿主没实现 → 退回库内的 sonner toast,保证独立使用时仍然可见。
13
+ *
14
+ * 注意:这里是「二选一」,两边不会同时收到同一条消息。
15
+ */
16
+ import type { HostBridge } from '@cut-workbench/core';
17
+ export type NotifyLevel = 'info' | 'success' | 'warning' | 'error';
18
+ export interface NotifyOptions {
19
+ /** 次要说明(多行用 \n 分隔) */
20
+ description?: string;
21
+ /** 仅对库内 toast 生效的显示时长(毫秒);宿主实现自行决定 */
22
+ duration?: number;
23
+ }
24
+ /** 一次通知的完整描述,便于宿主侧统一处理 */
25
+ export interface NotifyPayload extends NotifyOptions {
26
+ level: NotifyLevel;
27
+ title: string;
28
+ }
29
+ export declare function setDefaultBridge(bridge: HostBridge | null): void;
30
+ export declare function getDefaultBridge(): HostBridge | null;
31
+ /**
32
+ * 发一条通知。优先走宿主桥,没有宿主实现才用库内 toast。
33
+ *
34
+ * @param bridge 显式传入的宿主桥;传 `undefined` 时使用已注册的默认桥
35
+ * (见 `setDefaultBridge`),仍没有则退回库内 toast
36
+ */
37
+ export declare function notify(bridge: HostBridge | null | undefined, payload: NotifyPayload): void;
38
+ /** 便捷构造:只需要一句错误提示时用 */
39
+ export declare function notifyError(bridge: HostBridge | null | undefined, title: string, description?: string): void;
40
+ export type { HostBridge };
@@ -0,0 +1,233 @@
1
+ import { createContext, useContext, useSyncExternalStore } from "react";
2
+ import { formatDuration, usToSec } from "@cut-workbench/core";
3
+ import { clsx } from "clsx";
4
+ import { twMerge } from "tailwind-merge";
5
+ import { jsx } from "react/jsx-runtime";
6
+ import { cva } from "class-variance-authority";
7
+ import { Slot } from "radix-ui";
8
+ import { toast } from "sonner";
9
+ // src/lib/cn.ts
10
+ /**
11
+ * 合并 className:tailwind-merge 负责消解冲突,clsx 负责条件拼接。
12
+ */
13
+ function cn(...inputs) {
14
+ return twMerge(clsx(inputs));
15
+ }
16
+ // src/components/ui/badge.tsx
17
+ var badgeVariants = cva("inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", {
18
+ variants: { variant: {
19
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
20
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
21
+ destructive: "bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
22
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
23
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
24
+ link: "text-primary underline-offset-4 [a&]:hover:underline"
25
+ } },
26
+ defaultVariants: { variant: "default" }
27
+ });
28
+ function Badge({ className, variant = "default", asChild = false, ...props }) {
29
+ const Comp = asChild ? Slot.Root : "span";
30
+ return /* @__PURE__ */ jsx(Comp, {
31
+ "data-slot": "badge",
32
+ "data-variant": variant,
33
+ className: cn(badgeVariants({ variant }), className),
34
+ ...props
35
+ });
36
+ }
37
+ // src/context.tsx
38
+ /**
39
+ * 工作台自身的 Context:UI 状态(面板尺寸 / 缩放 / 对话框)+ 宿主桥。
40
+ *
41
+ * 与 `@cut-workbench/react` 的编辑器 Context 分开:编辑器是「数据与引擎」,
42
+ * 这里是「某一次挂载的布局与交互」。两者生命周期可以不同(例如宿主切换项目时
43
+ * 复用同一个编辑器、只重建 UI)。
44
+ */
45
+ var WorkbenchContext = createContext(null);
46
+ function useWorkbench() {
47
+ const value = useContext(WorkbenchContext);
48
+ if (value == null) throw new Error("[cut-workbench] 缺少 <WorkbenchProvider>:组件必须挂在工作台内部");
49
+ return value;
50
+ }
51
+ /** 订阅 UI 状态(带 selector) */
52
+ function useUi(selector) {
53
+ const { ui } = useWorkbench();
54
+ return useSyncExternalStore(ui.subscribe, () => selector(ui.getState()), () => selector(ui.getState()));
55
+ }
56
+ /** 取 UI store 本体(事件回调里读写用) */
57
+ function useUiStoreApi() {
58
+ return useWorkbench().ui;
59
+ }
60
+ /** 默认功能开关 */
61
+ var DEFAULT_FEATURES = {
62
+ mediaLibrary: true,
63
+ preview: true,
64
+ timeline: true,
65
+ inspector: true,
66
+ export: true,
67
+ canvasSettings: true,
68
+ lab: false
69
+ };
70
+ var DEFAULT_CONFIG = {
71
+ features: DEFAULT_FEATURES,
72
+ keyboard: "global",
73
+ resizable: true
74
+ };
75
+ // src/components/ui/card.tsx
76
+ function Card({ className, ...props }) {
77
+ return /* @__PURE__ */ jsx("div", {
78
+ "data-slot": "card",
79
+ className: cn("flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm", className),
80
+ ...props
81
+ });
82
+ }
83
+ function CardHeader({ className, ...props }) {
84
+ return /* @__PURE__ */ jsx("div", {
85
+ "data-slot": "card-header",
86
+ className: cn("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", className),
87
+ ...props
88
+ });
89
+ }
90
+ function CardTitle({ className, ...props }) {
91
+ return /* @__PURE__ */ jsx("div", {
92
+ "data-slot": "card-title",
93
+ className: cn("leading-none font-semibold", className),
94
+ ...props
95
+ });
96
+ }
97
+ function CardDescription({ className, ...props }) {
98
+ return /* @__PURE__ */ jsx("div", {
99
+ "data-slot": "card-description",
100
+ className: cn("text-sm text-muted-foreground", className),
101
+ ...props
102
+ });
103
+ }
104
+ function CardContent({ className, ...props }) {
105
+ return /* @__PURE__ */ jsx("div", {
106
+ "data-slot": "card-content",
107
+ className: cn("px-6", className),
108
+ ...props
109
+ });
110
+ }
111
+ // src/lib/format.ts
112
+ /** 字节数 → 人类可读 */
113
+ function formatBytes(bytes) {
114
+ if (bytes < 1024) return `${bytes} B`;
115
+ const units = [
116
+ "KB",
117
+ "MB",
118
+ "GB"
119
+ ];
120
+ let value = bytes / 1024;
121
+ let i = 0;
122
+ while (value >= 1024 && i < units.length - 1) {
123
+ value /= 1024;
124
+ i += 1;
125
+ }
126
+ return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[i]}`;
127
+ }
128
+ /** 像素尺寸 → `1920×1080` */
129
+ function formatResolution(width, height) {
130
+ if (!width || !height) return "—";
131
+ return `${width}×${height}`;
132
+ }
133
+ /** 秒数 → `12.3s` */
134
+ function formatElapsed(seconds) {
135
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
136
+ return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
137
+ }
138
+ /** 微秒 → 秒(保留一位小数,用于表单展示) */
139
+ function formatSeconds(us) {
140
+ return usToSec(us).toFixed(2);
141
+ }
142
+ /** 百分比 0~1 → `42%` */
143
+ function formatPercent(ratio) {
144
+ return `${Math.round(ratio * 100)}%`;
145
+ }
146
+ // src/lib/notify.ts
147
+ /**
148
+ * 通知统一出口。
149
+ *
150
+ * 为什么需要这一层:`sonner` 的 toast 队列是**模块级全局单例**,每个挂载的
151
+ * `<Toaster>` 都会渲染队列里的全部消息。宿主一旦自己也挂了 Toaster(常见于
152
+ * 已有的设计系统),同一条消息就会被渲染两次 —— 一条是宿主的样式,一条是
153
+ * 工作台的样式,肉眼看到就是「两个不同样式的相同提示」。
154
+ *
155
+ * 因此所有提示都必须走这里,由 `HostBridge.notify` 决定最终去向:
156
+ * 1. 宿主实现了 `notify` → 完全交给宿主(系统通知 / 自己的 toast / 日志),
157
+ * 工作台不再碰 sonner,也就不会重复;
158
+ * 2. 宿主没实现 → 退回库内的 sonner toast,保证独立使用时仍然可见。
159
+ *
160
+ * 注意:这里是「二选一」,两边不会同时收到同一条消息。
161
+ */
162
+ /**
163
+ * 把 sonner 的选项收敛成桥的签名。
164
+ *
165
+ * 宿主的 `notify` 只收 `(level, title, detail)`,所以 `duration` 之类的展示
166
+ * 细节在接管后会被丢弃 —— 这是刻意的:显示多久属于宿主决定,库不应该规定。
167
+ */
168
+ function toBridgeArgs(payload) {
169
+ const { level, title, description } = payload;
170
+ return description == null ? [level, title] : [
171
+ level,
172
+ title,
173
+ description
174
+ ];
175
+ }
176
+ function showInLibrary(payload) {
177
+ const { level, title, description, duration } = payload;
178
+ const options = {};
179
+ if (description != null) options.description = description;
180
+ if (duration != null) options.duration = duration;
181
+ const hasOptions = Object.keys(options).length > 0;
182
+ logToConsole(payload);
183
+ switch (level) {
184
+ case "success":
185
+ toast.success(title, hasOptions ? options : void 0);
186
+ break;
187
+ case "warning":
188
+ toast.warning(title, hasOptions ? options : void 0);
189
+ break;
190
+ case "error":
191
+ toast.error(title, hasOptions ? options : void 0);
192
+ break;
193
+ default: toast.info(title, hasOptions ? options : void 0);
194
+ }
195
+ }
196
+ function logToConsole(payload) {
197
+ const { level, title, description } = payload;
198
+ const line = description == null ? title : `${title} · ${description}`;
199
+ if (level === "error") console.error("[cut-workbench]", line);
200
+ else if (level === "warning") console.warn("[cut-workbench]", line);
201
+ else console.info("[cut-workbench]", line);
202
+ }
203
+ /**
204
+ * 当前挂载的默认宿主桥。
205
+ *
206
+ * 为什么需要它:`reportError` 这类**非 hook**函数拿不到 React context
207
+ * (例如 `useAvCanvas` 的初始化失败、事件回调)。由 `WorkbenchProvider`
208
+ * 挂载时注册,卸载时清空,使这些路径也能尊重宿主的 `notify` 实现。
209
+ *
210
+ * 多实例场景下这里是「最后挂载者生效」—— 提示本身不区分实例,可以接受;
211
+ * 需要精确归属的地方走 `useWorkbench().bridge`。
212
+ */
213
+ var defaultBridge = null;
214
+ function setDefaultBridge(bridge) {
215
+ defaultBridge = bridge;
216
+ }
217
+ /**
218
+ * 发一条通知。优先走宿主桥,没有宿主实现才用库内 toast。
219
+ *
220
+ * @param bridge 显式传入的宿主桥;传 `undefined` 时使用已注册的默认桥
221
+ * (见 `setDefaultBridge`),仍没有则退回库内 toast
222
+ */
223
+ function notify(bridge, payload) {
224
+ const target = bridge === void 0 ? defaultBridge : bridge;
225
+ if (target?.notify != null) {
226
+ target.notify(...toBridgeArgs(payload));
227
+ return;
228
+ }
229
+ showInLibrary(payload);
230
+ }
231
+ export { useUi as _, formatElapsed as a, Badge as b, formatSeconds as c, CardDescription as d, CardHeader as f, WorkbenchContext as g, DEFAULT_FEATURES as h, formatDuration as i, Card as l, DEFAULT_CONFIG as m, setDefaultBridge as n, formatPercent as o, CardTitle as p, formatBytes as r, formatResolution as s, notify as t, CardContent as u, useUiStoreApi as v, cn as x, useWorkbench as y };
232
+
233
+ //# sourceMappingURL=notify-CEwFQgjC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify-CEwFQgjC.js","names":[],"sources":["../src/lib/cn.ts","../src/components/ui/badge.tsx","../src/context.tsx","../src/components/ui/card.tsx","../src/lib/format.ts","../src/lib/notify.ts"],"sourcesContent":["import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\n/**\n * 合并 className:tailwind-merge 负责消解冲突,clsx 负责条件拼接。\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n","import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"@/lib/cn\"\nimport { Slot } from \"radix-ui\"\n\nconst badgeVariants = cva(\n \"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"bg-destructive text-destructive-foreground focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90\",\n outline:\n \"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n ghost: \"[a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 [a&]:hover:underline\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Badge({\n className,\n variant = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot.Root : \"span\"\n\n return (\n <Comp\n data-slot=\"badge\"\n data-variant={variant}\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport { Badge, badgeVariants }\n","/**\r\n * 工作台自身的 Context:UI 状态(面板尺寸 / 缩放 / 对话框)+ 宿主桥。\r\n *\r\n * 与 `@cut-workbench/react` 的编辑器 Context 分开:编辑器是「数据与引擎」,\r\n * 这里是「某一次挂载的布局与交互」。两者生命周期可以不同(例如宿主切换项目时\r\n * 复用同一个编辑器、只重建 UI)。\r\n */\r\n\r\nimport { createContext, useContext, useSyncExternalStore, type ReactNode } from 'react'\r\nimport type { HostBridge } from '@cut-workbench/core'\r\nimport type { UiState, UiStore } from './store/ui-store'\r\n\r\nexport interface WorkbenchTheme {\r\n /**\r\n * 宿主请求的主题:\r\n * - `light` / `dark`:固定主题;\r\n * - `inherit`:跟随宿主 `<html class=\"dark\">`(见 theme.ts)。\r\n */\r\n mode?: 'light' | 'dark' | 'inherit'\r\n /**\r\n * 实际生效的主题(`inherit` 已折算成亮/暗)。\r\n *\r\n * 消费方(例如 Sonner 的 Toaster)必须用这个值,而不是 `mode` ——\r\n * `mode` 可能是 `inherit`,那不是 Sonner 认识的主题名。\r\n */\r\n resolved?: 'light' | 'dark'\r\n}\r\n\r\nexport interface WorkbenchConfig {\r\n /** 功能开关 */\r\n features: {\r\n mediaLibrary: boolean\r\n preview: boolean\r\n timeline: boolean\r\n inspector: boolean\r\n export: boolean\r\n canvasSettings: boolean\r\n /** 能力实验室:默认关闭,属于内部验证用途 */\r\n lab: boolean\r\n }\r\n /** 键盘快捷键作用域:global 全局、scoped 仅容器聚焦时、off 关闭 */\r\n keyboard: 'global' | 'scoped' | 'off'\r\n /** 是否允许拖拽调整面板尺寸 */\r\n resizable: boolean\r\n}\r\n\r\nexport interface WorkbenchContextValue {\r\n ui: UiStore\r\n bridge: HostBridge\r\n config: WorkbenchConfig\r\n theme: Required<WorkbenchTheme>\r\n /** Radix portal 的挂载点(作用域化样式与 z-index 依赖它) */\r\n portalContainer: HTMLElement | null\r\n}\r\n\r\nexport const WorkbenchContext = createContext<WorkbenchContextValue | null>(null)\r\n\r\nexport function useWorkbench(): WorkbenchContextValue {\r\n const value = useContext(WorkbenchContext)\r\n if (value == null) {\r\n throw new Error('[cut-workbench] 缺少 <WorkbenchProvider>:组件必须挂在工作台内部')\r\n }\r\n return value\r\n}\r\n\r\n/** 订阅 UI 状态(带 selector) */\r\nexport function useUi<T>(selector: (state: UiState) => T): T {\r\n const { ui } = useWorkbench()\r\n return useSyncExternalStore(\r\n ui.subscribe,\r\n () => selector(ui.getState()),\r\n () => selector(ui.getState()),\r\n )\r\n}\r\n\r\n/** 取 UI store 本体(事件回调里读写用) */\r\nexport function useUiStoreApi(): UiStore {\r\n return useWorkbench().ui\r\n}\r\n\r\nexport interface WorkbenchProviderProps {\r\n value: WorkbenchContextValue\r\n children: ReactNode\r\n}\r\n\r\nexport function WorkbenchProvider({ value, children }: WorkbenchProviderProps) {\r\n return <WorkbenchContext.Provider value={value}>{children}</WorkbenchContext.Provider>\r\n}\r\n\r\n/** 默认功能开关 */\r\nexport const DEFAULT_FEATURES: WorkbenchConfig['features'] = {\r\n mediaLibrary: true,\r\n preview: true,\r\n timeline: true,\r\n inspector: true,\r\n export: true,\r\n canvasSettings: true,\r\n lab: false,\r\n}\r\n\r\nexport const DEFAULT_CONFIG: WorkbenchConfig = {\r\n features: DEFAULT_FEATURES,\r\n keyboard: 'global',\r\n resizable: true,\r\n}\r\n","import * as React from \"react\"\nimport { cn } from \"@/lib/cn\"\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card\"\n className={cn(\n \"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\"leading-none font-semibold\", className)}\n {...props}\n />\n )\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-description\"\n className={cn(\"text-sm text-muted-foreground\", className)}\n {...props}\n />\n )\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\n \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-content\"\n className={cn(\"px-6\", className)}\n {...props}\n />\n )\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardAction,\n CardDescription,\n CardContent,\n}\n","import { formatDuration, formatTimecode, usToSec } from '@cut-workbench/core'\n\nexport { formatDuration, formatTimecode }\n\n/** 字节数 → 人类可读 */\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`\n const units = ['KB', 'MB', 'GB']\n let value = bytes / 1024\n let i = 0\n while (value >= 1024 && i < units.length - 1) {\n value /= 1024\n i += 1\n }\n return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[i]}`\n}\n\n/** 像素尺寸 → `1920×1080` */\nexport function formatResolution(width: number, height: number): string {\n if (!width || !height) return '—'\n return `${width}×${height}`\n}\n\n/** 秒数 → `12.3s` */\nexport function formatElapsed(seconds: number): string {\n if (seconds < 60) return `${seconds.toFixed(1)}s`\n const m = Math.floor(seconds / 60)\n const s = Math.round(seconds % 60)\n return `${m}m ${s}s`\n}\n\n/** 微秒 → 秒(保留一位小数,用于表单展示) */\nexport function formatSeconds(us: number): string {\n return usToSec(us).toFixed(2)\n}\n\n/** 百分比 0~1 → `42%` */\nexport function formatPercent(ratio: number): string {\n return `${Math.round(ratio * 100)}%`\n}\n","/**\n * 通知统一出口。\n *\n * 为什么需要这一层:`sonner` 的 toast 队列是**模块级全局单例**,每个挂载的\n * `<Toaster>` 都会渲染队列里的全部消息。宿主一旦自己也挂了 Toaster(常见于\n * 已有的设计系统),同一条消息就会被渲染两次 —— 一条是宿主的样式,一条是\n * 工作台的样式,肉眼看到就是「两个不同样式的相同提示」。\n *\n * 因此所有提示都必须走这里,由 `HostBridge.notify` 决定最终去向:\n * 1. 宿主实现了 `notify` → 完全交给宿主(系统通知 / 自己的 toast / 日志),\n * 工作台不再碰 sonner,也就不会重复;\n * 2. 宿主没实现 → 退回库内的 sonner toast,保证独立使用时仍然可见。\n *\n * 注意:这里是「二选一」,两边不会同时收到同一条消息。\n */\n\nimport { toast } from 'sonner'\nimport type { HostBridge } from '@cut-workbench/core'\n\nexport type NotifyLevel = 'info' | 'success' | 'warning' | 'error'\n\nexport interface NotifyOptions {\n /** 次要说明(多行用 \\n 分隔) */\n description?: string\n /** 仅对库内 toast 生效的显示时长(毫秒);宿主实现自行决定 */\n duration?: number\n}\n\n/** 一次通知的完整描述,便于宿主侧统一处理 */\nexport interface NotifyPayload extends NotifyOptions {\n level: NotifyLevel\n title: string\n}\n\n/**\n * 把 sonner 的选项收敛成桥的签名。\n *\n * 宿主的 `notify` 只收 `(level, title, detail)`,所以 `duration` 之类的展示\n * 细节在接管后会被丢弃 —— 这是刻意的:显示多久属于宿主决定,库不应该规定。\n */\nfunction toBridgeArgs(payload: NotifyPayload): [NotifyLevel, string, string?] {\n const { level, title, description } = payload\n return description == null ? [level, title] : [level, title, description]\n}\n\nfunction showInLibrary(payload: NotifyPayload): void {\n const { level, title, description, duration } = payload\n const options: { description?: string; duration?: number } = {}\n if (description != null) options.description = description\n if (duration != null) options.duration = duration\n const hasOptions = Object.keys(options).length > 0\n\n // 同时打一份到 console:toast 会自动消失,控制台是排查问题的唯一痕迹。\n logToConsole(payload)\n\n switch (level) {\n case 'success':\n toast.success(title, hasOptions ? options : undefined)\n break\n case 'warning':\n toast.warning(title, hasOptions ? options : undefined)\n break\n case 'error':\n toast.error(title, hasOptions ? options : undefined)\n break\n default:\n toast.info(title, hasOptions ? options : undefined)\n }\n}\n\nfunction logToConsole(payload: NotifyPayload): void {\n const { level, title, description } = payload\n const line = description == null ? title : `${title} · ${description}`\n if (level === 'error') console.error('[cut-workbench]', line)\n else if (level === 'warning') console.warn('[cut-workbench]', line)\n else console.info('[cut-workbench]', line)\n}\n\n/**\n * 当前挂载的默认宿主桥。\n *\n * 为什么需要它:`reportError` 这类**非 hook**函数拿不到 React context\n * (例如 `useAvCanvas` 的初始化失败、事件回调)。由 `WorkbenchProvider`\n * 挂载时注册,卸载时清空,使这些路径也能尊重宿主的 `notify` 实现。\n *\n * 多实例场景下这里是「最后挂载者生效」—— 提示本身不区分实例,可以接受;\n * 需要精确归属的地方走 `useWorkbench().bridge`。\n */\nlet defaultBridge: HostBridge | null = null\n\nexport function setDefaultBridge(bridge: HostBridge | null): void {\n defaultBridge = bridge\n}\n\nexport function getDefaultBridge(): HostBridge | null {\n return defaultBridge\n}\n\n/**\n * 发一条通知。优先走宿主桥,没有宿主实现才用库内 toast。\n *\n * @param bridge 显式传入的宿主桥;传 `undefined` 时使用已注册的默认桥\n * (见 `setDefaultBridge`),仍没有则退回库内 toast\n */\nexport function notify(\n bridge: HostBridge | null | undefined,\n payload: NotifyPayload,\n): void {\n const target = bridge === undefined ? defaultBridge : bridge\n if (target?.notify != null) {\n target.notify(...toBridgeArgs(payload))\n return\n }\n showInLibrary(payload)\n}\n\n/** 便捷构造:只需要一句错误提示时用 */\nexport function notifyError(\n bridge: HostBridge | null | undefined,\n title: string,\n description?: string,\n): void {\n notify(bridge, { level: 'error', title, description })\n}\n\nexport type { HostBridge }\n"],"mappings":"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"}