@cut-workbench/workbench 0.1.0 → 0.2.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.
package/dist/index.cjs CHANGED
@@ -1,90 +1,76 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- // \0rolldown/runtime.js
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
- key = keys[i];
12
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
- get: ((k) => from[k]).bind(null, key),
14
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
- });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
- value: mod,
21
- enumerable: true
22
- }) : target, mod));
2
+ const require_format = require("./format--DhqHMgX.cjs");
23
3
  let react_dom_client = require("react-dom/client");
24
4
  let react = require("react");
25
- react = __toESM(react, 1);
5
+ react = require_format.__toESM(react, 1);
26
6
  let _cut_workbench_core_engine = require("@cut-workbench/core/engine");
27
7
  let _cut_workbench_core = require("@cut-workbench/core");
28
8
  let _cut_workbench_react = require("@cut-workbench/react");
29
- let clsx = require("clsx");
30
- let tailwind_merge = require("tailwind-merge");
31
9
  let react_jsx_runtime = require("react/jsx-runtime");
32
10
  let lucide_react = require("lucide-react");
33
11
  let class_variance_authority = require("class-variance-authority");
34
12
  let radix_ui = require("radix-ui");
35
13
  let sonner = require("sonner");
36
14
  let react_dropzone = require("react-dropzone");
37
- let _cut_workbench_core_lab = require("@cut-workbench/core/lab");
38
15
  let zustand_vanilla = require("zustand/vanilla");
39
- // src/lib/cn.ts
40
- /**
41
- * 合并 className:tailwind-merge 负责消解冲突,clsx 负责条件拼接。
42
- */
43
- function cn(...inputs) {
44
- return (0, tailwind_merge.twMerge)((0, clsx.clsx)(inputs));
45
- }
46
16
  // src/components/layout/AppShell.tsx
47
17
  /**
48
- * 三栏 + 底部时间轴骨架。
18
+ * 工作台骨架:顶栏 + 上半区(素材 / 预览 / 属性)+ 底部通栏时间轴。
19
+ *
20
+ * ```
21
+ * ┌───────────────────────────────────────────┐
22
+ * │ 顶栏 │
23
+ * ├──────────┬──────────────┬─────────────────┤
24
+ * │ 素材库 │ 预览 │ 属性 │ 上半区
25
+ * ├──────────┴──────────────┴─────────────────┤
26
+ * │ 时间轴(横跨整行) │ 下栏
27
+ * └───────────────────────────────────────────┘
28
+ * ```
29
+ *
30
+ * 时间轴必须是**通栏**的:它曾被塞进中间列,于是左右面板一拉宽就把时间轴
31
+ * 挤窄,工具条上的按钮直接溢出到属性面板上(实测右栏拉宽时溢出 203px)。
32
+ * 与此同时,几乎所有 NLE 都是「轨道占满底部一条」,通栏也更符合预期。
49
33
  *
50
- * 布局用 inline style 的像素宽度做「受控」尺寸,配合 PanelResizer 的拖拽回写;
51
- * 这样不会因为 tailwind 的动态 class 被 purge 掉而失效。
34
+ * 尺寸一律用 inline style 的像素值(受控),配合 PanelResizer 的拖拽回写;
35
+ * 不用 tailwind 动态 class,避免被 purge 掉导致尺寸失效。
52
36
  */
53
37
  function AppShell({ topBar, left, center, right, timeline, leftWidth, rightWidth, timelineHeight, leftResizer, rightResizer, timelineResizer, className }) {
54
38
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55
- className: cn("flex h-full min-h-0 flex-col bg-background", className),
56
- children: [topBar, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57
- className: "flex min-h-0 flex-1",
58
- children: [
59
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
60
- className: "flex min-h-0 shrink-0 flex-col border-r bg-card/40",
61
- style: { width: leftWidth },
62
- children: left
63
- }),
64
- leftResizer,
65
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
66
- className: "flex min-w-0 flex-1 flex-col",
67
- children: [
68
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
69
- className: "flex min-h-0 flex-1 flex-col",
70
- children: center
71
- }),
72
- timelineResizer,
73
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
74
- className: "flex min-h-0 shrink-0 flex-col border-t bg-timeline-bg",
75
- style: { height: timelineHeight },
76
- children: timeline
77
- })
78
- ]
79
- }),
80
- rightResizer,
81
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
82
- className: "flex min-h-0 shrink-0 flex-col border-l bg-card/40",
83
- style: { width: rightWidth },
84
- children: right
85
- })
86
- ]
87
- })]
39
+ className: require_format.cn("flex h-full min-h-0 flex-col bg-background", className),
40
+ children: [
41
+ topBar,
42
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
43
+ className: "flex min-h-0 flex-1 overflow-x-hidden overflow-y-auto",
44
+ children: [
45
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
46
+ className: "flex min-h-[220px] shrink-0 flex-col overflow-hidden border-r bg-card/40",
47
+ style: { width: leftWidth },
48
+ "data-panel": "media",
49
+ children: left
50
+ }),
51
+ leftResizer,
52
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("main", {
53
+ className: "flex min-h-[220px] min-w-0 flex-1 flex-col overflow-hidden",
54
+ "data-panel": "preview",
55
+ children: center
56
+ }),
57
+ rightResizer,
58
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
59
+ className: "flex min-h-[220px] shrink-0 flex-col overflow-hidden border-l bg-card/40",
60
+ style: { width: rightWidth },
61
+ "data-panel": "inspector",
62
+ children: right
63
+ })
64
+ ]
65
+ }),
66
+ timelineResizer,
67
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
68
+ className: "flex min-h-0 shrink-0 flex-col overflow-hidden border-t bg-timeline-bg",
69
+ style: { height: timelineHeight },
70
+ "data-panel": "timeline",
71
+ children: timeline
72
+ })
73
+ ]
88
74
  });
89
75
  }
90
76
  // src/components/ui/button.tsx
@@ -120,7 +106,7 @@ function Button({ className, variant = "default", size = "default", asChild = fa
120
106
  "data-slot": "button",
121
107
  "data-variant": variant,
122
108
  "data-size": size,
123
- className: cn(buttonVariants({
109
+ className: require_format.cn(buttonVariants({
124
110
  variant,
125
111
  size,
126
112
  className
@@ -134,7 +120,7 @@ function Separator({ className, orientation = "horizontal", decorative = true, .
134
120
  "data-slot": "separator",
135
121
  decorative,
136
122
  orientation,
137
- className: cn("shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", className),
123
+ className: require_format.cn("shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", className),
138
124
  ...props
139
125
  });
140
126
  }
@@ -213,33 +199,12 @@ function TooltipContent({ className, sideOffset = 0, children, ...props }) {
213
199
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Tooltip.Content, {
214
200
  "data-slot": "tooltip-content",
215
201
  sideOffset,
216
- className: cn("z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className),
202
+ className: require_format.cn("z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className),
217
203
  ...props,
218
204
  children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tooltip.Arrow, { className: "z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" })]
219
205
  })
220
206
  });
221
207
  }
222
- // src/components/ui/badge.tsx
223
- var badgeVariants = (0, class_variance_authority.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", {
224
- variants: { variant: {
225
- default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
226
- secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
227
- 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",
228
- outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
229
- ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
230
- link: "text-primary underline-offset-4 [a&]:hover:underline"
231
- } },
232
- defaultVariants: { variant: "default" }
233
- });
234
- function Badge({ className, variant = "default", asChild = false, ...props }) {
235
- const Comp = asChild ? radix_ui.Slot.Root : "span";
236
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
237
- "data-slot": "badge",
238
- "data-variant": variant,
239
- className: cn(badgeVariants({ variant }), className),
240
- ...props
241
- });
242
- }
243
208
  // src/components/ui/popover.tsx
244
209
  function Popover({ ...props }) {
245
210
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Popover.Root, {
@@ -261,7 +226,7 @@ function PopoverContent({ className, align = "center", sideOffset = 4, ...props
261
226
  "data-slot": "popover-content",
262
227
  align,
263
228
  sideOffset,
264
- className: cn("z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
229
+ className: require_format.cn("z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
265
230
  ...props
266
231
  })
267
232
  });
@@ -272,7 +237,7 @@ function CapabilityBadge({ info }) {
272
237
  const { status, report, summary } = info;
273
238
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Popover, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverTrigger, {
274
239
  asChild: true,
275
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Badge, {
240
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_format.Badge, {
276
241
  variant: "outline",
277
242
  className: "cursor-pointer gap-1 text-[11px] font-normal select-none",
278
243
  children: [status === "checking" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }) : status === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CheckCircle2, { className: "size-3 text-success" }) : status === "degraded" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertTriangle, { className: "size-3 text-warning" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.XCircle, { className: "size-3 text-destructive" }), summary]
@@ -387,10 +352,17 @@ var DEFAULT_CONFIG = {
387
352
  * - 品牌区可替换(`brand`),不强制显示项目名;
388
353
  * - 保存/打开是可选回调,宿主不提供就不渲染按钮;
389
354
  * - 「视图切换(编辑器/实验室)」只在开启 lab 特性时显示。
355
+ *
356
+ * 可自定义的层次(由浅到深):
357
+ * 1. **文案**:`labels` 覆盖工程名占位、能力徽标等;
358
+ * 2. **局部**:`brand` / `title` / `status` / `actions` / `showCapabilityBadge`
359
+ * 逐块替换;
360
+ * 3. **整条**:`slots.topBar`(传 `null` 表示完全不要顶栏)。
390
361
  */
391
362
  /** 顶栏:品牌 / 工程名 / 撤销重做 / 保存 / 导出 / 能力徽标 / 视图切换 */
392
- function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject, brand, showLab = false }) {
363
+ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject, brand, title, status, actions, showCapabilityBadge = true, showLab = false, editableTitle = false, labels }) {
393
364
  const projectName = (0, _cut_workbench_react.useEditor)((s) => s.project.name);
365
+ const renameProject = (0, _cut_workbench_react.useEditor)((s) => s.renameProject);
394
366
  const dirty = (0, _cut_workbench_react.useEditor)((s) => s.dirty);
395
367
  const undo = (0, _cut_workbench_react.useEditor)((s) => s.undo);
396
368
  const redo = (0, _cut_workbench_react.useEditor)((s) => s.redo);
@@ -402,44 +374,49 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
402
374
  const clipCount = (0, _cut_workbench_react.useEditor)((s) => s.project.clips.length);
403
375
  const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
404
376
  const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
405
- const branding = brand === null ? null : brand ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
406
- className: "flex items-center gap-2 pr-2",
407
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
408
- className: "flex size-7 items-center justify-center rounded-md bg-primary text-primary-foreground",
409
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Clapperboard, { className: "size-4" })
410
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
411
- className: "text-sm font-semibold tracking-tight",
412
- children: "Cut Workbench"
413
- })]
377
+ const text = {
378
+ untitledProject: "未命名工程",
379
+ emptyProject: "空工程",
380
+ export: "导出",
381
+ canvasSettings: "画布设置",
382
+ showLab: "能力实验室",
383
+ backToEditor: "返回编辑器",
384
+ save: "保存工程",
385
+ open: "加载工程",
386
+ undo: "撤销",
387
+ redo: "重做",
388
+ ...labels
389
+ };
390
+ const branding = brand == null ? null : brand;
391
+ const projectTitle = title === null ? null : title ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectTitle, {
392
+ name: projectName,
393
+ dirty,
394
+ placeholder: text.untitledProject,
395
+ editable: editableTitle,
396
+ onRename: renameProject
397
+ });
398
+ const statusNode = status === null ? null : status ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
399
+ className: "hidden text-xs text-muted-foreground sm:inline",
400
+ children: [
401
+ clipCount > 0 ? `${clipCount} 个片段` : text.emptyProject,
402
+ " ·",
403
+ " ",
404
+ (0, _cut_workbench_core.ratioLabel)(projectWidth, projectHeight)
405
+ ]
414
406
  });
415
407
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
416
- className: "flex h-12 shrink-0 items-center gap-2 border-b bg-card px-3",
408
+ className: "flex h-12 shrink-0 items-center gap-2 overflow-hidden border-b bg-card px-3",
409
+ "data-workbench-toolbar": true,
417
410
  children: [
418
411
  branding,
419
- branding != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
412
+ branding != null && projectTitle != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
420
413
  orientation: "vertical",
421
414
  className: "h-5"
422
415
  }),
416
+ projectTitle,
417
+ statusNode,
423
418
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
424
- className: "flex min-w-0 items-center gap-1.5",
425
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
426
- className: "truncate text-sm font-medium",
427
- children: projectName
428
- }), dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
429
- className: "size-1.5 rounded-full bg-primary",
430
- title: "未保存"
431
- })]
432
- }),
433
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
434
- className: "hidden text-xs text-muted-foreground sm:inline",
435
- children: [
436
- clipCount > 0 ? `${clipCount} 个片段` : "空工程",
437
- " · ",
438
- (0, _cut_workbench_core.ratioLabel)(projectWidth, projectHeight)
439
- ]
440
- }),
441
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
442
- className: "ml-auto flex items-center gap-1",
419
+ className: "ml-auto flex shrink-0 items-center gap-1",
443
420
  children: [
444
421
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
445
422
  asChild: true,
@@ -448,10 +425,10 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
448
425
  size: "icon-sm",
449
426
  onClick: undo,
450
427
  disabled: !canUndo,
451
- "aria-label": "撤销",
428
+ "aria-label": text.undo,
452
429
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Undo2, { className: "size-4" })
453
430
  })
454
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "撤销 (Ctrl+Z)" })] }),
431
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipContent, { children: [text.undo, " (Ctrl+Z)"] })] }),
455
432
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
456
433
  asChild: true,
457
434
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
@@ -459,10 +436,10 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
459
436
  size: "icon-sm",
460
437
  onClick: redo,
461
438
  disabled: !canRedo,
462
- "aria-label": "重做",
439
+ "aria-label": text.redo,
463
440
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Redo2, { className: "size-4" })
464
441
  })
465
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "重做 (Ctrl+Shift+Z)" })] }),
442
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipContent, { children: [text.redo, " (Ctrl+Shift+Z)"] })] }),
466
443
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
467
444
  orientation: "vertical",
468
445
  className: "mx-1 h-5"
@@ -473,29 +450,29 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
473
450
  variant: "ghost",
474
451
  size: "icon-sm",
475
452
  onClick: onOpenProject,
476
- "aria-label": "加载工程",
453
+ "aria-label": text.open,
477
454
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FolderOpen, { className: "size-4" })
478
455
  })
479
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "加载已保存工程" })] }),
456
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: text.open })] }),
480
457
  onSaveProject != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
481
458
  asChild: true,
482
459
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
483
460
  variant: "ghost",
484
461
  size: "icon-sm",
485
462
  onClick: onSaveProject,
486
- "aria-label": "保存工程",
463
+ "aria-label": text.save,
487
464
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Save, { className: "size-4" })
488
465
  })
489
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "保存工程到本地" })] }),
466
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: text.save })] }),
490
467
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
491
468
  variant: "ghost",
492
469
  size: "sm",
493
470
  className: "gap-1.5",
494
- "aria-label": "画布设置",
471
+ "aria-label": text.canvasSettings,
495
472
  onClick: () => openDialog("projectSettings"),
496
473
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Monitor, { className: "size-4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
497
474
  className: "hidden md:inline",
498
- children: "画布设置"
475
+ children: text.canvasSettings
499
476
  })]
500
477
  }),
501
478
  showLab && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
@@ -505,21 +482,67 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
505
482
  onClick: () => setRoute(route === "lab" ? "editor" : "lab"),
506
483
  children: [route === "lab" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Video, { className: "size-4" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FlaskConical, { className: "size-4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
507
484
  className: "hidden md:inline",
508
- children: route === "lab" ? "返回编辑器" : "能力实验室"
485
+ children: route === "lab" ? text.backToEditor : text.showLab
509
486
  })]
510
487
  }),
511
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityBadge, { info: capability }),
488
+ showCapabilityBadge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityBadge, { info: capability }),
489
+ actions,
512
490
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
513
491
  size: "sm",
514
492
  className: "ml-1 gap-1.5",
515
493
  onClick: onExport,
516
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "size-4" }), "导出"]
494
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "size-4" }), text.export]
517
495
  })
518
496
  ]
519
497
  })
520
498
  ]
521
499
  });
522
500
  }
501
+ /**
502
+ * 工程名展示(可选双击重命名)。
503
+ *
504
+ * 重命名走编辑器 store 的 `renameProject`,因此会进撤销栈、并标记 dirty ——
505
+ * 直接改 DOM 或本地 state 会让「保存了什么」和「看到什么」对不上。
506
+ */
507
+ function ProjectTitle({ name, dirty, placeholder, editable, onRename }) {
508
+ const [editing, setEditing] = (0, react.useState)(false);
509
+ const [draft, setDraft] = (0, react.useState)(name);
510
+ const commit = () => {
511
+ setEditing(false);
512
+ const next = draft.trim();
513
+ if (next.length > 0 && next !== name) onRename(next);
514
+ };
515
+ if (editing) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
516
+ autoFocus: true,
517
+ value: draft,
518
+ onChange: (e) => setDraft(e.target.value),
519
+ onBlur: commit,
520
+ onKeyDown: (e) => {
521
+ if (e.key === "Enter") commit();
522
+ if (e.key === "Escape") {
523
+ setDraft(name);
524
+ setEditing(false);
525
+ }
526
+ },
527
+ className: "h-7 w-48 min-w-0 rounded border bg-background px-1.5 text-sm outline-none focus:ring-1 focus:ring-ring",
528
+ "aria-label": "工程名称"
529
+ });
530
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
531
+ className: "flex min-w-0 items-center gap-1.5",
532
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
533
+ className: require_format.cn("truncate text-sm font-medium", editable && "cursor-text"),
534
+ title: editable ? "双击重命名" : name.length > 0 ? name : placeholder,
535
+ onDoubleClick: editable ? () => {
536
+ setDraft(name.length > 0 ? name : "");
537
+ setEditing(true);
538
+ } : void 0,
539
+ children: name.length > 0 ? name : placeholder
540
+ }), dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
541
+ className: "size-1.5 shrink-0 rounded-full bg-primary",
542
+ title: "未保存"
543
+ })]
544
+ });
545
+ }
523
546
  // src/components/layout/use-panel-resize.ts
524
547
  /**
525
548
  * 面板拖拽调整。内部用 rAF 节流,松手才把最终值写回 store(避免每帧重渲染整棵树)。
@@ -598,44 +621,8 @@ function PanelResizer({ orientation, value, min, max, invert, onChange, classNam
598
621
  "aria-orientation": isVertical ? "vertical" : "horizontal",
599
622
  "aria-label": label,
600
623
  onPointerDown,
601
- className: cn("group relative z-20 shrink-0 touch-none transition-colors", isVertical ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize", dragging ? "bg-primary/60" : "bg-border hover:bg-primary/40", className),
602
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: cn("absolute", isVertical ? "-left-1 -right-1 inset-y-0" : "-top-1 -bottom-1 inset-x-0") })
603
- });
604
- }
605
- // src/components/ui/card.tsx
606
- function Card({ className, ...props }) {
607
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
608
- "data-slot": "card",
609
- className: cn("flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm", className),
610
- ...props
611
- });
612
- }
613
- function CardHeader({ className, ...props }) {
614
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
615
- "data-slot": "card-header",
616
- 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),
617
- ...props
618
- });
619
- }
620
- function CardTitle({ className, ...props }) {
621
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
622
- "data-slot": "card-title",
623
- className: cn("leading-none font-semibold", className),
624
- ...props
625
- });
626
- }
627
- function CardDescription({ className, ...props }) {
628
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
629
- "data-slot": "card-description",
630
- className: cn("text-sm text-muted-foreground", className),
631
- ...props
632
- });
633
- }
634
- function CardContent({ className, ...props }) {
635
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
636
- "data-slot": "card-content",
637
- className: cn("px-6", className),
638
- ...props
624
+ className: require_format.cn("group relative z-20 shrink-0 touch-none transition-colors", isVertical ? "w-1 cursor-col-resize" : "h-1 cursor-row-resize", dragging ? "bg-primary/60" : "bg-border hover:bg-primary/40", className),
625
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: require_format.cn("absolute", isVertical ? "-left-1 -right-1 inset-y-0" : "-top-1 -bottom-1 inset-x-0") })
639
626
  });
640
627
  }
641
628
  // src/components/common/LoadingOverlay.tsx
@@ -643,7 +630,7 @@ function CardContent({ className, ...props }) {
643
630
  function LoadingOverlay({ visible, title = "处理中…", description, progress, className, fullscreen, children }) {
644
631
  if (!visible) return null;
645
632
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
646
- className: cn("z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm", fullscreen ? "fixed inset-0" : "absolute inset-0", className),
633
+ className: require_format.cn("z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm", fullscreen ? "fixed inset-0" : "absolute inset-0", className),
647
634
  role: "status",
648
635
  "aria-live": "polite",
649
636
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -705,7 +692,12 @@ var CHECKS = [
705
692
  hint: "绿幕抠像(可选)"
706
693
  }
707
694
  ];
708
- /** 不满足运行条件时的阻断页:逐项说明缺失能力,不做静默降级 */
695
+ /**
696
+ * 不满足运行条件时的阻断页:逐项说明缺失能力,不做静默降级。
697
+ *
698
+ * 「依赖没装」与「浏览器不支持」要分开呈现 —— 前者是宿主的构建问题(可执行
699
+ * 安装命令解决),后者只能换浏览器。混在一起会让人对着一条装包报错去升浏览器。
700
+ */
709
701
  function UnsupportedBrowser({ report, loading, error }) {
710
702
  if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
711
703
  className: "relative flex min-h-screen items-center justify-center bg-background",
@@ -715,23 +707,46 @@ function UnsupportedBrowser({ report, loading, error }) {
715
707
  fullscreen: false
716
708
  })
717
709
  });
710
+ const missing = report?.missingDependencies ?? [];
711
+ const dependenciesMissing = missing.length > 0;
718
712
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
719
713
  className: "flex min-h-screen items-center justify-center bg-background p-6",
720
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
714
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_format.Card, {
721
715
  className: "w-full max-w-2xl",
722
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
716
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_format.CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
723
717
  className: "flex items-center gap-2 text-destructive",
724
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertOctagon, { className: "size-5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardTitle, {
718
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertOctagon, { className: "size-5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_format.CardTitle, {
725
719
  className: "text-base",
726
- children: "当前浏览器无法运行本应用"
720
+ children: dependenciesMissing ? "缺少引擎依赖" : "当前浏览器无法运行本应用"
727
721
  })]
728
722
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
729
723
  className: "text-sm text-muted-foreground",
730
- children: "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
731
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardContent, {
724
+ children: dependenciesMissing ? "工作台的预览与导出依赖 WebAV(@webav/av-cliper、@webav/av-canvas)。当前项目里没有解析到它们。" : "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
725
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_format.CardContent, {
732
726
  className: "space-y-4",
733
727
  children: [
734
- report != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
728
+ dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
729
+ className: "space-y-2 rounded-md border border-destructive/40 bg-destructive/5 p-3",
730
+ children: [
731
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
732
+ className: "text-xs font-medium text-destructive",
733
+ children: ["未解析到:", missing.join("、")]
734
+ }),
735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
736
+ className: "text-xs text-muted-foreground",
737
+ children: "在宿主项目里执行:"
738
+ }),
739
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
740
+ className: "overflow-x-auto rounded bg-muted px-3 py-2 text-xs",
741
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: _cut_workbench_core_engine.ENGINE_DEPENDENCIES_INSTALL_COMMAND })
742
+ }),
743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
744
+ className: "text-xs text-muted-foreground",
745
+ children: "安装后需要重新跑一次构建 —— 缺失依赖只在构建期解析,浏览器里刷新页面没用。"
746
+ })
747
+ ]
748
+ }),
749
+ report != null && !dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
735
750
  className: "grid gap-2 sm:grid-cols-2",
736
751
  children: CHECKS.map((check) => {
737
752
  const value = report[check.key];
@@ -801,7 +816,7 @@ var Toaster = (props) => {
801
816
  function Textarea({ className, ...props }) {
802
817
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
803
818
  "data-slot": "textarea",
804
- className: cn("flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40", className),
819
+ className: require_format.cn("flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40", className),
805
820
  ...props
806
821
  });
807
822
  }
@@ -809,7 +824,7 @@ function Textarea({ className, ...props }) {
809
824
  function Label({ className, ...props }) {
810
825
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Label.Root, {
811
826
  "data-slot": "label",
812
- className: cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className),
827
+ className: require_format.cn("flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", className),
813
828
  ...props
814
829
  });
815
830
  }
@@ -817,7 +832,7 @@ function Label({ className, ...props }) {
817
832
  function ScrollArea({ className, children, ...props }) {
818
833
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.ScrollArea.Root, {
819
834
  "data-slot": "scroll-area",
820
- className: cn("relative", className),
835
+ className: require_format.cn("relative", className),
821
836
  ...props,
822
837
  children: [
823
838
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.Viewport, {
@@ -834,7 +849,7 @@ function ScrollBar({ className, orientation = "vertical", ...props }) {
834
849
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaScrollbar, {
835
850
  "data-slot": "scroll-area-scrollbar",
836
851
  orientation,
837
- className: cn("flex touch-none p-px transition-colors select-none", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent", className),
852
+ className: require_format.cn("flex touch-none p-px transition-colors select-none", orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent", className),
838
853
  ...props,
839
854
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaThumb, {
840
855
  "data-slot": "scroll-area-thumb",
@@ -873,7 +888,7 @@ function DialogPortal({ ...props }) {
873
888
  function DialogOverlay({ className, ...props }) {
874
889
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Overlay, {
875
890
  "data-slot": "dialog-overlay",
876
- className: cn("fixed inset-0 z-50 bg-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className),
891
+ className: require_format.cn("fixed inset-0 z-50 bg-overlay data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className),
877
892
  ...props
878
893
  });
879
894
  }
@@ -882,7 +897,7 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
882
897
  "data-slot": "dialog-portal",
883
898
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogOverlay, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Content, {
884
899
  "data-slot": "dialog-content",
885
- className: cn("fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", className),
900
+ className: require_format.cn("fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", className),
886
901
  ...props,
887
902
  children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Close, {
888
903
  "data-slot": "dialog-close",
@@ -898,14 +913,14 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
898
913
  function DialogHeader({ className, ...props }) {
899
914
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
900
915
  "data-slot": "dialog-header",
901
- className: cn("flex flex-col gap-2 text-center sm:text-left", className),
916
+ className: require_format.cn("flex flex-col gap-2 text-center sm:text-left", className),
902
917
  ...props
903
918
  });
904
919
  }
905
920
  function DialogFooter({ className, showCloseButton = false, children, ...props }) {
906
921
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
907
922
  "data-slot": "dialog-footer",
908
- className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
923
+ className: require_format.cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
909
924
  ...props,
910
925
  children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Close, {
911
926
  asChild: true,
@@ -919,14 +934,14 @@ function DialogFooter({ className, showCloseButton = false, children, ...props }
919
934
  function DialogTitle({ className, ...props }) {
920
935
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Title, {
921
936
  "data-slot": "dialog-title",
922
- className: cn("text-lg leading-none font-semibold", className),
937
+ className: require_format.cn("text-lg leading-none font-semibold", className),
923
938
  ...props
924
939
  });
925
940
  }
926
941
  function DialogDescription({ className, ...props }) {
927
942
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Description, {
928
943
  "data-slot": "dialog-description",
929
- className: cn("text-sm text-muted-foreground", className),
944
+ className: require_format.cn("text-sm text-muted-foreground", className),
930
945
  ...props
931
946
  });
932
947
  }
@@ -955,7 +970,7 @@ function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
955
970
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Content, {
956
971
  "data-slot": "dropdown-menu-content",
957
972
  sideOffset,
958
- className: cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
973
+ className: require_format.cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
959
974
  ...props
960
975
  }) });
961
976
  }
@@ -964,7 +979,7 @@ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
964
979
  "data-slot": "dropdown-menu-item",
965
980
  "data-inset": inset,
966
981
  "data-variant": variant,
967
- className: cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
982
+ className: require_format.cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
968
983
  ...props
969
984
  });
970
985
  }
@@ -972,14 +987,14 @@ function DropdownMenuLabel({ className, inset, ...props }) {
972
987
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Label, {
973
988
  "data-slot": "dropdown-menu-label",
974
989
  "data-inset": inset,
975
- className: cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
990
+ className: require_format.cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
976
991
  ...props
977
992
  });
978
993
  }
979
994
  function DropdownMenuSeparator({ className, ...props }) {
980
995
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Separator, {
981
996
  "data-slot": "dropdown-menu-separator",
982
- className: cn("-mx-1 my-1 h-px bg-border", className),
997
+ className: require_format.cn("-mx-1 my-1 h-px bg-border", className),
983
998
  ...props
984
999
  });
985
1000
  }
@@ -987,7 +1002,7 @@ function DropdownMenuSeparator({ className, ...props }) {
987
1002
  /** 统一空态:图标 + 标题 + 说明 + 可选操作 */
988
1003
  function EmptyState({ icon: Icon, title, description, action, className, compact }) {
989
1004
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
990
- className: cn("flex flex-col items-center justify-center gap-2 text-center", compact ? "px-4 py-6" : "px-6 py-12", className),
1005
+ className: require_format.cn("flex flex-col items-center justify-center gap-2 text-center", compact ? "px-4 py-6" : "px-6 py-12", className),
991
1006
  children: [
992
1007
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
993
1008
  className: "flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground",
@@ -1005,41 +1020,6 @@ function EmptyState({ icon: Icon, title, description, action, className, compact
1005
1020
  ]
1006
1021
  });
1007
1022
  }
1008
- // src/lib/format.ts
1009
- /** 字节数 → 人类可读 */
1010
- function formatBytes(bytes) {
1011
- if (bytes < 1024) return `${bytes} B`;
1012
- const units = [
1013
- "KB",
1014
- "MB",
1015
- "GB"
1016
- ];
1017
- let value = bytes / 1024;
1018
- let i = 0;
1019
- while (value >= 1024 && i < units.length - 1) {
1020
- value /= 1024;
1021
- i += 1;
1022
- }
1023
- return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[i]}`;
1024
- }
1025
- /** 像素尺寸 → `1920×1080` */
1026
- function formatResolution(width, height) {
1027
- if (!width || !height) return "—";
1028
- return `${width}×${height}`;
1029
- }
1030
- /** 秒数 → `12.3s` */
1031
- function formatElapsed(seconds) {
1032
- if (seconds < 60) return `${seconds.toFixed(1)}s`;
1033
- return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
1034
- }
1035
- /** 微秒 → 秒(保留一位小数,用于表单展示) */
1036
- function formatSeconds(us) {
1037
- return (0, _cut_workbench_core.usToSec)(us).toFixed(2);
1038
- }
1039
- /** 百分比 0~1 → `42%` */
1040
- function formatPercent(ratio) {
1041
- return `${Math.round(ratio * 100)}%`;
1042
- }
1043
1023
  // src/features/media-library/components/DropZone.tsx
1044
1024
  var ACCEPT = {
1045
1025
  "video/mp4": [".mp4", ".m4v"],
@@ -1080,12 +1060,12 @@ function DropZone({ onFiles, disabled, className, compact }) {
1080
1060
  });
1081
1061
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1082
1062
  ...getRootProps(),
1083
- className: cn("group relative flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed text-center transition-colors", compact ? "px-3 py-2.5" : "px-4 py-8", isDragActive ? "border-primary bg-primary/10" : "border-border hover:border-primary/60 hover:bg-muted/40", disabled && "pointer-events-none opacity-50", className),
1063
+ className: require_format.cn("group relative flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-lg border border-dashed text-center transition-colors", compact ? "px-3 py-2.5" : "px-4 py-8", isDragActive ? "border-primary bg-primary/10" : "border-border hover:border-primary/60 hover:bg-muted/40", disabled && "pointer-events-none opacity-50", className),
1084
1064
  children: [
1085
1065
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { ...getInputProps() }),
1086
1066
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1087
1067
  className: "flex items-center gap-2 text-sm font-medium",
1088
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: cn("text-muted-foreground", compact ? "size-3.5" : "size-4") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: compact ? "导入素材" : isDragActive ? "松开以导入" : "拖入文件,或点击选择" })]
1068
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: require_format.cn("text-muted-foreground", compact ? "size-3.5" : "size-4") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: compact ? "导入素材" : isDragActive ? "松开以导入" : "拖入文件,或点击选择" })]
1089
1069
  }),
1090
1070
  !compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1091
1071
  className: "text-xs text-muted-foreground",
@@ -1107,7 +1087,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
1107
1087
  function Skeleton({ className, ...props }) {
1108
1088
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1109
1089
  "data-slot": "skeleton",
1110
- className: cn("animate-pulse rounded-md bg-accent", className),
1090
+ className: require_format.cn("animate-pulse rounded-md bg-accent", className),
1111
1091
  ...props
1112
1092
  });
1113
1093
  }
@@ -1156,7 +1136,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
1156
1136
  onKeyDown: (e) => {
1157
1137
  if (e.key === "Enter") onAddToTimeline();
1158
1138
  },
1159
- className: cn("group relative flex cursor-pointer flex-col gap-1.5 rounded-lg border p-1.5 text-left transition-colors outline-none", missing ? "border-warning/50 bg-warning/5" : selected ? "border-primary bg-primary/10 ring-1 ring-primary/40" : "border-border bg-card hover:border-primary/40 hover:bg-muted/40 focus-visible:border-primary"),
1139
+ className: require_format.cn("group relative flex cursor-pointer flex-col gap-1.5 rounded-lg border p-1.5 text-left transition-colors outline-none", missing ? "border-warning/50 bg-warning/5" : selected ? "border-primary bg-primary/10 ring-1 ring-primary/40" : "border-border bg-card hover:border-primary/40 hover:bg-muted/40 focus-visible:border-primary"),
1160
1140
  children: [
1161
1141
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1162
1142
  className: "relative aspect-video w-full overflow-hidden rounded-md bg-muted",
@@ -1177,7 +1157,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
1177
1157
  opacity: .9
1178
1158
  })
1179
1159
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1180
- className: cn("flex size-full items-center justify-center", kind.tone),
1160
+ className: require_format.cn("flex size-full items-center justify-center", kind.tone),
1181
1161
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-7" })
1182
1162
  }),
1183
1163
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -1224,7 +1204,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
1224
1204
  children: asset.name
1225
1205
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1226
1206
  className: "truncate text-[10px] text-muted-foreground",
1227
- children: [asset.kind === "audio" ? `${formatBytes(asset.size)} · 48kHz 双声道` : asset.kind === "text" || asset.kind === "subtitle" ? `${formatResolution(asset.width, asset.height)}` : `${formatResolution(asset.width, asset.height)} · ${formatBytes(asset.size)}`, asset.hasAudio ? " · 含音轨" : ""]
1207
+ children: [asset.kind === "audio" ? `${require_format.formatBytes(asset.size)} · 48kHz 双声道` : asset.kind === "text" || asset.kind === "subtitle" ? `${require_format.formatResolution(asset.width, asset.height)}` : `${require_format.formatResolution(asset.width, asset.height)} · ${require_format.formatBytes(asset.size)}`, asset.hasAudio ? " · 含音轨" : ""]
1228
1208
  })]
1229
1209
  }),
1230
1210
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
@@ -1266,7 +1246,7 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
1266
1246
  className: "grid grid-cols-2 gap-2.5 p-2.5",
1267
1247
  children: [
1268
1248
  progress.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1269
- className: cn("flex flex-col gap-1.5 rounded-lg border border-dashed p-1.5", item.error != null && "border-destructive/50 bg-destructive/5"),
1249
+ className: require_format.cn("flex flex-col gap-1.5 rounded-lg border border-dashed p-1.5", item.error != null && "border-destructive/50 bg-destructive/5"),
1270
1250
  children: [
1271
1251
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, { className: "aspect-video w-full rounded-md" }),
1272
1252
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1505,11 +1485,11 @@ function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
1505
1485
  }),
1506
1486
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
1507
1487
  label: "分辨率",
1508
- value: formatResolution(metaAsset.width, metaAsset.height)
1488
+ value: require_format.formatResolution(metaAsset.width, metaAsset.height)
1509
1489
  }),
1510
1490
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
1511
1491
  label: "大小",
1512
- value: metaAsset.kind === "text" ? "—" : formatBytes(metaAsset.size)
1492
+ value: metaAsset.kind === "text" ? "—" : require_format.formatBytes(metaAsset.size)
1513
1493
  }),
1514
1494
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
1515
1495
  label: "音轨",
@@ -2305,7 +2285,7 @@ function PreviewStage({ attachRef, width, height, emptyHint, onCapture, onFullsc
2305
2285
  const scaleY = height > 0 ? box.h / height : 1;
2306
2286
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2307
2287
  ref: outerRef,
2308
- className: cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
2288
+ className: require_format.cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
2309
2289
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2310
2290
  className: "relative shrink-0 overflow-hidden rounded-md bg-black shadow-2xl ring-1 ring-border",
2311
2291
  style: {
@@ -2366,14 +2346,14 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
2366
2346
  value,
2367
2347
  min,
2368
2348
  max,
2369
- className: cn("relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", className),
2349
+ className: require_format.cn("relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", className),
2370
2350
  ...props,
2371
2351
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Track, {
2372
2352
  "data-slot": "slider-track",
2373
- className: cn("relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"),
2353
+ className: require_format.cn("relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"),
2374
2354
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Range, {
2375
2355
  "data-slot": "slider-range",
2376
- className: cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
2356
+ className: require_format.cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
2377
2357
  })
2378
2358
  }), Array.from({ length: _values.length }, (_, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Thumb, {
2379
2359
  "data-slot": "slider-thumb",
@@ -2388,7 +2368,7 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
2388
2368
  */
2389
2369
  function Timecode({ us, showMs = true, className, muted, title }) {
2390
2370
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2391
- className: cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
2371
+ className: require_format.cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
2392
2372
  title: title ?? `${us} µs`,
2393
2373
  children: (0, _cut_workbench_core.formatTimecode)(us, showMs)
2394
2374
  });
@@ -2400,15 +2380,51 @@ var RATES = [
2400
2380
  1.5,
2401
2381
  2
2402
2382
  ];
2383
+ /**
2384
+ * 紧凑档位(按容器宽度,不是视口 —— 预览列宽由拖拽决定)。
2385
+ *
2386
+ * 阈值来自实测的控件总宽:完整形态约 680px,`< 700` 先去音量滑杆与速率、
2387
+ * `< 560` 再去时间码的「总时长」与进度条、`< 420` 再去跳首尾。
2388
+ * 只靠 `shrink-0` + `overflow:hidden` 会在 650px 左右把时间码裁掉,
2389
+ * 且**没有任何报错**,只有截图能看出来 —— 所以这里显式分级。
2390
+ */
2391
+ var FULL_PX = 700;
2392
+ var COMPACT_PX = 620;
2393
+ var TIGHT_PX = 520;
2394
+ var ULTRA_COMPACT_PX = 420;
2395
+ /** 低于此宽度连时间码都放不下:只留播放/逐帧,横向可滚 */
2396
+ var MICRO_PX = 200;
2403
2397
  /** 播放控制条:逐帧 / 跳首尾 / 播放 / 时间码 / 速率 / 音量 */
2404
2398
  function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playbackRate, disabled, onToggle, onJumpStart, onJumpEnd, onStepFrame, onSeek, onVolumeChange, onRateChange, className }) {
2399
+ const rootRef = (0, react.useRef)(null);
2400
+ const [width, setWidth] = (0, react.useState)(0);
2401
+ /**
2402
+ * 用容器宽度而不是 CSS 媒体查询。
2403
+ *
2404
+ * 预览面板宽度由左侧拖拽决定,和视口宽度无关 —— 用户把两侧拉宽时,
2405
+ * 视口一点没变,`max-[560px]:hidden` 这类媒体查询不会触发,时间码照样被压扁。
2406
+ */
2407
+ (0, react.useEffect)(() => {
2408
+ const el = rootRef.current;
2409
+ if (el == null) return;
2410
+ const ro = new ResizeObserver(() => setWidth(el.clientWidth));
2411
+ ro.observe(el);
2412
+ setWidth(el.clientWidth);
2413
+ return () => ro.disconnect();
2414
+ }, []);
2415
+ const compact = width > 0 && width < COMPACT_PX;
2416
+ const tight = width > 0 && width < TIGHT_PX;
2417
+ const ultraCompact = width > 0 && width < ULTRA_COMPACT_PX;
2418
+ const micro = width > 0 && width < MICRO_PX;
2419
+ const full = width === 0 || width >= FULL_PX;
2405
2420
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2406
- className: cn("flex h-11 shrink-0 items-center gap-1.5 border-t bg-card/60 px-3", className),
2421
+ ref: rootRef,
2422
+ className: require_format.cn("flex h-11 shrink-0 items-center gap-1.5 overflow-hidden border-t bg-card/60", ultraCompact ? "px-1.5" : "px-3", className),
2407
2423
  children: [
2408
2424
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2409
- className: "flex items-center gap-0.5",
2425
+ className: "flex shrink-0 items-center gap-0.5",
2410
2426
  children: [
2411
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2427
+ !ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2412
2428
  asChild: true,
2413
2429
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2414
2430
  variant: "ghost",
@@ -2450,7 +2466,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
2450
2466
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.StepForward, { className: "size-3.5" })
2451
2467
  })
2452
2468
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "下一帧 (→)" })] }),
2453
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2469
+ !ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2454
2470
  asChild: true,
2455
2471
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2456
2472
  variant: "ghost",
@@ -2464,23 +2480,19 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
2464
2480
  ]
2465
2481
  }),
2466
2482
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2467
- className: "mx-2 flex items-baseline gap-1",
2468
- children: [
2469
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2470
- us: currentUs,
2471
- className: "text-[13px] font-medium"
2472
- }),
2473
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2474
- className: "text-xs text-muted-foreground",
2475
- children: "/"
2476
- }),
2477
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2478
- us: durationUs,
2479
- muted: true
2480
- })
2481
- ]
2483
+ className: require_format.cn("mx-2 flex shrink-0 items-baseline gap-1 whitespace-nowrap", ultraCompact && "mx-1", micro && "hidden"),
2484
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2485
+ us: currentUs,
2486
+ className: "text-[13px] font-medium"
2487
+ }), !tight && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2488
+ className: "text-xs text-muted-foreground",
2489
+ children: "/"
2490
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2491
+ us: durationUs,
2492
+ muted: true
2493
+ })] })]
2482
2494
  }),
2483
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2495
+ !ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2484
2496
  type: "range",
2485
2497
  className: "mx-1 h-1 min-w-16 flex-1 cursor-pointer appearance-none rounded-full bg-muted accent-primary",
2486
2498
  min: 0,
@@ -2492,14 +2504,14 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
2492
2504
  "aria-label": "播放进度"
2493
2505
  }),
2494
2506
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2495
- className: "flex items-center gap-1",
2507
+ className: require_format.cn("flex shrink-0 items-center gap-1", compact && "ml-auto", micro && "hidden"),
2496
2508
  children: [
2497
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2509
+ !compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2498
2510
  className: "flex items-center rounded-md border p-0.5",
2499
2511
  children: RATES.map((rate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2500
2512
  type: "button",
2501
2513
  onClick: () => onRateChange(rate),
2502
- className: cn("rounded px-1.5 py-0.5 text-[11px] tabular-nums transition-colors", playbackRate === rate ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"),
2514
+ className: require_format.cn("rounded px-1.5 py-0.5 text-[11px] tabular-nums transition-colors", playbackRate === rate ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"),
2503
2515
  title: rate !== 1 ? "变速预览(不会改变导出结果)" : "正常速度",
2504
2516
  children: [rate, "×"]
2505
2517
  }, rate))
@@ -2514,7 +2526,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
2514
2526
  children: masterVolume > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Volume2, { className: "size-3.5" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.VolumeX, { className: "size-3.5" })
2515
2527
  })
2516
2528
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: masterVolume > 0 ? "静音预览" : "取消静音" })] }),
2517
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
2529
+ full && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
2518
2530
  className: "w-20",
2519
2531
  min: 0,
2520
2532
  max: 1,
@@ -2798,143 +2810,148 @@ var DEFAULT_BITRATE = 8e6;
2798
2810
  /** 时间轴顶部工具条:分割 / 复制 / 删除 / 吸附开关 / 缩放 / 加轨道 */
2799
2811
  function TimelineToolbar({ playheadUs, durationUs, pxPerSecond, selectionCount, magnetEnabled, onToggleMagnet, onSplit, onDuplicate, onRemove, onZoomIn, onZoomOut, onZoomToFit, onAddTrack, canZoomIn, canZoomOut }) {
2800
2812
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2801
- className: "flex h-9 shrink-0 items-center gap-1 border-b bg-card/60 px-2",
2802
- children: [
2803
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2804
- asChild: true,
2805
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2806
- variant: "ghost",
2807
- size: "icon-sm",
2808
- onClick: onSplit,
2809
- "aria-label": "分割",
2810
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Scissors, { className: "size-3.5" })
2811
- })
2812
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "在播放头分割 (S)" })] }),
2813
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2814
- asChild: true,
2815
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2816
- variant: "ghost",
2817
- size: "icon-sm",
2818
- onClick: onDuplicate,
2819
- disabled: selectionCount === 0,
2820
- "aria-label": "复制片段",
2821
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Copy, { className: "size-3.5" })
2822
- })
2823
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "复制片段 (Ctrl+D)" })] }),
2824
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2825
- asChild: true,
2826
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2827
- variant: "ghost",
2828
- size: "icon-sm",
2829
- onClick: onRemove,
2830
- disabled: selectionCount === 0,
2831
- "aria-label": "删除片段",
2832
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "size-3.5" })
2833
- })
2834
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "删除片段 (Delete)" })] }),
2835
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2836
- orientation: "vertical",
2837
- className: "mx-1 h-4"
2838
- }),
2839
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2840
- asChild: true,
2841
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2842
- variant: magnetEnabled ? "secondary" : "ghost",
2843
- size: "icon-sm",
2844
- onClick: onToggleMagnet,
2845
- "aria-label": "吸附",
2846
- "aria-pressed": magnetEnabled,
2847
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Magnet, { className: "size-3.5" })
2848
- })
2849
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "吸附对齐(阈值 15px)" })] }),
2850
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2851
- orientation: "vertical",
2852
- className: "mx-1 h-4"
2853
- }),
2854
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2855
- className: "flex items-center gap-0.5",
2856
- children: [
2857
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2858
- asChild: true,
2859
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2860
- variant: "ghost",
2861
- size: "icon-sm",
2862
- onClick: onZoomOut,
2863
- disabled: !canZoomOut,
2864
- "aria-label": "缩小",
2865
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomOut, { className: "size-3.5" })
2866
- })
2867
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "缩小 (-)" })] }),
2868
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2869
- className: "w-11 text-center font-mono text-[10px] text-muted-foreground tabular-nums",
2870
- children: Math.round(pxPerSecond)
2871
- }),
2872
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2873
- asChild: true,
2874
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2813
+ className: "thin-scrollbar @container flex h-9 shrink-0 items-center gap-1 overflow-x-auto overflow-y-hidden border-b bg-card/60 px-2",
2814
+ role: "toolbar",
2815
+ "aria-label": "时间轴工具",
2816
+ "data-timeline-toolbar": true,
2817
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2818
+ className: "flex shrink-0 items-center gap-1",
2819
+ children: [
2820
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2821
+ asChild: true,
2822
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2823
+ variant: "ghost",
2824
+ size: "icon-sm",
2825
+ onClick: onSplit,
2826
+ "aria-label": "分割",
2827
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Scissors, { className: "size-3.5" })
2828
+ })
2829
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "在播放头分割 (S)" })] }),
2830
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2831
+ asChild: true,
2832
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2833
+ variant: "ghost",
2834
+ size: "icon-sm",
2835
+ onClick: onDuplicate,
2836
+ disabled: selectionCount === 0,
2837
+ "aria-label": "复制片段",
2838
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Copy, { className: "size-3.5" })
2839
+ })
2840
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "复制片段 (Ctrl+D)" })] }),
2841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2842
+ asChild: true,
2843
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2844
+ variant: "ghost",
2845
+ size: "icon-sm",
2846
+ onClick: onRemove,
2847
+ disabled: selectionCount === 0,
2848
+ "aria-label": "删除片段",
2849
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "size-3.5" })
2850
+ })
2851
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "删除片段 (Delete)" })] }),
2852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2853
+ orientation: "vertical",
2854
+ className: "mx-1 h-4"
2855
+ }),
2856
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2857
+ asChild: true,
2858
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2859
+ variant: magnetEnabled ? "secondary" : "ghost",
2860
+ size: "icon-sm",
2861
+ onClick: onToggleMagnet,
2862
+ "aria-label": "吸附",
2863
+ "aria-pressed": magnetEnabled,
2864
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Magnet, { className: "size-3.5" })
2865
+ })
2866
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "吸附对齐(阈值 15px)" })] }),
2867
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2868
+ orientation: "vertical",
2869
+ className: "mx-1 h-4"
2870
+ }),
2871
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2872
+ className: "flex items-center gap-0.5",
2873
+ children: [
2874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2875
+ asChild: true,
2876
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2877
+ variant: "ghost",
2878
+ size: "icon-sm",
2879
+ onClick: onZoomOut,
2880
+ disabled: !canZoomOut,
2881
+ "aria-label": "缩小",
2882
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomOut, { className: "size-3.5" })
2883
+ })
2884
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "缩小 (-)" })] }),
2885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2886
+ className: "w-11 text-center font-mono text-[10px] text-muted-foreground tabular-nums",
2887
+ children: Math.round(pxPerSecond)
2888
+ }),
2889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2890
+ asChild: true,
2891
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2892
+ variant: "ghost",
2893
+ size: "icon-sm",
2894
+ onClick: onZoomIn,
2895
+ disabled: !canZoomIn,
2896
+ "aria-label": "放大",
2897
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, { className: "size-3.5" })
2898
+ })
2899
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "放大 (+)" })] }),
2900
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2875
2901
  variant: "ghost",
2876
- size: "icon-sm",
2877
- onClick: onZoomIn,
2878
- disabled: !canZoomIn,
2879
- "aria-label": "放大",
2880
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, { className: "size-3.5" })
2902
+ size: "xs",
2903
+ onClick: onZoomToFit,
2904
+ className: "text-[10px]",
2905
+ children: "适应"
2881
2906
  })
2882
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "放大 (+)" })] }),
2883
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2907
+ ]
2908
+ }),
2909
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2910
+ orientation: "vertical",
2911
+ className: "mx-1 h-4"
2912
+ }),
2913
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2914
+ className: "flex items-center gap-0.5",
2915
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
2884
2916
  variant: "ghost",
2885
2917
  size: "xs",
2886
- onClick: onZoomToFit,
2887
- className: "text-[10px]",
2888
- children: "适应"
2889
- })
2890
- ]
2891
- }),
2892
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2893
- orientation: "vertical",
2894
- className: "mx-1 h-4"
2895
- }),
2896
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2897
- className: "flex items-center gap-0.5",
2898
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
2899
- variant: "ghost",
2900
- size: "xs",
2901
- onClick: () => onAddTrack("visual"),
2902
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "视频轨"]
2903
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
2904
- variant: "ghost",
2905
- size: "xs",
2906
- onClick: () => onAddTrack("audio"),
2907
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "音频轨"]
2908
- })]
2909
- }),
2910
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2911
- className: "ml-auto flex items-center gap-2 text-[11px] text-muted-foreground",
2912
- children: [
2913
- selectionCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2914
- className: "rounded bg-primary/15 px-1.5 py-0.5 text-primary",
2915
- children: ["已选 ", selectionCount]
2916
- }),
2917
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Info, { className: "size-3" }),
2918
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2919
- className: "tabular-nums",
2920
- children: [
2921
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2922
- us: playheadUs,
2923
- className: "text-foreground"
2924
- }),
2925
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2926
- className: "mx-1",
2927
- children: "/"
2928
- }),
2929
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2930
- us: durationUs,
2931
- muted: true
2932
- })
2933
- ]
2934
- })
2935
- ]
2936
- })
2937
- ]
2918
+ onClick: () => onAddTrack("visual"),
2919
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "视频轨"]
2920
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
2921
+ variant: "ghost",
2922
+ size: "xs",
2923
+ onClick: () => onAddTrack("audio"),
2924
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "音频轨"]
2925
+ })]
2926
+ })
2927
+ ]
2928
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2929
+ className: "ml-auto hidden shrink-0 items-center gap-2 pl-2 text-[11px] text-muted-foreground @[720px]:flex",
2930
+ children: [
2931
+ selectionCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2932
+ className: "rounded bg-primary/15 px-1.5 py-0.5 text-primary",
2933
+ children: ["已选 ", selectionCount]
2934
+ }),
2935
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Info, { className: "size-3" }),
2936
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2937
+ className: "tabular-nums whitespace-nowrap",
2938
+ children: [
2939
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2940
+ us: playheadUs,
2941
+ className: "text-foreground"
2942
+ }),
2943
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2944
+ className: "mx-1",
2945
+ children: "/"
2946
+ }),
2947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2948
+ us: durationUs,
2949
+ muted: true
2950
+ })
2951
+ ]
2952
+ })
2953
+ ]
2954
+ })]
2938
2955
  });
2939
2956
  }
2940
2957
  // src/features/timeline/components/Ruler.tsx
@@ -2951,14 +2968,14 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
2951
2968
  widthPx
2952
2969
  ]);
2953
2970
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2954
- className: cn("relative shrink-0 cursor-ew-resize border-b bg-timeline-bg select-none", className),
2971
+ className: require_format.cn("relative shrink-0 cursor-ew-resize border-b bg-timeline-bg select-none", className),
2955
2972
  style: {
2956
2973
  height: 28,
2957
2974
  width: widthPx
2958
2975
  },
2959
2976
  children: [
2960
2977
  ticks.map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2961
- className: cn("absolute bottom-0 w-px", tick.major ? "h-2 bg-muted-foreground/50" : "h-1 bg-muted-foreground/25"),
2978
+ className: require_format.cn("absolute bottom-0 w-px", tick.major ? "h-2 bg-muted-foreground/50" : "h-1 bg-muted-foreground/25"),
2962
2979
  style: { left: tick.px }
2963
2980
  }, `${tick.us}-${tick.major ? "M" : "m"}`)),
2964
2981
  ticks.filter((t) => t.major).map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2986,7 +3003,7 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
2986
3003
  */
2987
3004
  var TrackHeader = (0, react.memo)(function TrackHeader({ track, onToggleMute, onToggleLock, onToggleHidden, onRemove }) {
2988
3005
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2989
- className: cn("flex items-center gap-1 border-r border-b border-timeline-grid px-2", track.kind === "audio" ? "bg-clip-audio/10" : "bg-clip-video/10", track.hidden && "opacity-60"),
3006
+ className: require_format.cn("flex items-center gap-1 border-r border-b border-timeline-grid px-2", track.kind === "audio" ? "bg-clip-audio/10" : "bg-clip-video/10", track.hidden && "opacity-60"),
2990
3007
  style: { height: 56 },
2991
3008
  "data-track-header": true,
2992
3009
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3041,7 +3058,7 @@ function IconToggle({ active, onClick, label, icon: Icon }) {
3041
3058
  onClick,
3042
3059
  "aria-label": label,
3043
3060
  "aria-pressed": active,
3044
- className: cn("flex size-[18px] items-center justify-center rounded transition-colors", active ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"),
3061
+ className: require_format.cn("flex size-[18px] items-center justify-center rounded transition-colors", active ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"),
3045
3062
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-3" })
3046
3063
  })
3047
3064
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: label })] });
@@ -3132,7 +3149,7 @@ var ClipBlock = (0, react.memo)(function ClipBlock({ clip, asset, pxPerSecond, s
3132
3149
  handlers.beginMove(event, clip);
3133
3150
  },
3134
3151
  onDoubleClick: () => handlers.seekTo(clip),
3135
- className: cn("group absolute top-1 bottom-1 overflow-hidden rounded-md border text-left select-none transition-shadow", KIND_BG[kind], selected ? "ring-2 ring-primary ring-offset-1 ring-offset-timeline-bg" : "hover:brightness-110", locked ? "cursor-not-allowed opacity-70" : "cursor-grab active:cursor-grabbing"),
3152
+ className: require_format.cn("group absolute top-1 bottom-1 overflow-hidden rounded-md border text-left select-none transition-shadow", KIND_BG[kind], selected ? "ring-2 ring-primary ring-offset-1 ring-offset-timeline-bg" : "hover:brightness-110", locked ? "cursor-not-allowed opacity-70" : "cursor-grab active:cursor-grabbing"),
3136
3153
  style: {
3137
3154
  left: leftPx,
3138
3155
  width: widthPx,
@@ -3211,7 +3228,7 @@ function ContextMenuPortal({ ...props }) {
3211
3228
  function ContextMenuContent({ className, ...props }) {
3212
3229
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Content, {
3213
3230
  "data-slot": "context-menu-content",
3214
- className: cn("z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
3231
+ className: require_format.cn("z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className),
3215
3232
  ...props
3216
3233
  }) });
3217
3234
  }
@@ -3220,21 +3237,21 @@ function ContextMenuItem({ className, inset, variant = "default", ...props }) {
3220
3237
  "data-slot": "context-menu-item",
3221
3238
  "data-inset": inset,
3222
3239
  "data-variant": variant,
3223
- className: cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
3240
+ className: require_format.cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className),
3224
3241
  ...props
3225
3242
  });
3226
3243
  }
3227
3244
  function ContextMenuSeparator({ className, ...props }) {
3228
3245
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Separator, {
3229
3246
  "data-slot": "context-menu-separator",
3230
- className: cn("-mx-1 my-1 h-px bg-border", className),
3247
+ className: require_format.cn("-mx-1 my-1 h-px bg-border", className),
3231
3248
  ...props
3232
3249
  });
3233
3250
  }
3234
3251
  function ContextMenuShortcut({ className, ...props }) {
3235
3252
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3236
3253
  "data-slot": "context-menu-shortcut",
3237
- className: cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
3254
+ className: require_format.cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
3238
3255
  ...props
3239
3256
  });
3240
3257
  }
@@ -3291,14 +3308,14 @@ function ClipContextMenu({ children, onSplit, onDuplicate, onRemove, onShowInfo,
3291
3308
  /** 播放头:一条竖线 + 顶部把手,跨过所有轨道 */
3292
3309
  function Playhead({ px, playing, className }) {
3293
3310
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3294
- className: cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
3311
+ className: require_format.cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
3295
3312
  style: { left: px },
3296
3313
  "aria-hidden": true,
3297
3314
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3298
- className: cn("absolute inset-y-0 -left-px w-0.5 bg-playhead", playing && "shadow-[0_0_6px_var(--cw-playhead)]"),
3315
+ className: require_format.cn("absolute inset-y-0 -left-px w-0.5 bg-playhead", playing && "shadow-[0_0_6px_var(--cw-playhead)]"),
3299
3316
  style: { top: 24 }
3300
3317
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3301
- className: cn("absolute -translate-x-1/2 rounded-sm bg-playhead", playing ? "size-3 rotate-45" : "h-2.5 w-2.5 rotate-45"),
3318
+ className: require_format.cn("absolute -translate-x-1/2 rounded-sm bg-playhead", playing ? "size-3 rotate-45" : "h-2.5 w-2.5 rotate-45"),
3302
3319
  style: { top: 14 }
3303
3320
  })]
3304
3321
  });
@@ -3307,7 +3324,7 @@ function Playhead({ px, playing, className }) {
3307
3324
  /** 框选矩形 */
3308
3325
  function Marquee({ x1, y1, x2, y2, className }) {
3309
3326
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3310
- className: cn("pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/15", className),
3327
+ className: require_format.cn("pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/15", className),
3311
3328
  style: {
3312
3329
  left: Math.min(x1, x2),
3313
3330
  top: Math.min(y1, y2),
@@ -3898,7 +3915,7 @@ function Timeline() {
3898
3915
  splitClip
3899
3916
  ]);
3900
3917
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3901
- className: "flex min-h-0 flex-1 flex-col",
3918
+ className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
3902
3919
  children: [
3903
3920
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelineToolbar, {
3904
3921
  playheadUs,
@@ -3918,7 +3935,7 @@ function Timeline() {
3918
3935
  canZoomOut: zoom.canZoomOut
3919
3936
  }),
3920
3937
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3921
- className: "relative flex min-h-0 flex-1",
3938
+ className: "relative flex min-h-0 min-w-0 flex-1 overflow-hidden",
3922
3939
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3923
3940
  className: "thin-scrollbar flex shrink-0 flex-col overflow-y-auto border-r",
3924
3941
  style: { width: 150 },
@@ -3982,7 +3999,7 @@ function Timeline() {
3982
3999
  "data-timeline-tracks": true,
3983
4000
  children: [
3984
4001
  project.tracks.map((track, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3985
- className: cn("relative border-b border-timeline-grid", track.kind === "audio" ? "bg-timeline-track/30" : "bg-timeline-track/50", track.hidden && "opacity-40"),
4002
+ className: require_format.cn("relative border-b border-timeline-grid", track.kind === "audio" ? "bg-timeline-track/30" : "bg-timeline-track/50", track.hidden && "opacity-40"),
3986
4003
  style: { height: 56 },
3987
4004
  "data-track-row": track.id,
3988
4005
  children: [
@@ -4053,7 +4070,7 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
4053
4070
  "data-slot": "tabs",
4054
4071
  "data-orientation": orientation,
4055
4072
  orientation,
4056
- className: cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
4073
+ className: require_format.cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
4057
4074
  ...props
4058
4075
  });
4059
4076
  }
@@ -4068,21 +4085,21 @@ function TabsList({ className, variant = "default", ...props }) {
4068
4085
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.List, {
4069
4086
  "data-slot": "tabs-list",
4070
4087
  "data-variant": variant,
4071
- className: cn(tabsListVariants({ variant }), className),
4088
+ className: require_format.cn(tabsListVariants({ variant }), className),
4072
4089
  ...props
4073
4090
  });
4074
4091
  }
4075
4092
  function TabsTrigger({ className, ...props }) {
4076
4093
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Trigger, {
4077
4094
  "data-slot": "tabs-trigger",
4078
- className: cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", className),
4095
+ className: require_format.cn("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent", "data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground", "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100", className),
4079
4096
  ...props
4080
4097
  });
4081
4098
  }
4082
4099
  function TabsContent({ className, ...props }) {
4083
4100
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Content, {
4084
4101
  "data-slot": "tabs-content",
4085
- className: cn("flex-1 outline-none", className),
4102
+ className: require_format.cn("flex-1 outline-none", className),
4086
4103
  ...props
4087
4104
  });
4088
4105
  }
@@ -4091,7 +4108,7 @@ function Input({ className, type, ...props }) {
4091
4108
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4092
4109
  type,
4093
4110
  "data-slot": "input",
4094
- className: cn("h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", "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", className),
4111
+ className: require_format.cn("h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", "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", className),
4095
4112
  ...props
4096
4113
  });
4097
4114
  }
@@ -4112,7 +4129,7 @@ function SelectTrigger({ className, size = "default", children, ...props }) {
4112
4129
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Trigger, {
4113
4130
  "data-slot": "select-trigger",
4114
4131
  "data-size": size,
4115
- className: cn("flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className),
4132
+ className: require_format.cn("flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className),
4116
4133
  ...props,
4117
4134
  children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.Icon, {
4118
4135
  asChild: true,
@@ -4126,14 +4143,14 @@ function SelectContent({ className, children, position = "item-aligned", align =
4126
4143
  container: container ?? void 0,
4127
4144
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Content, {
4128
4145
  "data-slot": "select-content",
4129
- className: cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className),
4146
+ className: require_format.cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className),
4130
4147
  position,
4131
4148
  align,
4132
4149
  ...props,
4133
4150
  children: [
4134
4151
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollUpButton, {}),
4135
4152
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.Viewport, {
4136
- className: cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"),
4153
+ className: require_format.cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"),
4137
4154
  children
4138
4155
  }),
4139
4156
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollDownButton, {})
@@ -4144,7 +4161,7 @@ function SelectContent({ className, children, position = "item-aligned", align =
4144
4161
  function SelectItem({ className, children, ...props }) {
4145
4162
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Item, {
4146
4163
  "data-slot": "select-item",
4147
- className: cn("relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className),
4164
+ className: require_format.cn("relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", className),
4148
4165
  ...props,
4149
4166
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4150
4167
  "data-slot": "select-item-indicator",
@@ -4156,7 +4173,7 @@ function SelectItem({ className, children, ...props }) {
4156
4173
  function SelectScrollUpButton({ className, ...props }) {
4157
4174
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollUpButton, {
4158
4175
  "data-slot": "select-scroll-up-button",
4159
- className: cn("flex cursor-default items-center justify-center py-1", className),
4176
+ className: require_format.cn("flex cursor-default items-center justify-center py-1", className),
4160
4177
  ...props,
4161
4178
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUpIcon, { className: "size-4" })
4162
4179
  });
@@ -4164,7 +4181,7 @@ function SelectScrollUpButton({ className, ...props }) {
4164
4181
  function SelectScrollDownButton({ className, ...props }) {
4165
4182
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollDownButton, {
4166
4183
  "data-slot": "select-scroll-down-button",
4167
- className: cn("flex cursor-default items-center justify-center py-1", className),
4184
+ className: require_format.cn("flex cursor-default items-center justify-center py-1", className),
4168
4185
  ...props,
4169
4186
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDownIcon, { className: "size-4" })
4170
4187
  });
@@ -4173,7 +4190,7 @@ function SelectScrollDownButton({ className, ...props }) {
4173
4190
  /** 属性面板的一行:标签 + 控件(标签在左,控件占满剩余宽度) */
4174
4191
  function Field({ label, hint, children, className, htmlFor }) {
4175
4192
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4176
- className: cn("flex items-center gap-2", className),
4193
+ className: require_format.cn("flex items-center gap-2", className),
4177
4194
  children: [
4178
4195
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Label, {
4179
4196
  htmlFor,
@@ -4194,7 +4211,7 @@ function Field({ label, hint, children, className, htmlFor }) {
4194
4211
  /** 属性分组 */
4195
4212
  function Section({ title, children, action, className }) {
4196
4213
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4197
- className: cn("space-y-2", className),
4214
+ className: require_format.cn("space-y-2", className),
4198
4215
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4199
4216
  className: "flex items-center justify-between",
4200
4217
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
@@ -4389,7 +4406,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
4389
4406
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
4390
4407
  type: "number",
4391
4408
  className: "h-7 text-xs tabular-nums",
4392
- value: formatSeconds(clip.startUs),
4409
+ value: require_format.formatSeconds(clip.startUs),
4393
4410
  step: .1,
4394
4411
  min: 0,
4395
4412
  onChange: (e) => {
@@ -4403,7 +4420,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
4403
4420
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
4404
4421
  type: "number",
4405
4422
  className: "h-7 text-xs tabular-nums",
4406
- value: formatSeconds(clip.durationUs),
4423
+ value: require_format.formatSeconds(clip.durationUs),
4407
4424
  step: .1,
4408
4425
  min: .1,
4409
4426
  onChange: (e) => {
@@ -4510,7 +4527,7 @@ function AudioProps({ clip, onPatch }) {
4510
4527
  className: "text-[10px] text-muted-foreground",
4511
4528
  children: [
4512
4529
  "片段时长 ",
4513
- formatSeconds(clip.durationUs),
4530
+ require_format.formatSeconds(clip.durationUs),
4514
4531
  "s,淡入使用 setAnimation 线性插值。"
4515
4532
  ]
4516
4533
  })
@@ -4765,7 +4782,7 @@ function SubtitleProps({ clip, onPatch, onRebuild }) {
4765
4782
  className: "text-[10px] text-muted-foreground",
4766
4783
  children: [
4767
4784
  "片段时长 ",
4768
- formatSeconds(clip.durationUs),
4785
+ require_format.formatSeconds(clip.durationUs),
4769
4786
  "s",
4770
4787
  cues.length > 0 && ` · 最后一条结束于 ${(cues[cues.length - 1].endMs / 1e3).toFixed(2)}s`
4771
4788
  ]
@@ -5037,7 +5054,7 @@ function InspectorPanel() {
5037
5054
  title: asset.name,
5038
5055
  children: asset.name
5039
5056
  }),
5040
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
5057
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_format.Badge, {
5041
5058
  variant: "outline",
5042
5059
  className: "text-[10px]",
5043
5060
  children: KIND_LABEL[asset.kind]
@@ -5060,7 +5077,7 @@ function InspectorPanel() {
5060
5077
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
5061
5078
  (0, _cut_workbench_core.formatDuration)(clip.durationUs),
5062
5079
  " · ",
5063
- formatResolution(asset.width, asset.height)
5080
+ require_format.formatResolution(asset.width, asset.height)
5064
5081
  ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
5065
5082
  "入点 ",
5066
5083
  (clip.inPointUs / 1e6).toFixed(2),
@@ -5165,11 +5182,11 @@ function Switch({ className, size = "default", ...props }) {
5165
5182
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Root, {
5166
5183
  "data-slot": "switch",
5167
5184
  "data-size": size,
5168
- className: cn("peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80", className),
5185
+ className: require_format.cn("peer group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80", className),
5169
5186
  ...props,
5170
5187
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Thumb, {
5171
5188
  "data-slot": "switch-thumb",
5172
- className: cn("pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground")
5189
+ className: require_format.cn("pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:data-[state=checked]:bg-primary-foreground dark:data-[state=unchecked]:bg-foreground")
5173
5190
  })
5174
5191
  });
5175
5192
  }
@@ -5177,7 +5194,7 @@ function Switch({ className, size = "default", ...props }) {
5177
5194
  function Progress({ className, value, ...props }) {
5178
5195
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Root, {
5179
5196
  "data-slot": "progress",
5180
- className: cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
5197
+ className: require_format.cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
5181
5198
  ...props,
5182
5199
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Indicator, {
5183
5200
  "data-slot": "progress-indicator",
@@ -5855,7 +5872,7 @@ function ExportDialog() {
5855
5872
  className: "flex justify-between",
5856
5873
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "预估大小" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5857
5874
  className: "font-mono tabular-nums",
5858
- children: formatBytes(estimatedBytes)
5875
+ children: require_format.formatBytes(estimatedBytes)
5859
5876
  })]
5860
5877
  }),
5861
5878
  hasTextClips && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5892,10 +5909,10 @@ function ExportDialog() {
5892
5909
  children: [
5893
5910
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CheckCircle2, { className: "size-3.5" }),
5894
5911
  "导出完成 · 耗时 ",
5895
- formatElapsed(state.result.elapsedMs / 1e3),
5912
+ require_format.formatElapsed(state.result.elapsedMs / 1e3),
5896
5913
  " ·",
5897
5914
  " ",
5898
- formatBytes(state.result.blob.size),
5915
+ require_format.formatBytes(state.result.blob.size),
5899
5916
  state.result.mode === "main" && "(主线程)"
5900
5917
  ]
5901
5918
  }),
@@ -5958,7 +5975,7 @@ function ExportProgress({ progress, label = "正在合成…", className }) {
5958
5975
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }), label]
5959
5976
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5960
5977
  className: "font-mono tabular-nums",
5961
- children: formatPercent(progress)
5978
+ children: require_format.formatPercent(progress)
5962
5979
  })]
5963
5980
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Progress, { value: progress * 100 })]
5964
5981
  });
@@ -6034,7 +6051,7 @@ function CanvasSettingsBody({ onDone }) {
6034
6051
  setDraftW(String(preset.width));
6035
6052
  setDraftH(String(preset.height));
6036
6053
  },
6037
- className: cn("flex flex-col items-center gap-1 rounded-md border px-2 py-2 text-[11px] transition-colors", activePreset?.id === preset.id ? "border-primary bg-primary/10 text-foreground" : "text-muted-foreground hover:border-primary/50 hover:text-foreground"),
6054
+ className: require_format.cn("flex flex-col items-center gap-1 rounded-md border px-2 py-2 text-[11px] transition-colors", activePreset?.id === preset.id ? "border-primary bg-primary/10 text-foreground" : "text-muted-foreground hover:border-primary/50 hover:text-foreground"),
6038
6055
  title: preset.hint,
6039
6056
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6040
6057
  className: "block rounded-[2px] border border-current opacity-70",
@@ -6095,7 +6112,7 @@ function CanvasSettingsBody({ onDone }) {
6095
6112
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6096
6113
  type: "button",
6097
6114
  onClick: () => setMode("scale"),
6098
- className: cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "scale" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
6115
+ className: require_format.cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "scale" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
6099
6116
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6100
6117
  className: "block font-medium text-foreground",
6101
6118
  children: "随画布缩放"
@@ -6106,7 +6123,7 @@ function CanvasSettingsBody({ onDone }) {
6106
6123
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6107
6124
  type: "button",
6108
6125
  onClick: () => setMode("keep"),
6109
- className: cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "keep" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
6126
+ className: require_format.cn("rounded-md border px-2.5 py-2 text-left text-[11px] transition-colors", mode === "keep" ? "border-primary bg-primary/10" : "text-muted-foreground hover:border-primary/50"),
6110
6127
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6111
6128
  className: "block font-medium text-foreground",
6112
6129
  children: "保持原位置"
@@ -6414,1173 +6431,6 @@ function useCapabilityInfo() {
6414
6431
  summary: (0, _cut_workbench_core_engine.summarizeCapabilities)(report)
6415
6432
  };
6416
6433
  }
6417
- // src/features/lab/components/LabCard.tsx
6418
- /** Lab 卡片外壳:标题 + 用到的 API + (不可用时的原因) */
6419
- function LabCard({ title, description, apis, children, unavailable, className }) {
6420
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
6421
- className: cn("flex flex-col", className),
6422
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardHeader, {
6423
- className: "gap-1",
6424
- children: [
6425
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6426
- className: "flex items-center gap-2",
6427
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6428
- className: "flex size-6 items-center justify-center rounded bg-primary/10 text-primary",
6429
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FlaskConical, { className: "size-3.5" })
6430
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardTitle, {
6431
- className: "text-sm",
6432
- children: title
6433
- })]
6434
- }),
6435
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardDescription, {
6436
- className: "text-xs",
6437
- children: description
6438
- }),
6439
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6440
- className: "flex flex-wrap gap-1 pt-1",
6441
- children: apis.map((api) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
6442
- variant: "outline",
6443
- className: "font-mono text-[10px] font-normal text-muted-foreground",
6444
- children: api
6445
- }, api))
6446
- })
6447
- ]
6448
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardContent, {
6449
- className: "flex flex-1 flex-col gap-2.5",
6450
- children: unavailable != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6451
- className: "rounded-md border border-dashed border-warning/40 bg-warning/5 p-3 text-xs text-warning",
6452
- children: unavailable
6453
- }) : children
6454
- })]
6455
- });
6456
- }
6457
- // src/features/lab/hooks/use-lab-action.ts
6458
- /** 生成可下载链接(Lab 通用) */
6459
- function useDownload() {
6460
- const urlsRef = (0, react.useRef)([]);
6461
- return (0, react.useCallback)((blob, filename) => {
6462
- const url = URL.createObjectURL(blob);
6463
- urlsRef.current.push(url);
6464
- const link = document.createElement("a");
6465
- link.href = url;
6466
- link.download = filename;
6467
- document.body.appendChild(link);
6468
- link.click();
6469
- link.remove();
6470
- window.setTimeout(() => URL.revokeObjectURL(url), 15e3);
6471
- }, []);
6472
- }
6473
- /** 带 busy 状态的异步执行器:统一错误 toast,禁止静默吞异常 */
6474
- function useLabAction() {
6475
- const [busy, setBusy] = (0, react.useState)(null);
6476
- return {
6477
- busy,
6478
- run: (0, react.useCallback)(async (label, task) => {
6479
- setBusy(label);
6480
- try {
6481
- return await task();
6482
- } catch (err) {
6483
- const message = err instanceof Error ? err.message : String(err);
6484
- console.error(err);
6485
- sonner.toast.error("操作失败", { description: message });
6486
- return null;
6487
- } finally {
6488
- setBusy(null);
6489
- }
6490
- }, [])
6491
- };
6492
- }
6493
- // src/features/lab/components/shared.tsx
6494
- /**
6495
- * Lab 共享的可视组件。
6496
- *
6497
- * 只导出组件(React Fast Refresh 要求单文件只导出组件);
6498
- * 工具函数与 hook 放在 `../hooks/use-lab-action`。
6499
- */
6500
- /** 文件选择按钮 */
6501
- function FilePickButton({ label, accept, multiple, onFiles, disabled }) {
6502
- const inputRef = (0, react.useRef)(null);
6503
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6504
- ref: inputRef,
6505
- type: "file",
6506
- accept,
6507
- multiple,
6508
- className: "hidden",
6509
- onChange: (e) => {
6510
- const files = Array.from(e.target.files ?? []);
6511
- if (files.length > 0) onFiles(files);
6512
- e.target.value = "";
6513
- }
6514
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6515
- type: "button",
6516
- disabled,
6517
- onClick: () => inputRef.current?.click(),
6518
- 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",
6519
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6520
- className: "max-w-full truncate",
6521
- children: label
6522
- })
6523
- })] });
6524
- }
6525
- /** 结果预览:视频 + 下载 */
6526
- function ResultPreview({ blob, filename }) {
6527
- const download = useDownload();
6528
- const [url] = (0, react.useState)(() => URL.createObjectURL(blob));
6529
- (0, react.useEffect)(() => () => URL.revokeObjectURL(url), [url]);
6530
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6531
- className: "space-y-1.5",
6532
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
6533
- src: url,
6534
- controls: true,
6535
- className: "w-full rounded-md border bg-stage-bg",
6536
- style: { maxHeight: 200 }
6537
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6538
- className: "flex items-center justify-between gap-2",
6539
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6540
- className: "min-w-0 flex-1 truncate text-[10px] text-muted-foreground",
6541
- children: [(blob.size / 1024 / 1024).toFixed(2), " MB"]
6542
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6543
- type: "button",
6544
- onClick: () => download(blob, filename),
6545
- className: "rounded-md bg-primary px-2 py-1 text-[11px] text-primary-foreground hover:bg-primary/90",
6546
- children: "下载"
6547
- })]
6548
- })]
6549
- });
6550
- }
6551
- /** 忙碌提示 */
6552
- function Busy({ label }) {
6553
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6554
- className: "flex items-center gap-2 rounded-md bg-muted px-3 py-2 text-xs text-muted-foreground",
6555
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }), label]
6556
- });
6557
- }
6558
- /** 简易滑杆 */
6559
- function SliderRow({ label, value, min, max, step, format, onChange }) {
6560
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6561
- className: "space-y-1",
6562
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6563
- className: "flex justify-between text-[10px] text-muted-foreground",
6564
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6565
- className: "font-mono tabular-nums",
6566
- children: format ? format(value) : value.toFixed(2)
6567
- })]
6568
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6569
- type: "range",
6570
- min,
6571
- max,
6572
- step,
6573
- value,
6574
- onChange: (e) => onChange(Number(e.target.value)),
6575
- className: "h-1 w-full cursor-pointer appearance-none rounded-full bg-muted accent-primary"
6576
- })]
6577
- });
6578
- }
6579
- // src/features/lab/components/ConcatenateLab.tsx
6580
- /**
6581
- * Lab 1:无损拼接。
6582
- *
6583
- * 用 `fastConcatMP4` 直接拼接多个 MP4 的样本,不重编码。
6584
- * 前提是编码 / 分辨率 / 采样率完全一致;不一致时本 Lab 会先预检并给出具体差异。
6585
- */
6586
- function ConcatenateLab() {
6587
- const [files, setFiles] = (0, react.useState)([]);
6588
- const [infos, setInfos] = (0, react.useState)([]);
6589
- const [result, setResult] = (0, react.useState)(null);
6590
- const { busy, run } = useLabAction();
6591
- const handleFiles = async (picked) => {
6592
- setFiles(picked);
6593
- setResult(null);
6594
- setInfos([]);
6595
- const detected = await run("正在读取文件信息…", async () => {
6596
- const list = [];
6597
- for (const file of picked) list.push(await (0, _cut_workbench_core_lab.readMp4Info)(file));
6598
- return list;
6599
- });
6600
- if (detected != null) setInfos(detected);
6601
- };
6602
- const handleConcat = async () => {
6603
- const output = await run("正在无损拼接…", () => (0, _cut_workbench_core_lab.concatMp4)(files));
6604
- if (output != null) {
6605
- setResult(output.blob);
6606
- setInfos(output.infos);
6607
- }
6608
- };
6609
- const first = infos[0];
6610
- const mismatch = first != null && infos.some((info) => info.width !== first.width || info.height !== first.height || info.codec !== first.codec || info.audioSampleRate !== first.audioSampleRate);
6611
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
6612
- title: "无损拼接",
6613
- description: "直接把多个 MP4 的样本拼在一起,不重编码,速度极快。要求规格完全一致。",
6614
- apis: ["fastConcatMP4", "MP4Clip.getFileHeaderBinData"],
6615
- children: [
6616
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
6617
- label: files.length > 0 ? `已选 ${files.length} 个文件(重新选择)` : "选择 2 个以上 MP4",
6618
- accept: "video/mp4",
6619
- multiple: true,
6620
- onFiles: (picked) => void handleFiles(picked),
6621
- disabled: busy != null
6622
- }),
6623
- files.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
6624
- className: "space-y-1 text-[10px] text-muted-foreground",
6625
- children: files.map((file, index) => {
6626
- const info = infos[index];
6627
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
6628
- className: "flex justify-between gap-2",
6629
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6630
- className: "truncate",
6631
- children: file.name
6632
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6633
- className: "shrink-0 font-mono",
6634
- children: info != null ? `${formatResolution(info.width, info.height)} · ${(0, _cut_workbench_core.formatDuration)(info.durationUs)} · ${info.codec}` : "检测中…"
6635
- })]
6636
- }, `${file.name}-${index}`);
6637
- })
6638
- }),
6639
- mismatch && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6640
- className: "flex items-start gap-1.5 rounded-md bg-warning/10 p-2 text-[10px] text-warning",
6641
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertTriangle, { className: "mt-0.5 size-3 shrink-0" }), "检测到规格不一致,拼接会失败并给出具体原因(fastConcatMP4 的硬性要求)。"]
6642
- }),
6643
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6644
- type: "button",
6645
- disabled: files.length < 2 || busy != null,
6646
- onClick: () => void handleConcat(),
6647
- className: "rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
6648
- children: "开始拼接"
6649
- }),
6650
- busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
6651
- result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
6652
- blob: result,
6653
- filename: "concat.mp4"
6654
- })
6655
- ]
6656
- });
6657
- }
6658
- // src/features/lab/components/RemuxLab.tsx
6659
- /**
6660
- * Lab 2:配音。
6661
- *
6662
- * `mixinMP4AndAudio` 只重编码音频轨,视频轨原样复制 —— 因此速度快、画质无损。
6663
- */
6664
- function RemuxLab() {
6665
- const [video, setVideo] = (0, react.useState)(null);
6666
- const [audio, setAudio] = (0, react.useState)(null);
6667
- const [volume, setVolume] = (0, react.useState)(1);
6668
- const [loop, setLoop] = (0, react.useState)(true);
6669
- const [result, setResult] = (0, react.useState)(null);
6670
- const { busy, run } = useLabAction();
6671
- const handleMix = async () => {
6672
- if (video == null || audio == null) return;
6673
- const output = await run("正在混合音频…", () => (0, _cut_workbench_core_lab.mixAudioIntoMp4)(video, audio, volume, loop));
6674
- if (output != null) setResult(output);
6675
- };
6676
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
6677
- title: "视频配音",
6678
- description: "把音轨混入 MP4:只重编码音频,视频轨不动,画质零损失。",
6679
- apis: ["mixinMP4AndAudio"],
6680
- children: [
6681
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
6682
- label: video == null ? "选择视频(MP4)" : video.name,
6683
- accept: "video/mp4",
6684
- onFiles: (files) => {
6685
- setVideo(files[0] ?? null);
6686
- setResult(null);
6687
- },
6688
- disabled: busy != null
6689
- }),
6690
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
6691
- label: audio == null ? "选择音频(MP3/WAV/M4A)" : audio.name,
6692
- accept: "audio/*",
6693
- onFiles: (files) => {
6694
- setAudio(files[0] ?? null);
6695
- setResult(null);
6696
- },
6697
- disabled: busy != null
6698
- }),
6699
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
6700
- label: "音量",
6701
- value: volume,
6702
- min: 0,
6703
- max: 2,
6704
- step: .05,
6705
- format: (v) => `${Math.round(v * 100)}%`,
6706
- onChange: setVolume
6707
- }),
6708
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
6709
- className: "flex items-center gap-2 text-[11px] text-muted-foreground",
6710
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6711
- type: "checkbox",
6712
- checked: loop,
6713
- onChange: (e) => setLoop(e.target.checked),
6714
- className: "accent-primary"
6715
- }), "音频短于视频时循环填充"]
6716
- }),
6717
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6718
- type: "button",
6719
- disabled: video == null || audio == null || busy != null,
6720
- onClick: () => void handleMix(),
6721
- className: "rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
6722
- children: "开始混合"
6723
- }),
6724
- busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
6725
- result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
6726
- blob: result,
6727
- filename: "mixin.mp4"
6728
- })
6729
- ]
6730
- });
6731
- }
6732
- // src/features/lab/components/ChromakeyLab.tsx
6733
- /**
6734
- * Lab 3:绿幕抠像。
6735
- *
6736
- * `createChromakey` 返回一个函数,把 VideoFrame 转成去背后的 ImageBitmap。
6737
- * 配合 `MP4Clip.tickInterceptor` 即可对整段视频生效(导出时同样有效)。
6738
- *
6739
- * ⚠️ 依赖 WebGL2;不支持时本卡片隐藏操作、仅显示原因。
6740
- */
6741
- /**
6742
- * 抠像结果的兜底底色:读取主题令牌,而不是写死 `#101018`。
6743
- *
6744
- * 抠像后的 VideoFrame 带透明通道,需要一层底色才能看清;用 `--cw-stage-bg`
6745
- * 让 Lab 预览区与工作台舞台保持一致,宿主换主题时一起变。
6746
- * 取不到时退回深色(纯逻辑环境 / 未包 `.cw-root`)。
6747
- */
6748
- function readStageBackground() {
6749
- if (typeof document === "undefined") return "#101018";
6750
- const root = document.querySelector(".cw-root");
6751
- if (root == null) return "#101018";
6752
- const value = getComputedStyle(root).getPropertyValue("--cw-stage-bg").trim();
6753
- return value.length > 0 ? value : "#101018";
6754
- }
6755
- function ChromakeyLab() {
6756
- const [webgl2] = (0, react.useState)(_cut_workbench_core_lab.isWebGL2Available);
6757
- const [file, setFile] = (0, react.useState)(null);
6758
- const [similarity, setSimilarity] = (0, react.useState)(.4);
6759
- const [smoothness, setSmoothness] = (0, react.useState)(.1);
6760
- const [spill, setSpill] = (0, react.useState)(.1);
6761
- const [timeUs, setTimeUs] = (0, react.useState)(0);
6762
- const [durationUs, setDurationUs] = (0, react.useState)(0);
6763
- const canvasRef = (0, react.useRef)(null);
6764
- const clipRef = (0, react.useRef)(null);
6765
- const { busy, run } = useLabAction();
6766
- (0, react.useEffect)(() => {
6767
- return () => {
6768
- clipRef.current?.destroy();
6769
- clipRef.current = null;
6770
- };
6771
- }, []);
6772
- const render = (0, react.useCallback)(async () => {
6773
- const clip = clipRef.current;
6774
- const canvas = canvasRef.current;
6775
- if (clip == null || canvas == null) return;
6776
- await (0, _cut_workbench_core_lab.renderChromakeyFrame)(clip, timeUs, {
6777
- similarity,
6778
- smoothness,
6779
- spill,
6780
- keyColor: [
6781
- 0,
6782
- 255,
6783
- 0
6784
- ]
6785
- }, canvas, readStageBackground());
6786
- }, [
6787
- similarity,
6788
- smoothness,
6789
- spill,
6790
- timeUs
6791
- ]);
6792
- const handleFile = async (files) => {
6793
- const picked = files[0];
6794
- if (picked == null) return;
6795
- const result = await run("正在解析视频…", async () => {
6796
- clipRef.current?.destroy();
6797
- return await (0, _cut_workbench_core_lab.createMp4Clip)(picked);
6798
- });
6799
- if (result == null) return;
6800
- clipRef.current = result.clip;
6801
- setFile(picked);
6802
- setDurationUs(result.meta.duration);
6803
- setTimeUs(Math.min(result.meta.duration / 2, 1e6));
6804
- sonner.toast.success("已载入视频,拖动参数查看抠像效果");
6805
- };
6806
- (0, react.useEffect)(() => {
6807
- if (clipRef.current == null || busy != null) return;
6808
- render();
6809
- }, [busy, render]);
6810
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
6811
- title: "绿幕抠像",
6812
- description: "用 WebGL2 实时扣除背景色,通过 tickInterceptor 作用到整段视频。",
6813
- apis: ["createChromakey", "MP4Clip.tickInterceptor"],
6814
- unavailable: webgl2 ? null : "当前设备不支持 WebGL2,createChromakey 无法运行。",
6815
- children: [
6816
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
6817
- label: file?.name ?? "选择绿幕视频(MP4)",
6818
- accept: "video/mp4",
6819
- onFiles: (files) => void handleFile(files),
6820
- disabled: busy != null
6821
- }),
6822
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6823
- className: "relative overflow-hidden rounded-md border bg-stage-bg",
6824
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
6825
- ref: canvasRef,
6826
- className: "block w-full",
6827
- style: { maxHeight: 180 }
6828
- }), file == null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6829
- className: "pointer-events-none absolute inset-0 flex h-24 items-center justify-center text-[10px] text-muted-foreground",
6830
- children: "选择视频后显示抠像结果"
6831
- })]
6832
- }),
6833
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
6834
- label: "相似度 similarity",
6835
- value: similarity,
6836
- min: 0,
6837
- max: 1,
6838
- step: .01,
6839
- format: (v) => v.toFixed(2),
6840
- onChange: setSimilarity
6841
- }),
6842
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
6843
- label: "平滑度 smoothness",
6844
- value: smoothness,
6845
- min: 0,
6846
- max: 1,
6847
- step: .01,
6848
- format: (v) => v.toFixed(2),
6849
- onChange: setSmoothness
6850
- }),
6851
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
6852
- label: "饱和度 spill",
6853
- value: spill,
6854
- min: 0,
6855
- max: 1,
6856
- step: .01,
6857
- format: (v) => v.toFixed(2),
6858
- onChange: setSpill
6859
- }),
6860
- durationUs > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
6861
- label: "取帧时间",
6862
- value: timeUs,
6863
- min: 0,
6864
- max: Math.max(1, durationUs - 1),
6865
- step: 1e5,
6866
- format: (v) => `${(v / 1e6).toFixed(2)}s`,
6867
- onChange: setTimeUs
6868
- }),
6869
- busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
6870
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6871
- className: "text-[10px] leading-relaxed text-muted-foreground",
6872
- children: "keyColor 固定为纯绿 (0,255,0)。实际项目中可读取首帧像素自动取色。"
6873
- })
6874
- ]
6875
- });
6876
- }
6877
- // src/features/lab/components/SubtitleLab.tsx
6878
- var SAMPLE_SRT = `1
6879
- 00:00:00,000 --> 00:00:02,000
6880
- 这是第一句字幕
6881
-
6882
- 2
6883
- 00:00:02,000 --> 00:00:04,000
6884
- 第二句会带描边
6885
-
6886
- 3
6887
- 00:00:04,000 --> 00:00:06,000
6888
- EmbedSubtitlesClip 烧录进画面
6889
- `;
6890
- /**
6891
- * Lab 4:字幕烧录。
6892
- *
6893
- * `EmbedSubtitlesClip` 本身就是一个 IClip:tick 返回带字幕的 VideoFrame。
6894
- * 因此既能放进 Combinator 导出,也能直接逐帧预览。
6895
- */
6896
- function SubtitleLab() {
6897
- const [srt, setSrt] = (0, react.useState)(SAMPLE_SRT);
6898
- const [timeUs, setTimeUs] = (0, react.useState)(0);
6899
- const [playing, setPlaying] = (0, react.useState)(false);
6900
- const [result, setResult] = (0, react.useState)(null);
6901
- const { busy, run } = useLabAction();
6902
- const canvasRef = (0, react.useRef)(null);
6903
- const clipRef = (0, react.useRef)(null);
6904
- const rafRef = (0, react.useRef)(null);
6905
- const cues = (0, _cut_workbench_core.parseSrt)(srt);
6906
- const durationUs = cues.length > 0 ? cues[cues.length - 1].endMs * 1e3 : 0;
6907
- (0, react.useEffect)(() => {
6908
- clipRef.current?.destroy();
6909
- if (cues.length === 0) {
6910
- clipRef.current = null;
6911
- return;
6912
- }
6913
- try {
6914
- clipRef.current = (0, _cut_workbench_core_lab.createSubtitlePreviewClip)(srt, 640, 360);
6915
- } catch (err) {
6916
- console.warn("[SubtitleLab] create failed", err);
6917
- clipRef.current = null;
6918
- }
6919
- return () => {
6920
- clipRef.current?.destroy();
6921
- clipRef.current = null;
6922
- };
6923
- }, [cues.length, srt]);
6924
- const draw = (0, react.useCallback)(async () => {
6925
- const clip = clipRef.current;
6926
- const canvas = canvasRef.current;
6927
- if (clip == null || canvas == null) return;
6928
- await (0, _cut_workbench_core_lab.renderSubtitleFrame)(clip, timeUs, canvas);
6929
- }, [timeUs]);
6930
- (0, react.useEffect)(() => {
6931
- draw();
6932
- }, [draw]);
6933
- (0, react.useEffect)(() => {
6934
- if (!playing) return;
6935
- let last = performance.now();
6936
- let current = timeUs;
6937
- const tick = () => {
6938
- const now = performance.now();
6939
- current += (now - last) * 1e3;
6940
- last = now;
6941
- if (durationUs > 0 && current >= durationUs) {
6942
- setTimeUs(durationUs);
6943
- setPlaying(false);
6944
- return;
6945
- }
6946
- setTimeUs(current);
6947
- rafRef.current = requestAnimationFrame(tick);
6948
- };
6949
- rafRef.current = requestAnimationFrame(tick);
6950
- return () => {
6951
- if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
6952
- };
6953
- }, [playing, durationUs]);
6954
- const handleBurn = async () => {
6955
- const width = 1280;
6956
- const height = 720;
6957
- const output = await run("正在烧录字幕…", () => (0, _cut_workbench_core_lab.burnSubtitles)(srt, width, height, durationUs));
6958
- if (output != null) setResult(output);
6959
- };
6960
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
6961
- title: "字幕烧录",
6962
- description: "SRT 解析后逐帧绘制到画面,最终编码进视频流(硬字幕)。",
6963
- apis: ["EmbedSubtitlesClip", "Combinator"],
6964
- children: [
6965
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
6966
- value: srt,
6967
- onChange: (e) => setSrt(e.target.value),
6968
- rows: 5,
6969
- className: "w-full rounded-md border bg-transparent p-2 font-mono text-[10px] outline-none focus-visible:border-ring",
6970
- placeholder: "粘贴 SRT 内容"
6971
- }),
6972
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6973
- className: "overflow-hidden rounded-md border bg-stage-bg",
6974
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
6975
- ref: canvasRef,
6976
- width: 640,
6977
- height: 360,
6978
- className: "block w-full",
6979
- style: { maxHeight: 180 }
6980
- })
6981
- }),
6982
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6983
- className: "flex items-center gap-2",
6984
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6985
- type: "button",
6986
- onClick: () => setPlaying((p) => !p),
6987
- disabled: cues.length === 0,
6988
- className: "flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-[11px] disabled:opacity-50",
6989
- children: [playing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, { className: "size-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Play, { className: "size-3" }), playing ? "停止" : "预览"]
6990
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6991
- className: "text-[10px] text-muted-foreground",
6992
- children: [
6993
- cues.length,
6994
- " 条 · ",
6995
- (timeUs / 1e6).toFixed(2),
6996
- "s / ",
6997
- (durationUs / 1e6).toFixed(2),
6998
- "s"
6999
- ]
7000
- })]
7001
- }),
7002
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
7003
- label: "时间",
7004
- value: timeUs,
7005
- min: 0,
7006
- max: Math.max(1, durationUs),
7007
- step: 1e4,
7008
- format: (v) => `${(v / 1e6).toFixed(2)}s`,
7009
- onChange: (v) => {
7010
- setPlaying(false);
7011
- setTimeUs(v);
7012
- }
7013
- }),
7014
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7015
- type: "button",
7016
- disabled: cues.length === 0 || busy != null,
7017
- onClick: () => void handleBurn(),
7018
- className: "rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
7019
- children: "烧录并导出 MP4"
7020
- }),
7021
- busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
7022
- result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
7023
- blob: result,
7024
- filename: "subtitles.mp4"
7025
- })
7026
- ]
7027
- });
7028
- }
7029
- // src/features/lab/components/TrackSplitLab.tsx
7030
- /**
7031
- * Lab 5:轨道拆分。
7032
- *
7033
- * `splitTrack()` 从一个 MP4Clip 派生出「纯视频」与「纯音频」两个 clip,
7034
- * 它们与源共享样本表,因此内存开销很小。
7035
- */
7036
- function TrackSplitLab() {
7037
- const [file, setFile] = (0, react.useState)(null);
7038
- const [result, setResult] = (0, react.useState)(null);
7039
- const { busy, run } = useLabAction();
7040
- const cleanupRef = (0, react.useRef)(null);
7041
- (0, react.useEffect)(() => {
7042
- return () => cleanupRef.current?.();
7043
- }, []);
7044
- const handleSplit = async () => {
7045
- if (file == null) return;
7046
- cleanupRef.current?.();
7047
- const output = await run("正在拆分轨道…", () => (0, _cut_workbench_core_lab.splitMp4Tracks)(file));
7048
- setResult(output);
7049
- cleanupRef.current = () => {
7050
- output?.videoClip?.destroy();
7051
- output?.audioClip?.destroy();
7052
- };
7053
- };
7054
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
7055
- title: "轨道拆分",
7056
- description: "把一个 MP4 拆成纯视频轨与纯音频轨两个独立 IClip,用于分别编辑。",
7057
- apis: ["MP4Clip.splitTrack"],
7058
- children: [
7059
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
7060
- label: file?.name ?? "选择 MP4",
7061
- accept: "video/mp4",
7062
- onFiles: (files) => {
7063
- setFile(files[0] ?? null);
7064
- setResult(null);
7065
- },
7066
- disabled: busy != null
7067
- }),
7068
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7069
- type: "button",
7070
- disabled: file == null || busy != null,
7071
- onClick: () => void handleSplit(),
7072
- 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",
7073
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Split, { className: "size-3" }), "拆分轨道"]
7074
- }),
7075
- busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
7076
- result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7077
- className: "grid gap-1.5 text-[10px]",
7078
- children: [
7079
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrackResult, {
7080
- label: "视频轨",
7081
- present: result.videoMeta != null,
7082
- detail: result.videoMeta != null ? `${result.videoMeta.width}×${result.videoMeta.height} · ${(0, _cut_workbench_core.formatDuration)(result.videoMeta.duration)}` : "源文件无视频轨"
7083
- }),
7084
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrackResult, {
7085
- label: "音频轨",
7086
- present: result.audioMeta != null,
7087
- detail: result.audioMeta != null ? `48kHz 双声道 · ${(0, _cut_workbench_core.formatDuration)(result.audioMeta.duration)}` : "源文件无音频轨"
7088
- }),
7089
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7090
- className: "pt-1 leading-relaxed text-muted-foreground",
7091
- children: "拆分出的 clip 与源共享样本表,可分别创建 Sprite 放到不同轨道;两者在时间轴上对齐即等于原片。"
7092
- })
7093
- ]
7094
- })
7095
- ]
7096
- });
7097
- }
7098
- function TrackResult({ label, present, detail }) {
7099
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7100
- className: "flex items-center justify-between gap-2 rounded-md border px-2 py-1.5",
7101
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7102
- className: present ? "text-foreground" : "text-muted-foreground",
7103
- children: label
7104
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7105
- className: present ? "font-mono text-success" : "text-muted-foreground",
7106
- children: present ? detail : detail
7107
- })]
7108
- });
7109
- }
7110
- // src/features/lab/components/ThumbnailLab.tsx
7111
- /**
7112
- * Lab 6:缩略图抽帧。
7113
- *
7114
- * `MP4Clip.thumbnails()` 默认每个关键帧生成一张;也可以传 step 按固定间隔取。
7115
- * 这里展示全部关键帧与时间戳。
7116
- */
7117
- function ThumbnailLab() {
7118
- const [file, setFile] = (0, react.useState)(null);
7119
- const [frames, setFrames] = (0, react.useState)([]);
7120
- const [width, setWidth] = (0, react.useState)(120);
7121
- const { busy, run } = useLabAction();
7122
- (0, react.useEffect)(() => {
7123
- return () => {
7124
- for (const frame of frames) URL.revokeObjectURL(frame.url);
7125
- };
7126
- }, [frames]);
7127
- const handleExtract = async () => {
7128
- if (file == null) return;
7129
- const output = await run("正在抽取关键帧…", () => (0, _cut_workbench_core_lab.extractKeyframes)(file, width));
7130
- if (output != null) setFrames(output);
7131
- };
7132
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
7133
- title: "缩略图抽帧",
7134
- description: "按关键帧抽取缩略图,可用于时间轴缩略图带或场景切分预览。",
7135
- apis: ["MP4Clip.thumbnails"],
7136
- children: [
7137
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilePickButton, {
7138
- label: file?.name ?? "选择 MP4",
7139
- accept: "video/mp4",
7140
- onFiles: (files) => {
7141
- setFile(files[0] ?? null);
7142
- setFrames([]);
7143
- },
7144
- disabled: busy != null
7145
- }),
7146
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7147
- className: "flex items-center gap-2",
7148
- children: [
7149
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
7150
- className: "text-[10px] text-muted-foreground",
7151
- children: "宽度"
7152
- }),
7153
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
7154
- type: "number",
7155
- min: 40,
7156
- max: 480,
7157
- step: 20,
7158
- value: width,
7159
- onChange: (e) => setWidth(Number(e.target.value)),
7160
- className: "h-6 w-16 rounded border bg-transparent px-1.5 text-[11px]"
7161
- }),
7162
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7163
- type: "button",
7164
- disabled: file == null || busy != null,
7165
- onClick: () => void handleExtract(),
7166
- className: "ml-auto rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
7167
- children: "抽取"
7168
- })
7169
- ]
7170
- }),
7171
- busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
7172
- frames.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
7173
- className: "text-[10px] text-muted-foreground",
7174
- children: [
7175
- "共 ",
7176
- frames.length,
7177
- " 张关键帧"
7178
- ]
7179
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7180
- className: "thin-scrollbar grid max-h-52 grid-cols-3 gap-1.5 overflow-auto",
7181
- children: frames.map((frame, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
7182
- className: "space-y-0.5",
7183
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
7184
- src: frame.url,
7185
- alt: "",
7186
- className: "w-full rounded border object-cover",
7187
- loading: "lazy"
7188
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
7189
- className: "font-mono text-[9px] text-muted-foreground tabular-nums",
7190
- children: (0, _cut_workbench_core.formatTimecode)(frame.ts)
7191
- })]
7192
- }, `${frame.ts}-${index}`))
7193
- })] })
7194
- ]
7195
- });
7196
- }
7197
- // src/features/lab/components/RecorderLab.tsx
7198
- /**
7199
- * Lab 7:屏幕 / 画布录制。
7200
- *
7201
- * `AVRecorder` 接收任意 MediaStream 并输出 MP4(H.264 + AAC)。
7202
- * 这里提供两种源:
7203
- * - 程序化画布(Canvas.captureStream,无需授权);
7204
- * - 摄像头(getUserMedia,需要用户授权)。
7205
- */
7206
- function RecorderLab() {
7207
- const [mode, setMode] = (0, react.useState)("demo");
7208
- const [recording, setRecording] = (0, react.useState)(false);
7209
- const [paused, setPaused] = (0, react.useState)(false);
7210
- const [bytes, setBytes] = (0, react.useState)(0);
7211
- const [state, setState] = (0, react.useState)("inactive");
7212
- const [result, setResult] = (0, react.useState)(null);
7213
- const [previewStream, setPreviewStream] = (0, react.useState)(null);
7214
- const videoRef = (0, react.useRef)(null);
7215
- const recorderRef = (0, react.useRef)(null);
7216
- const demoRef = (0, react.useRef)(null);
7217
- const cameraRef = (0, react.useRef)(null);
7218
- const { run } = useLabAction();
7219
- (0, react.useEffect)(() => {
7220
- const video = videoRef.current;
7221
- if (video == null) return;
7222
- video.srcObject = previewStream;
7223
- if (previewStream != null) video.play().catch(() => void 0);
7224
- return () => {
7225
- if (video.srcObject === previewStream) video.srcObject = null;
7226
- };
7227
- }, [previewStream]);
7228
- const cleanup = (0, react.useCallback)(() => {
7229
- demoRef.current?.stop();
7230
- demoRef.current = null;
7231
- cameraRef.current?.getTracks().forEach((t) => t.stop());
7232
- cameraRef.current = null;
7233
- recorderRef.current?.dispose();
7234
- recorderRef.current = null;
7235
- setPreviewStream(null);
7236
- }, []);
7237
- (0, react.useEffect)(() => cleanup, [cleanup]);
7238
- const acquireStream = async () => {
7239
- cleanup();
7240
- if (mode === "demo") {
7241
- const demo = (0, _cut_workbench_core_lab.createDemoCanvasStream)(1280, 720);
7242
- demoRef.current = demo;
7243
- setPreviewStream(demo.stream);
7244
- return demo.stream;
7245
- }
7246
- try {
7247
- const stream = await navigator.mediaDevices.getUserMedia({
7248
- video: true,
7249
- audio: true
7250
- });
7251
- cameraRef.current = stream;
7252
- setPreviewStream(stream);
7253
- return stream;
7254
- } catch (err) {
7255
- const message = err instanceof Error ? err.message : String(err);
7256
- sonner.toast.error("无法访问摄像头", { description: message });
7257
- return null;
7258
- }
7259
- };
7260
- const handleStart = async () => {
7261
- const stream = await acquireStream();
7262
- if (stream == null) return;
7263
- setBytes(0);
7264
- setResult(null);
7265
- const recorder = createRecorderAdapter(stream, (total) => setBytes(total));
7266
- recorderRef.current = recorder;
7267
- recorder.start();
7268
- setRecording(true);
7269
- setPaused(false);
7270
- setState(recorder.state());
7271
- };
7272
- const handlePauseToggle = () => {
7273
- const recorder = recorderRef.current;
7274
- if (recorder == null) return;
7275
- if (paused) {
7276
- recorder.resume();
7277
- setPaused(false);
7278
- } else {
7279
- recorder.pause();
7280
- setPaused(true);
7281
- }
7282
- setState(recorder.state());
7283
- };
7284
- const handleStop = async () => {
7285
- const recorder = recorderRef.current;
7286
- if (recorder == null) return;
7287
- const output = await run("正在封装 MP4…", async () => {
7288
- const raw = await recorder.stop();
7289
- return (0, _cut_workbench_core_lab.fixRecordedDuration)(raw);
7290
- });
7291
- setRecording(false);
7292
- setPaused(false);
7293
- setState(recorder.state());
7294
- recorderRef.current = null;
7295
- if (output != null) {
7296
- setResult(output);
7297
- sonner.toast.success(`录制完成 · ${(output.size / 1024 / 1024).toFixed(2)} MB`);
7298
- }
7299
- };
7300
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
7301
- title: "屏幕 / 画布录制",
7302
- description: "用 AVRecorder 把 MediaStream 实时编码为 MP4。可录制程序化画布或摄像头。",
7303
- apis: ["AVCanvas.captureStream", "AVRecorder.start/pause/resume/stop"],
7304
- children: [
7305
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7306
- className: "flex gap-1.5",
7307
- children: ["demo", "camera"].map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7308
- type: "button",
7309
- disabled: recording,
7310
- onClick: () => setMode(item),
7311
- className: `flex-1 rounded-md border px-2 py-1 text-[11px] disabled:opacity-50 ${mode === item ? "border-primary bg-primary/10 text-primary" : "text-muted-foreground"}`,
7312
- children: item === "demo" ? "动态画布" : "摄像头"
7313
- }, item))
7314
- }),
7315
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7316
- className: "overflow-hidden rounded-md border bg-stage-bg",
7317
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
7318
- ref: videoRef,
7319
- muted: true,
7320
- playsInline: true,
7321
- className: "block w-full",
7322
- style: { maxHeight: 170 }
7323
- })
7324
- }),
7325
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7326
- className: "flex flex-wrap items-center gap-1.5",
7327
- children: [!recording ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7328
- type: "button",
7329
- onClick: () => void handleStart(),
7330
- className: "flex items-center gap-1 rounded-md bg-destructive px-2.5 py-1 text-[11px] text-destructive-foreground",
7331
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Circle, { className: "size-3 fill-current" }), "开始录制"]
7332
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7333
- type: "button",
7334
- onClick: handlePauseToggle,
7335
- className: "flex items-center gap-1 rounded-md bg-secondary px-2.5 py-1 text-[11px]",
7336
- children: [paused ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Play, { className: "size-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pause, { className: "size-3" }), paused ? "继续" : "暂停"]
7337
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7338
- type: "button",
7339
- onClick: () => void handleStop(),
7340
- className: "flex items-center gap-1 rounded-md bg-primary px-2.5 py-1 text-[11px] text-primary-foreground",
7341
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, { className: "size-3" }), "停止"]
7342
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7343
- className: "ml-auto font-mono text-[10px] text-muted-foreground tabular-nums",
7344
- children: [
7345
- (bytes / 1024 / 1024).toFixed(2),
7346
- " MB · ",
7347
- (0, _cut_workbench_core_lab.formatRecorderState)(state)
7348
- ]
7349
- })]
7350
- }),
7351
- result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
7352
- blob: result,
7353
- filename: "recording.mp4"
7354
- })
7355
- ]
7356
- });
7357
- }
7358
- /** 适配 RecorderHandle(隔离 AVRecorder 细节) */
7359
- function createRecorderAdapter(stream, onChunk) {
7360
- const recorder = new _cut_workbench_core_lab.AVRecorder(stream, { expectFPS: 30 });
7361
- const chunks = [];
7362
- let total = 0;
7363
- return {
7364
- start: () => {
7365
- const readable = recorder.start(300);
7366
- (async () => {
7367
- const reader = readable.getReader();
7368
- for (;;) try {
7369
- const { done, value } = await reader.read();
7370
- if (done) break;
7371
- if (value != null) {
7372
- chunks.push(new Uint8Array(value));
7373
- total += value.byteLength;
7374
- onChunk(total);
7375
- }
7376
- } catch {
7377
- break;
7378
- }
7379
- })();
7380
- },
7381
- pause: () => recorder.pause(),
7382
- resume: () => recorder.resume(),
7383
- stop: async () => {
7384
- await recorder.stop();
7385
- return new Blob(chunks, { type: "video/mp4" });
7386
- },
7387
- state: () => recorder.state,
7388
- dispose: () => {
7389
- stream.getTracks().forEach((t) => t.stop());
7390
- }
7391
- };
7392
- }
7393
- // src/features/lab/components/CustomClipLab.tsx
7394
- /**
7395
- * Lab 8:自定义 IClip。
7396
- *
7397
- * `FrameCounterClip` 是本项目实现的 IClip:不依赖任何媒体文件,
7398
- * 逐帧用 OffscreenCanvas 画出帧号 / 进度条 / 色相变化。
7399
- *
7400
- * 它证明 IClip 接口(tick / ready / meta / clone / split / destroy)可以被任意扩展,
7401
- * 且能直接被 Combinator 编码成 MP4。
7402
- */
7403
- function CustomClipLab() {
7404
- const [durationSec, setDurationSec] = (0, react.useState)(5);
7405
- const [timeUs, setTimeUs] = (0, react.useState)(0);
7406
- const [playing, setPlaying] = (0, react.useState)(false);
7407
- const [result, setResult] = (0, react.useState)(null);
7408
- const canvasRef = (0, react.useRef)(null);
7409
- const clipRef = (0, react.useRef)(null);
7410
- const rafRef = (0, react.useRef)(null);
7411
- const { busy, run } = useLabAction();
7412
- const durationUs = Math.round(durationSec * 1e6);
7413
- (0, react.useEffect)(() => {
7414
- clipRef.current?.destroy();
7415
- try {
7416
- clipRef.current = new _cut_workbench_core_lab.FrameCounterClip(640, 360, durationUs, 30);
7417
- } catch (err) {
7418
- console.warn("[CustomClipLab] create failed", err);
7419
- clipRef.current = null;
7420
- }
7421
- return () => {
7422
- clipRef.current?.destroy();
7423
- clipRef.current = null;
7424
- };
7425
- }, [durationUs]);
7426
- const draw = (0, react.useCallback)(async () => {
7427
- const clip = clipRef.current;
7428
- const canvas = canvasRef.current;
7429
- if (clip == null || canvas == null) return;
7430
- await (0, _cut_workbench_core_lab.renderClipFrameToCanvas)(clip, timeUs, canvas);
7431
- }, [timeUs]);
7432
- (0, react.useEffect)(() => {
7433
- draw();
7434
- }, [draw]);
7435
- (0, react.useEffect)(() => {
7436
- if (!playing) return;
7437
- let last = performance.now();
7438
- let current = timeUs;
7439
- const tick = () => {
7440
- const now = performance.now();
7441
- current += (now - last) * 1e3;
7442
- last = now;
7443
- if (current >= durationUs) {
7444
- setTimeUs(durationUs - 1);
7445
- setPlaying(false);
7446
- return;
7447
- }
7448
- setTimeUs(current);
7449
- rafRef.current = requestAnimationFrame(tick);
7450
- };
7451
- rafRef.current = requestAnimationFrame(tick);
7452
- return () => {
7453
- if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
7454
- };
7455
- }, [playing, durationUs]);
7456
- const handleExport = async () => {
7457
- const clip = clipRef.current;
7458
- if (clip == null) return;
7459
- const output = await run("正在编码自定义 Clip…", () => (0, _cut_workbench_core_lab.exportCustomClip)(clip, durationUs));
7460
- if (output != null) setResult(output);
7461
- };
7462
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(LabCard, {
7463
- title: "自定义 IClip",
7464
- description: "纯程序化渲染的 Clip(帧计数器),不依赖任何媒体文件,证明接口可扩展。",
7465
- apis: [
7466
- "IClip.tick",
7467
- "IClip.clone",
7468
- "IClip.split",
7469
- "Combinator"
7470
- ],
7471
- children: [
7472
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7473
- className: "overflow-hidden rounded-md border bg-stage-bg",
7474
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
7475
- ref: canvasRef,
7476
- width: 640,
7477
- height: 360,
7478
- className: "block w-full",
7479
- style: { maxHeight: 180 }
7480
- })
7481
- }),
7482
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7483
- className: "flex items-center gap-2",
7484
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7485
- type: "button",
7486
- onClick: () => {
7487
- if (!playing && timeUs >= durationUs - 1) setTimeUs(0);
7488
- setPlaying((p) => !p);
7489
- },
7490
- className: "flex items-center gap-1 rounded-md bg-secondary px-2 py-1 text-[11px]",
7491
- children: [playing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Square, { className: "size-3" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Play, { className: "size-3" }), playing ? "停止" : "预览"]
7492
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
7493
- className: "font-mono text-[10px] text-muted-foreground tabular-nums",
7494
- children: [
7495
- (0, _cut_workbench_core.formatTimecode)(timeUs),
7496
- " / ",
7497
- (0, _cut_workbench_core.formatTimecode)(durationUs)
7498
- ]
7499
- })]
7500
- }),
7501
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
7502
- label: "时间",
7503
- value: timeUs,
7504
- min: 0,
7505
- max: Math.max(1, durationUs - 1),
7506
- step: 1e4,
7507
- format: (v) => `${(v / 1e6).toFixed(2)}s`,
7508
- onChange: (v) => {
7509
- setPlaying(false);
7510
- setTimeUs(v);
7511
- }
7512
- }),
7513
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderRow, {
7514
- label: "时长",
7515
- value: durationSec,
7516
- min: 1,
7517
- max: 20,
7518
- step: .5,
7519
- format: (v) => `${v.toFixed(1)}s`,
7520
- onChange: (v) => {
7521
- setPlaying(false);
7522
- setDurationSec(v);
7523
- setTimeUs(0);
7524
- }
7525
- }),
7526
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7527
- type: "button",
7528
- disabled: busy != null,
7529
- onClick: () => void handleExport(),
7530
- className: "rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground disabled:opacity-50",
7531
- children: "编码为 MP4"
7532
- }),
7533
- busy != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Busy, { label: busy }),
7534
- result != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResultPreview, {
7535
- blob: result,
7536
- filename: "custom-clip.mp4"
7537
- })
7538
- ]
7539
- });
7540
- }
7541
- // src/features/lab/components/LabPage.tsx
7542
- /**
7543
- * 能力实验室:每个 WebAV 高级 API 一个可独立运行的卡片。
7544
- *
7545
- * 与主编辑器共享同一套能力探测结果,因此这里不再重复检测。
7546
- */
7547
- function LabPage() {
7548
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7549
- className: "mx-auto max-w-7xl space-y-4 p-4",
7550
- children: [
7551
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
7552
- className: "space-y-1",
7553
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7554
- className: "flex items-center gap-2",
7555
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FlaskConical, { className: "size-4 text-primary" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
7556
- className: "text-sm font-semibold",
7557
- children: "能力实验室"
7558
- })]
7559
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
7560
- className: "text-xs text-muted-foreground",
7561
- children: "覆盖 av-cliper / av-canvas / av-recorder 的进阶 API。每个卡片可独立运行,失败时给出可读原因。"
7562
- })]
7563
- }),
7564
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7565
- className: "grid gap-3 md:grid-cols-2 xl:grid-cols-3",
7566
- children: [
7567
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConcatenateLab, {}),
7568
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RemuxLab, {}),
7569
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChromakeyLab, {}),
7570
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleLab, {}),
7571
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrackSplitLab, {}),
7572
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ThumbnailLab, {}),
7573
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecorderLab, {}),
7574
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomClipLab, {})
7575
- ]
7576
- }),
7577
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
7578
- className: "border-t pt-3 text-[10px] leading-relaxed text-muted-foreground",
7579
- children: "说明:所有输出均为 MP4(H.264 + AAC 48kHz 双声道)。WebAV 生成的是 fMP4 分片, 部分播放器需「修复时长」;浏览器本地处理,文件不会上传。"
7580
- })
7581
- ]
7582
- });
7583
- }
7584
6434
  // src/CutWorkbench.tsx
7585
6435
  /**
7586
6436
  * 工作台容器:把「三栏骨架 + 顶栏 + 弹窗」组装成一个可配置组件。
@@ -7592,11 +6442,22 @@ function LabPage() {
7592
6442
  * - 不强制能力阻断页(宿主可自行决定)。
7593
6443
  */
7594
6444
  /**
6445
+ * Lab 的懒加载器。
6446
+ *
6447
+ * ⚠️ 用**字面量**模块说明符做动态 import,而不是先从 `./features/lab` 拿组件:
6448
+ * 前者会被打包成 external(`import('@cut-workbench/workbench/lab')`),由宿主
6449
+ * 打包器解析;后者会把 lab 整棵子树(含 `@webav/av-recorder`)拉回本包的模块图。
6450
+ *
6451
+ * 解析到 lab 还是 stub,由宿主的 `resolve.alias` 决定 —— 这是移除 lab 的
6452
+ * 唯一可靠方式(运行时开关删不掉已产出的 chunk,见 src/lab.ts 顶部说明)。
6453
+ */
6454
+ var LabPage = (0, react.lazy)(() => import("@cut-workbench/workbench/lab").then((m) => ({ default: m.LabPage })));
6455
+ /**
7595
6456
  * 工作台主体。
7596
6457
  *
7597
6458
  * 必须在 `<WorkbenchProvider>` + `<CutEditorProvider>` 之内使用。
7598
6459
  */
7599
- function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceCapability = true }) {
6460
+ function CutWorkbench({ slots, toolbar: toolbarOptions, className, onSaveProject, onOpenProject, enforceCapability = true }) {
7600
6461
  const editor = (0, _cut_workbench_react.useCutEditor)();
7601
6462
  const capability = useCapabilityInfo();
7602
6463
  const leftWidth = useUi((s) => s.leftWidth);
@@ -7660,10 +6521,18 @@ function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceC
7660
6521
  onExport: handleExport,
7661
6522
  onSaveProject: onSaveProject != null ? handleSave : void 0,
7662
6523
  onOpenProject: onOpenProject != null ? handleOpen : void 0,
6524
+ brand: toolbarOptions?.brand,
6525
+ title: toolbarOptions?.title,
6526
+ status: toolbarOptions?.status,
6527
+ actions: toolbarOptions?.actions,
6528
+ showCapabilityBadge: toolbarOptions?.showCapabilityBadge,
6529
+ editableTitle: toolbarOptions?.editableTitle,
6530
+ labels: toolbarOptions?.labels,
7663
6531
  showLab: config.features.lab
7664
6532
  });
7665
6533
  }, [
7666
6534
  slots?.topBar,
6535
+ toolbarOptions,
7667
6536
  capability,
7668
6537
  handleExport,
7669
6538
  handleSave,
@@ -7690,7 +6559,17 @@ function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceC
7690
6559
  toolbar,
7691
6560
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7692
6561
  className: "min-h-0 flex-1 overflow-auto",
7693
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabPage, {})
6562
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Suspense, {
6563
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6564
+ className: "flex h-full items-center justify-center",
6565
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadingOverlay, {
6566
+ visible: true,
6567
+ title: "正在加载能力实验室…",
6568
+ fullscreen: false
6569
+ })
6570
+ }),
6571
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabPage, {})
6572
+ })
7694
6573
  }),
7695
6574
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportDialog, {}),
7696
6575
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CanvasSettingsDialog, {
@@ -8768,6 +7647,7 @@ function initCutWorkbench(container, options = {}) {
8768
7647
  }, (0, react.createElement)(CutWorkbench, {
8769
7648
  className: options.className ?? "h-full w-full",
8770
7649
  enforceCapability: options.enforceCapability ?? true,
7650
+ toolbar: options.toolbar,
8771
7651
  onSaveProject: () => void instance.saveProject(),
8772
7652
  onOpenProject: async () => {
8773
7653
  const saved = await storage.load(namespace);