@cut-workbench/workbench 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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_notify = require("./notify-CMNwDFGw.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_notify.__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_notify.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_notify.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_notify.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_notify.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_notify.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_notify.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]
@@ -341,44 +306,6 @@ function Row$1({ label, ok, optional }) {
341
306
  })]
342
307
  });
343
308
  }
344
- // src/context.tsx
345
- /**
346
- * 工作台自身的 Context:UI 状态(面板尺寸 / 缩放 / 对话框)+ 宿主桥。
347
- *
348
- * 与 `@cut-workbench/react` 的编辑器 Context 分开:编辑器是「数据与引擎」,
349
- * 这里是「某一次挂载的布局与交互」。两者生命周期可以不同(例如宿主切换项目时
350
- * 复用同一个编辑器、只重建 UI)。
351
- */
352
- var WorkbenchContext = (0, react.createContext)(null);
353
- function useWorkbench() {
354
- const value = (0, react.useContext)(WorkbenchContext);
355
- if (value == null) throw new Error("[cut-workbench] 缺少 <WorkbenchProvider>:组件必须挂在工作台内部");
356
- return value;
357
- }
358
- /** 订阅 UI 状态(带 selector) */
359
- function useUi(selector) {
360
- const { ui } = useWorkbench();
361
- return (0, react.useSyncExternalStore)(ui.subscribe, () => selector(ui.getState()), () => selector(ui.getState()));
362
- }
363
- /** 取 UI store 本体(事件回调里读写用) */
364
- function useUiStoreApi() {
365
- return useWorkbench().ui;
366
- }
367
- /** 默认功能开关 */
368
- var DEFAULT_FEATURES = {
369
- mediaLibrary: true,
370
- preview: true,
371
- timeline: true,
372
- inspector: true,
373
- export: true,
374
- canvasSettings: true,
375
- lab: false
376
- };
377
- var DEFAULT_CONFIG = {
378
- features: DEFAULT_FEATURES,
379
- keyboard: "global",
380
- resizable: true
381
- };
382
309
  // src/components/layout/WorkbenchToolbar.tsx
383
310
  /**
384
311
  * 内置顶栏(WorkbenchToolbar)。
@@ -387,59 +314,71 @@ var DEFAULT_CONFIG = {
387
314
  * - 品牌区可替换(`brand`),不强制显示项目名;
388
315
  * - 保存/打开是可选回调,宿主不提供就不渲染按钮;
389
316
  * - 「视图切换(编辑器/实验室)」只在开启 lab 特性时显示。
317
+ *
318
+ * 可自定义的层次(由浅到深):
319
+ * 1. **文案**:`labels` 覆盖工程名占位、能力徽标等;
320
+ * 2. **局部**:`brand` / `title` / `status` / `actions` / `showCapabilityBadge`
321
+ * 逐块替换;
322
+ * 3. **整条**:`slots.topBar`(传 `null` 表示完全不要顶栏)。
390
323
  */
391
324
  /** 顶栏:品牌 / 工程名 / 撤销重做 / 保存 / 导出 / 能力徽标 / 视图切换 */
392
- function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject, brand, showLab = false }) {
325
+ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject, brand, title, status, actions, showCapabilityBadge = true, showLab = false, editableTitle = false, labels }) {
393
326
  const projectName = (0, _cut_workbench_react.useEditor)((s) => s.project.name);
327
+ const renameProject = (0, _cut_workbench_react.useEditor)((s) => s.renameProject);
394
328
  const dirty = (0, _cut_workbench_react.useEditor)((s) => s.dirty);
395
329
  const undo = (0, _cut_workbench_react.useEditor)((s) => s.undo);
396
330
  const redo = (0, _cut_workbench_react.useEditor)((s) => s.redo);
397
331
  const canUndo = (0, _cut_workbench_react.useHistory)((s) => s.past.length > 0);
398
332
  const canRedo = (0, _cut_workbench_react.useHistory)((s) => s.future.length > 0);
399
- const route = useUi((s) => s.route);
400
- const setRoute = useUi((s) => s.setRoute);
401
- const openDialog = useUi((s) => s.openDialog);
333
+ const route = require_notify.useUi((s) => s.route);
334
+ const setRoute = require_notify.useUi((s) => s.setRoute);
335
+ const openDialog = require_notify.useUi((s) => s.openDialog);
402
336
  const clipCount = (0, _cut_workbench_react.useEditor)((s) => s.project.clips.length);
403
337
  const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
404
338
  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
- })]
339
+ const text = {
340
+ untitledProject: "未命名工程",
341
+ emptyProject: "空工程",
342
+ export: "导出",
343
+ canvasSettings: "画布设置",
344
+ showLab: "能力实验室",
345
+ backToEditor: "返回编辑器",
346
+ save: "保存工程",
347
+ open: "加载工程",
348
+ undo: "撤销",
349
+ redo: "重做",
350
+ ...labels
351
+ };
352
+ const branding = brand == null ? null : brand;
353
+ const projectTitle = title === null ? null : title ?? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProjectTitle, {
354
+ name: projectName,
355
+ dirty,
356
+ placeholder: text.untitledProject,
357
+ editable: editableTitle,
358
+ onRename: renameProject
359
+ });
360
+ const statusNode = status === null ? null : status ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
361
+ className: "hidden text-xs text-muted-foreground sm:inline",
362
+ children: [
363
+ clipCount > 0 ? `${clipCount} 个片段` : text.emptyProject,
364
+ " ·",
365
+ " ",
366
+ (0, _cut_workbench_core.ratioLabel)(projectWidth, projectHeight)
367
+ ]
414
368
  });
415
369
  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",
370
+ className: "flex h-12 shrink-0 items-center gap-2 overflow-hidden border-b bg-card px-3",
371
+ "data-workbench-toolbar": true,
417
372
  children: [
418
373
  branding,
419
- branding != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
374
+ branding != null && projectTitle != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
420
375
  orientation: "vertical",
421
376
  className: "h-5"
422
377
  }),
378
+ projectTitle,
379
+ statusNode,
423
380
  /* @__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",
381
+ className: "ml-auto flex shrink-0 items-center gap-1",
443
382
  children: [
444
383
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
445
384
  asChild: true,
@@ -448,10 +387,10 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
448
387
  size: "icon-sm",
449
388
  onClick: undo,
450
389
  disabled: !canUndo,
451
- "aria-label": "撤销",
390
+ "aria-label": text.undo,
452
391
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Undo2, { className: "size-4" })
453
392
  })
454
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "撤销 (Ctrl+Z)" })] }),
393
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipContent, { children: [text.undo, " (Ctrl+Z)"] })] }),
455
394
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
456
395
  asChild: true,
457
396
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
@@ -459,10 +398,10 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
459
398
  size: "icon-sm",
460
399
  onClick: redo,
461
400
  disabled: !canRedo,
462
- "aria-label": "重做",
401
+ "aria-label": text.redo,
463
402
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Redo2, { className: "size-4" })
464
403
  })
465
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "重做 (Ctrl+Shift+Z)" })] }),
404
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(TooltipContent, { children: [text.redo, " (Ctrl+Shift+Z)"] })] }),
466
405
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
467
406
  orientation: "vertical",
468
407
  className: "mx-1 h-5"
@@ -473,29 +412,29 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
473
412
  variant: "ghost",
474
413
  size: "icon-sm",
475
414
  onClick: onOpenProject,
476
- "aria-label": "加载工程",
415
+ "aria-label": text.open,
477
416
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.FolderOpen, { className: "size-4" })
478
417
  })
479
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "加载已保存工程" })] }),
418
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: text.open })] }),
480
419
  onSaveProject != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
481
420
  asChild: true,
482
421
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
483
422
  variant: "ghost",
484
423
  size: "icon-sm",
485
424
  onClick: onSaveProject,
486
- "aria-label": "保存工程",
425
+ "aria-label": text.save,
487
426
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Save, { className: "size-4" })
488
427
  })
489
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "保存工程到本地" })] }),
428
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: text.save })] }),
490
429
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
491
430
  variant: "ghost",
492
431
  size: "sm",
493
432
  className: "gap-1.5",
494
- "aria-label": "画布设置",
433
+ "aria-label": text.canvasSettings,
495
434
  onClick: () => openDialog("projectSettings"),
496
435
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Monitor, { className: "size-4" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
497
436
  className: "hidden md:inline",
498
- children: "画布设置"
437
+ children: text.canvasSettings
499
438
  })]
500
439
  }),
501
440
  showLab && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
@@ -505,21 +444,67 @@ function WorkbenchToolbar({ capability, onExport, onSaveProject, onOpenProject,
505
444
  onClick: () => setRoute(route === "lab" ? "editor" : "lab"),
506
445
  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
446
  className: "hidden md:inline",
508
- children: route === "lab" ? "返回编辑器" : "能力实验室"
447
+ children: route === "lab" ? text.backToEditor : text.showLab
509
448
  })]
510
449
  }),
511
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityBadge, { info: capability }),
450
+ showCapabilityBadge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityBadge, { info: capability }),
451
+ actions,
512
452
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
513
453
  size: "sm",
514
454
  className: "ml-1 gap-1.5",
515
455
  onClick: onExport,
516
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "size-4" }), "导出"]
456
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Download, { className: "size-4" }), text.export]
517
457
  })
518
458
  ]
519
459
  })
520
460
  ]
521
461
  });
522
462
  }
463
+ /**
464
+ * 工程名展示(可选双击重命名)。
465
+ *
466
+ * 重命名走编辑器 store 的 `renameProject`,因此会进撤销栈、并标记 dirty ——
467
+ * 直接改 DOM 或本地 state 会让「保存了什么」和「看到什么」对不上。
468
+ */
469
+ function ProjectTitle({ name, dirty, placeholder, editable, onRename }) {
470
+ const [editing, setEditing] = (0, react.useState)(false);
471
+ const [draft, setDraft] = (0, react.useState)(name);
472
+ const commit = () => {
473
+ setEditing(false);
474
+ const next = draft.trim();
475
+ if (next.length > 0 && next !== name) onRename(next);
476
+ };
477
+ if (editing) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
478
+ autoFocus: true,
479
+ value: draft,
480
+ onChange: (e) => setDraft(e.target.value),
481
+ onBlur: commit,
482
+ onKeyDown: (e) => {
483
+ if (e.key === "Enter") commit();
484
+ if (e.key === "Escape") {
485
+ setDraft(name);
486
+ setEditing(false);
487
+ }
488
+ },
489
+ 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",
490
+ "aria-label": "工程名称"
491
+ });
492
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
493
+ className: "flex min-w-0 items-center gap-1.5",
494
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
495
+ className: require_notify.cn("truncate text-sm font-medium", editable && "cursor-text"),
496
+ title: editable ? "双击重命名" : name.length > 0 ? name : placeholder,
497
+ onDoubleClick: editable ? () => {
498
+ setDraft(name.length > 0 ? name : "");
499
+ setEditing(true);
500
+ } : void 0,
501
+ children: name.length > 0 ? name : placeholder
502
+ }), dirty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
503
+ className: "size-1.5 shrink-0 rounded-full bg-primary",
504
+ title: "未保存"
505
+ })]
506
+ });
507
+ }
523
508
  // src/components/layout/use-panel-resize.ts
524
509
  /**
525
510
  * 面板拖拽调整。内部用 rAF 节流,松手才把最终值写回 store(避免每帧重渲染整棵树)。
@@ -598,44 +583,8 @@ function PanelResizer({ orientation, value, min, max, invert, onChange, classNam
598
583
  "aria-orientation": isVertical ? "vertical" : "horizontal",
599
584
  "aria-label": label,
600
585
  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
586
+ className: require_notify.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),
587
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: require_notify.cn("absolute", isVertical ? "-left-1 -right-1 inset-y-0" : "-top-1 -bottom-1 inset-x-0") })
639
588
  });
640
589
  }
641
590
  // src/components/common/LoadingOverlay.tsx
@@ -643,7 +592,7 @@ function CardContent({ className, ...props }) {
643
592
  function LoadingOverlay({ visible, title = "处理中…", description, progress, className, fullscreen, children }) {
644
593
  if (!visible) return null;
645
594
  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),
595
+ className: require_notify.cn("z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm", fullscreen ? "fixed inset-0" : "absolute inset-0", className),
647
596
  role: "status",
648
597
  "aria-live": "polite",
649
598
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -705,7 +654,12 @@ var CHECKS = [
705
654
  hint: "绿幕抠像(可选)"
706
655
  }
707
656
  ];
708
- /** 不满足运行条件时的阻断页:逐项说明缺失能力,不做静默降级 */
657
+ /**
658
+ * 不满足运行条件时的阻断页:逐项说明缺失能力,不做静默降级。
659
+ *
660
+ * 「依赖没装」与「浏览器不支持」要分开呈现 —— 前者是宿主的构建问题(可执行
661
+ * 安装命令解决),后者只能换浏览器。混在一起会让人对着一条装包报错去升浏览器。
662
+ */
709
663
  function UnsupportedBrowser({ report, loading, error }) {
710
664
  if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
711
665
  className: "relative flex min-h-screen items-center justify-center bg-background",
@@ -715,23 +669,46 @@ function UnsupportedBrowser({ report, loading, error }) {
715
669
  fullscreen: false
716
670
  })
717
671
  });
672
+ const missing = report?.missingDependencies ?? [];
673
+ const dependenciesMissing = missing.length > 0;
718
674
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
719
675
  className: "flex min-h-screen items-center justify-center bg-background p-6",
720
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
676
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.Card, {
721
677
  className: "w-full max-w-2xl",
722
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
678
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.CardHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
723
679
  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, {
680
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AlertOctagon, { className: "size-5" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.CardTitle, {
725
681
  className: "text-base",
726
- children: "当前浏览器无法运行本应用"
682
+ children: dependenciesMissing ? "缺少引擎依赖" : "当前浏览器无法运行本应用"
727
683
  })]
728
684
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
729
685
  className: "text-sm text-muted-foreground",
730
- children: "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
731
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardContent, {
686
+ children: dependenciesMissing ? "工作台的预览与导出依赖 WebAV(@webav/av-cliper、@webav/av-canvas)。当前项目里没有解析到它们。" : "WebAV 依赖 WebCodecs 与 OPFS,仅支持 Chromium 内核浏览器。请使用最新版 Chrome 或 Edge(非无痕模式)重新打开。"
687
+ })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(require_notify.CardContent, {
732
688
  className: "space-y-4",
733
689
  children: [
734
- report != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
690
+ dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
691
+ className: "space-y-2 rounded-md border border-destructive/40 bg-destructive/5 p-3",
692
+ children: [
693
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
694
+ className: "text-xs font-medium text-destructive",
695
+ children: ["未解析到:", missing.join("、")]
696
+ }),
697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
698
+ className: "text-xs text-muted-foreground",
699
+ children: "在宿主项目里执行:"
700
+ }),
701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
702
+ className: "overflow-x-auto rounded bg-muted px-3 py-2 text-xs",
703
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: _cut_workbench_core_engine.ENGINE_DEPENDENCIES_INSTALL_COMMAND })
704
+ }),
705
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
706
+ className: "text-xs text-muted-foreground",
707
+ children: "安装后需要重新跑一次构建 —— 缺失依赖只在构建期解析,浏览器里刷新页面没用。"
708
+ })
709
+ ]
710
+ }),
711
+ report != null && !dependenciesMissing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
735
712
  className: "grid gap-2 sm:grid-cols-2",
736
713
  children: CHECKS.map((check) => {
737
714
  const value = report[check.key];
@@ -801,7 +778,7 @@ var Toaster = (props) => {
801
778
  function Textarea({ className, ...props }) {
802
779
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
803
780
  "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),
781
+ className: require_notify.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
782
  ...props
806
783
  });
807
784
  }
@@ -809,7 +786,7 @@ function Textarea({ className, ...props }) {
809
786
  function Label({ className, ...props }) {
810
787
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Label.Root, {
811
788
  "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),
789
+ className: require_notify.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
790
  ...props
814
791
  });
815
792
  }
@@ -817,7 +794,7 @@ function Label({ className, ...props }) {
817
794
  function ScrollArea({ className, children, ...props }) {
818
795
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.ScrollArea.Root, {
819
796
  "data-slot": "scroll-area",
820
- className: cn("relative", className),
797
+ className: require_notify.cn("relative", className),
821
798
  ...props,
822
799
  children: [
823
800
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.Viewport, {
@@ -834,7 +811,7 @@ function ScrollBar({ className, orientation = "vertical", ...props }) {
834
811
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaScrollbar, {
835
812
  "data-slot": "scroll-area-scrollbar",
836
813
  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),
814
+ className: require_notify.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
815
  ...props,
839
816
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ScrollArea.ScrollAreaThumb, {
840
817
  "data-slot": "scroll-area-thumb",
@@ -873,7 +850,7 @@ function DialogPortal({ ...props }) {
873
850
  function DialogOverlay({ className, ...props }) {
874
851
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Overlay, {
875
852
  "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),
853
+ className: require_notify.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
854
  ...props
878
855
  });
879
856
  }
@@ -882,7 +859,7 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
882
859
  "data-slot": "dialog-portal",
883
860
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogOverlay, {}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Content, {
884
861
  "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),
862
+ className: require_notify.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
863
  ...props,
887
864
  children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Dialog.Close, {
888
865
  "data-slot": "dialog-close",
@@ -898,14 +875,14 @@ function DialogContent({ className, children, showCloseButton = true, ...props }
898
875
  function DialogHeader({ className, ...props }) {
899
876
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
900
877
  "data-slot": "dialog-header",
901
- className: cn("flex flex-col gap-2 text-center sm:text-left", className),
878
+ className: require_notify.cn("flex flex-col gap-2 text-center sm:text-left", className),
902
879
  ...props
903
880
  });
904
881
  }
905
882
  function DialogFooter({ className, showCloseButton = false, children, ...props }) {
906
883
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
907
884
  "data-slot": "dialog-footer",
908
- className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
885
+ className: require_notify.cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className),
909
886
  ...props,
910
887
  children: [children, showCloseButton && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Close, {
911
888
  asChild: true,
@@ -919,14 +896,14 @@ function DialogFooter({ className, showCloseButton = false, children, ...props }
919
896
  function DialogTitle({ className, ...props }) {
920
897
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Title, {
921
898
  "data-slot": "dialog-title",
922
- className: cn("text-lg leading-none font-semibold", className),
899
+ className: require_notify.cn("text-lg leading-none font-semibold", className),
923
900
  ...props
924
901
  });
925
902
  }
926
903
  function DialogDescription({ className, ...props }) {
927
904
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Dialog.Description, {
928
905
  "data-slot": "dialog-description",
929
- className: cn("text-sm text-muted-foreground", className),
906
+ className: require_notify.cn("text-sm text-muted-foreground", className),
930
907
  ...props
931
908
  });
932
909
  }
@@ -955,7 +932,7 @@ function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
955
932
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Content, {
956
933
  "data-slot": "dropdown-menu-content",
957
934
  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),
935
+ className: require_notify.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
936
  ...props
960
937
  }) });
961
938
  }
@@ -964,7 +941,7 @@ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
964
941
  "data-slot": "dropdown-menu-item",
965
942
  "data-inset": inset,
966
943
  "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),
944
+ className: require_notify.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
945
  ...props
969
946
  });
970
947
  }
@@ -972,14 +949,14 @@ function DropdownMenuLabel({ className, inset, ...props }) {
972
949
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Label, {
973
950
  "data-slot": "dropdown-menu-label",
974
951
  "data-inset": inset,
975
- className: cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
952
+ className: require_notify.cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className),
976
953
  ...props
977
954
  });
978
955
  }
979
956
  function DropdownMenuSeparator({ className, ...props }) {
980
957
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.DropdownMenu.Separator, {
981
958
  "data-slot": "dropdown-menu-separator",
982
- className: cn("-mx-1 my-1 h-px bg-border", className),
959
+ className: require_notify.cn("-mx-1 my-1 h-px bg-border", className),
983
960
  ...props
984
961
  });
985
962
  }
@@ -987,7 +964,7 @@ function DropdownMenuSeparator({ className, ...props }) {
987
964
  /** 统一空态:图标 + 标题 + 说明 + 可选操作 */
988
965
  function EmptyState({ icon: Icon, title, description, action, className, compact }) {
989
966
  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),
967
+ className: require_notify.cn("flex flex-col items-center justify-center gap-2 text-center", compact ? "px-4 py-6" : "px-6 py-12", className),
991
968
  children: [
992
969
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
993
970
  className: "flex size-10 items-center justify-center rounded-full bg-muted text-muted-foreground",
@@ -1005,43 +982,164 @@ function EmptyState({ icon: Icon, title, description, action, className, compact
1005
982
  ]
1006
983
  });
1007
984
  }
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}`;
985
+ // src/features/media-library/lib/pick-files.ts
986
+ var INPUT_ID = "cut-workbench-file-input";
987
+ /** 内置回退:隐藏 input,选完即弃 */
988
+ function pickWithInput(accept, multiple) {
989
+ return new Promise((resolve) => {
990
+ const input = document.createElement("input");
991
+ input.type = "file";
992
+ input.id = INPUT_ID;
993
+ input.multiple = multiple;
994
+ if (accept.length > 0) input.accept = accept.join(",");
995
+ input.style.display = "none";
996
+ document.body.appendChild(input);
997
+ const finish = (files) => {
998
+ input.remove();
999
+ resolve(files);
1000
+ };
1001
+ input.addEventListener("change", () => finish(Array.from(input.files ?? [])), { once: true });
1002
+ window.addEventListener("focus", () => {
1003
+ window.setTimeout(() => {
1004
+ if (document.body.contains(input)) finish([]);
1005
+ }, 300);
1006
+ }, { once: true });
1007
+ input.click();
1008
+ });
1029
1009
  }
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`;
1010
+ /**
1011
+ * @returns 选中的文件;用户取消时为空数组
1012
+ */
1013
+ async function pickMediaFiles(bridge, options) {
1014
+ const multiple = options.multiple ?? true;
1015
+ if (bridge?.files?.pickFiles != null) {
1016
+ const result = await bridge.files.pickFiles({
1017
+ accept: options.accept,
1018
+ multiple,
1019
+ description: options.description
1020
+ });
1021
+ if (result != null) return result;
1022
+ }
1023
+ return pickWithInput(options.accept, multiple);
1034
1024
  }
1035
- /** 微秒 → 秒(保留一位小数,用于表单展示) */
1036
- function formatSeconds(us) {
1037
- return (0, _cut_workbench_core.usToSec)(us).toFixed(2);
1025
+ // src/features/media-library/hooks/useMediaImport.ts
1026
+ var ACCEPT = [
1027
+ "video/mp4",
1028
+ "audio/*",
1029
+ "image/*",
1030
+ ".srt",
1031
+ "text/plain"
1032
+ ];
1033
+ /**
1034
+ * 找出可以「重连」的素材。
1035
+ *
1036
+ * 场景:刷新后工程从 IndexedDB 恢复,但素材二进制消失(File 无法持久化)。
1037
+ * 用户重新导入同一个文件时,复用原 assetId,时间轴上已有片段立即恢复。
1038
+ * 匹配条件:同名 + 同类型 + 当前确实缺少二进制。
1039
+ */
1040
+ function findReconnectableAssetId(state, file, registry) {
1041
+ if (state.missingAssetIds.length === 0) return null;
1042
+ return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
1038
1043
  }
1039
- /** 百分比 0~1 → `42%` */
1040
- function formatPercent(ratio) {
1041
- return `${Math.round(ratio * 100)}%`;
1044
+ /**
1045
+ * 批量导入素材。
1046
+ *
1047
+ * - 串行处理,避免同时开辟多个解码器把内存打爆;
1048
+ * - 单个文件失败只影响自身,最后汇总 toast;
1049
+ * - 进度写在本地 state(不放进 store,避免高频更新引起全局重渲染);
1050
+ * - 若与「缺失源文件」的素材同名同类型,则重连而不是新增。
1051
+ */
1052
+ function useMediaImport() {
1053
+ const editor = (0, _cut_workbench_react.useCutEditor)();
1054
+ const clips = editor.runtime.clips;
1055
+ const store = editor.store;
1056
+ const [progress, setProgress] = (0, react.useState)([]);
1057
+ const [importing, setImporting] = (0, react.useState)(false);
1058
+ const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
1059
+ const replaceAsset = (0, _cut_workbench_react.useEditor)((s) => s.replaceAsset);
1060
+ const clearMissing = (0, _cut_workbench_react.useEditor)((s) => s.clearMissing);
1061
+ const selectAsset = (0, _cut_workbench_react.useEditor)((s) => s.selectAsset);
1062
+ const beginImport = require_notify.useUi((s) => s.beginImport);
1063
+ const endImport = require_notify.useUi((s) => s.endImport);
1064
+ const { bridge } = require_notify.useWorkbench();
1065
+ return {
1066
+ importing,
1067
+ progress,
1068
+ importFiles: (0, react.useCallback)(async (files) => {
1069
+ if (files.length === 0) return [];
1070
+ setImporting(true);
1071
+ beginImport();
1072
+ setProgress(files.map((f) => ({
1073
+ name: f.name,
1074
+ progress: 0
1075
+ })));
1076
+ const imported = [];
1077
+ const failures = [];
1078
+ let reconnected = 0;
1079
+ for (let i = 0; i < files.length; i += 1) {
1080
+ const file = files[i];
1081
+ if (!file) continue;
1082
+ try {
1083
+ const reconnectId = findReconnectableAssetId(store.getState(), file, clips);
1084
+ const result = await (0, _cut_workbench_core_engine.importFile)(clips, file, (p) => {
1085
+ setProgress((prev) => prev.map((item, index) => index === i ? {
1086
+ ...item,
1087
+ progress: p
1088
+ } : item));
1089
+ });
1090
+ if (reconnectId != null) {
1091
+ clips.replaceAssetSource(reconnectId, file, result.master ?? null);
1092
+ replaceAsset(reconnectId, result.asset);
1093
+ clearMissing(reconnectId);
1094
+ imported.push({
1095
+ ...result.asset,
1096
+ id: reconnectId
1097
+ });
1098
+ reconnected += 1;
1099
+ } else {
1100
+ addAsset(result.asset);
1101
+ imported.push(result.asset);
1102
+ }
1103
+ } catch (err) {
1104
+ const message = err instanceof _cut_workbench_core_engine.UnsupportedFileError ? err.message : err instanceof Error ? err.message : String(err);
1105
+ failures.push(`${file.name}:${message}`);
1106
+ setProgress((prev) => prev.map((item, index) => index === i ? {
1107
+ ...item,
1108
+ error: message
1109
+ } : item));
1110
+ }
1111
+ }
1112
+ setImporting(false);
1113
+ endImport();
1114
+ if (imported.length > 0) {
1115
+ selectAsset(imported[0]?.id ?? null);
1116
+ require_notify.notify(bridge, {
1117
+ level: "success",
1118
+ title: reconnected > 0 ? `已导入 ${imported.length} 个素材(其中 ${reconnected} 个重连到已有片段)` : `已导入 ${imported.length} 个素材`
1119
+ });
1120
+ }
1121
+ if (failures.length > 0) require_notify.notify(bridge, {
1122
+ level: "error",
1123
+ title: `${failures.length} 个文件导入失败`,
1124
+ description: failures.slice(0, 3).join("\n"),
1125
+ duration: 8e3
1126
+ });
1127
+ window.setTimeout(() => setProgress([]), 3e3);
1128
+ return imported;
1129
+ }, [
1130
+ addAsset,
1131
+ beginImport,
1132
+ bridge,
1133
+ clearMissing,
1134
+ endImport,
1135
+ replaceAsset,
1136
+ selectAsset
1137
+ ])
1138
+ };
1042
1139
  }
1043
1140
  // src/features/media-library/components/DropZone.tsx
1044
- var ACCEPT = {
1141
+ /** react-dropzone 用的 MIME → 扩展名映射 */
1142
+ var DROPZONE_ACCEPT = {
1045
1143
  "video/mp4": [".mp4", ".m4v"],
1046
1144
  "audio/*": [
1047
1145
  ".mp3",
@@ -1064,28 +1162,51 @@ var ACCEPT = {
1064
1162
  /**
1065
1163
  * 拖拽 / 点击导入区域。
1066
1164
  *
1165
+ * 两条入口:
1166
+ * - **拖拽**:走 react-dropzone(宿主无法介入,这是浏览器原生行为);
1167
+ * - **点击**:优先走 `HostBridge.files.pickFiles` —— 宿主可能不用系统文件
1168
+ * 对话框,而是从自己的素材库 / 云端 / URL 导入。宿主返回 `null`(不接管)
1169
+ * 时才回退到内置 `<input type=file>`。
1170
+ *
1067
1171
  * 注意:react-dropzone 的 accept 只做初筛,真正按 MIME + 扩展名校验在
1068
1172
  * core/media/file-reader,从而保证「拖入 .webm 得到明确错误」而不是被静默拒绝。
1069
1173
  */
1070
1174
  function DropZone({ onFiles, disabled, className, compact }) {
1071
- const { getRootProps, getInputProps, isDragActive, open } = (0, react_dropzone.useDropzone)({
1175
+ const { bridge } = require_notify.useWorkbench();
1176
+ const { getRootProps, getInputProps, isDragActive } = (0, react_dropzone.useDropzone)({
1072
1177
  onDrop: (accepted) => {
1073
1178
  if (accepted.length > 0) onFiles(accepted);
1074
1179
  },
1075
- noClick: compact,
1076
- noKeyboard: compact,
1180
+ noClick: true,
1181
+ noKeyboard: true,
1077
1182
  multiple: true,
1078
1183
  disabled,
1079
- accept: ACCEPT
1080
- });
1184
+ accept: DROPZONE_ACCEPT
1185
+ });
1186
+ const handleClick = (0, react.useCallback)(() => {
1187
+ if (disabled) return;
1188
+ pickMediaFiles(bridge, {
1189
+ accept: ACCEPT,
1190
+ multiple: true,
1191
+ description: "选择要导入的素材"
1192
+ }).then((files) => {
1193
+ if (files.length > 0) onFiles(files);
1194
+ });
1195
+ }, [
1196
+ bridge,
1197
+ disabled,
1198
+ onFiles
1199
+ ]);
1081
1200
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1082
1201
  ...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),
1202
+ onClick: handleClick,
1203
+ "data-drop-zone": "",
1204
+ className: require_notify.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
1205
  children: [
1085
1206
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", { ...getInputProps() }),
1086
1207
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1087
1208
  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 ? "松开以导入" : "拖入文件,或点击选择" })]
1209
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: require_notify.cn("text-muted-foreground", compact ? "size-3.5" : "size-4") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: compact ? "导入素材" : isDragActive ? "松开以导入" : "拖入文件,或点击选择" })]
1089
1210
  }),
1090
1211
  !compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1091
1212
  className: "text-xs text-muted-foreground",
@@ -1096,7 +1217,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
1096
1217
  className: "sr-only",
1097
1218
  onClick: (e) => {
1098
1219
  e.stopPropagation();
1099
- open();
1220
+ handleClick();
1100
1221
  },
1101
1222
  children: "选择文件"
1102
1223
  })
@@ -1107,7 +1228,7 @@ function DropZone({ onFiles, disabled, className, compact }) {
1107
1228
  function Skeleton({ className, ...props }) {
1108
1229
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1109
1230
  "data-slot": "skeleton",
1110
- className: cn("animate-pulse rounded-md bg-accent", className),
1231
+ className: require_notify.cn("animate-pulse rounded-md bg-accent", className),
1111
1232
  ...props
1112
1233
  });
1113
1234
  }
@@ -1156,7 +1277,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
1156
1277
  onKeyDown: (e) => {
1157
1278
  if (e.key === "Enter") onAddToTimeline();
1158
1279
  },
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"),
1280
+ className: require_notify.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
1281
  children: [
1161
1282
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1162
1283
  className: "relative aspect-video w-full overflow-hidden rounded-md bg-muted",
@@ -1177,7 +1298,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
1177
1298
  opacity: .9
1178
1299
  })
1179
1300
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1180
- className: cn("flex size-full items-center justify-center", kind.tone),
1301
+ className: require_notify.cn("flex size-full items-center justify-center", kind.tone),
1181
1302
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-7" })
1182
1303
  }),
1183
1304
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
@@ -1224,7 +1345,7 @@ function MediaCard({ asset, selected, missing, onSelect, onAddToTimeline, onShow
1224
1345
  children: asset.name
1225
1346
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1226
1347
  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 ? " · 含音轨" : ""]
1348
+ children: [asset.kind === "audio" ? `${require_notify.formatBytes(asset.size)} · 48kHz 双声道` : asset.kind === "text" || asset.kind === "subtitle" ? `${require_notify.formatResolution(asset.width, asset.height)}` : `${require_notify.formatResolution(asset.width, asset.height)} · ${require_notify.formatBytes(asset.size)}`, asset.hasAudio ? " · 含音轨" : ""]
1228
1349
  })]
1229
1350
  }),
1230
1351
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DropdownMenu, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DropdownMenuTrigger, {
@@ -1266,7 +1387,7 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
1266
1387
  className: "grid grid-cols-2 gap-2.5 p-2.5",
1267
1388
  children: [
1268
1389
  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"),
1390
+ className: require_notify.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
1391
  children: [
1271
1392
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Skeleton, { className: "aspect-video w-full rounded-md" }),
1272
1393
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1317,122 +1438,21 @@ function MediaGrid({ assets, selectedAssetId, missingAssetIds = [], progress, on
1317
1438
  ]
1318
1439
  });
1319
1440
  }
1320
- // src/features/media-library/hooks/useMediaImport.ts
1321
- /**
1322
- * 找出可以「重连」的素材。
1323
- *
1324
- * 场景:刷新后工程从 IndexedDB 恢复,但素材二进制消失(File 无法持久化)。
1325
- * 用户重新导入同一个文件时,复用原 assetId,时间轴上已有片段立即恢复。
1326
- * 匹配条件:同名 + 同类型 + 当前确实缺少二进制。
1327
- */
1328
- function findReconnectableAssetId(state, file, registry) {
1329
- if (state.missingAssetIds.length === 0) return null;
1330
- return state.assets.find((asset) => state.missingAssetIds.includes(asset.id) && asset.name === file.name && registry.getFile(asset.id) == null)?.id ?? null;
1331
- }
1441
+ // src/features/media-library/components/MediaLibraryPanel.tsx
1442
+ var KIND_LABEL$1 = {
1443
+ video: "视频",
1444
+ audio: "音频",
1445
+ image: "图片",
1446
+ text: "文字",
1447
+ subtitle: "字幕"
1448
+ };
1332
1449
  /**
1333
- * 批量导入素材。
1334
- *
1335
- * - 串行处理,避免同时开辟多个解码器把内存打爆;
1336
- * - 单个文件失败只影响自身,最后汇总 toast;
1337
- * - 进度写在本地 state(不放进 store,避免高频更新引起全局重渲染);
1338
- * - 若与「缺失源文件」的素材同名同类型,则重连而不是新增。
1450
+ * 素材库面板:导入区 + 新建(文字/字幕)+ 网格 + 详情。
1339
1451
  */
1340
- function useMediaImport() {
1341
- const editor = (0, _cut_workbench_react.useCutEditor)();
1342
- const clips = editor.runtime.clips;
1343
- const store = editor.store;
1344
- const [progress, setProgress] = (0, react.useState)([]);
1345
- const [importing, setImporting] = (0, react.useState)(false);
1346
- const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
1347
- const replaceAsset = (0, _cut_workbench_react.useEditor)((s) => s.replaceAsset);
1348
- const clearMissing = (0, _cut_workbench_react.useEditor)((s) => s.clearMissing);
1349
- const selectAsset = (0, _cut_workbench_react.useEditor)((s) => s.selectAsset);
1350
- const beginImport = useUi((s) => s.beginImport);
1351
- const endImport = useUi((s) => s.endImport);
1352
- return {
1353
- importing,
1354
- progress,
1355
- importFiles: (0, react.useCallback)(async (files) => {
1356
- if (files.length === 0) return [];
1357
- setImporting(true);
1358
- beginImport();
1359
- setProgress(files.map((f) => ({
1360
- name: f.name,
1361
- progress: 0
1362
- })));
1363
- const imported = [];
1364
- const failures = [];
1365
- let reconnected = 0;
1366
- for (let i = 0; i < files.length; i += 1) {
1367
- const file = files[i];
1368
- if (!file) continue;
1369
- try {
1370
- const reconnectId = findReconnectableAssetId(store.getState(), file, clips);
1371
- const result = await (0, _cut_workbench_core_engine.importFile)(clips, file, (p) => {
1372
- setProgress((prev) => prev.map((item, index) => index === i ? {
1373
- ...item,
1374
- progress: p
1375
- } : item));
1376
- });
1377
- if (reconnectId != null) {
1378
- clips.replaceAssetSource(reconnectId, file, result.master ?? null);
1379
- replaceAsset(reconnectId, result.asset);
1380
- clearMissing(reconnectId);
1381
- imported.push({
1382
- ...result.asset,
1383
- id: reconnectId
1384
- });
1385
- reconnected += 1;
1386
- } else {
1387
- addAsset(result.asset);
1388
- imported.push(result.asset);
1389
- }
1390
- } catch (err) {
1391
- const message = err instanceof _cut_workbench_core_engine.UnsupportedFileError ? err.message : err instanceof Error ? err.message : String(err);
1392
- failures.push(`${file.name}:${message}`);
1393
- setProgress((prev) => prev.map((item, index) => index === i ? {
1394
- ...item,
1395
- error: message
1396
- } : item));
1397
- }
1398
- }
1399
- setImporting(false);
1400
- endImport();
1401
- if (imported.length > 0) {
1402
- selectAsset(imported[0]?.id ?? null);
1403
- sonner.toast.success(reconnected > 0 ? `已导入 ${imported.length} 个素材(其中 ${reconnected} 个重连到已有片段)` : `已导入 ${imported.length} 个素材`);
1404
- }
1405
- if (failures.length > 0) sonner.toast.error(`${failures.length} 个文件导入失败`, {
1406
- description: failures.slice(0, 3).join("\n"),
1407
- duration: 8e3
1408
- });
1409
- window.setTimeout(() => setProgress([]), 3e3);
1410
- return imported;
1411
- }, [
1412
- addAsset,
1413
- beginImport,
1414
- clearMissing,
1415
- endImport,
1416
- replaceAsset,
1417
- selectAsset
1418
- ])
1419
- };
1420
- }
1421
- // src/features/media-library/components/MediaLibraryPanel.tsx
1422
- var KIND_LABEL$1 = {
1423
- video: "视频",
1424
- audio: "音频",
1425
- image: "图片",
1426
- text: "文字",
1427
- subtitle: "字幕"
1428
- };
1429
- /**
1430
- * 素材库面板:导入区 + 新建(文字/字幕)+ 网格 + 详情。
1431
- */
1432
- function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
1433
- const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
1434
- const selectedAssetId = (0, _cut_workbench_react.useEditor)((s) => s.selectedAssetId);
1435
- const missingAssetIds = (0, _cut_workbench_react.useEditor)((s) => s.missingAssetIds);
1452
+ function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
1453
+ const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
1454
+ const selectedAssetId = (0, _cut_workbench_react.useEditor)((s) => s.selectedAssetId);
1455
+ const missingAssetIds = (0, _cut_workbench_react.useEditor)((s) => s.missingAssetIds);
1436
1456
  const selectAsset = (0, _cut_workbench_react.useEditor)((s) => s.selectAsset);
1437
1457
  const { importing, progress, importFiles } = useMediaImport();
1438
1458
  const [metaAssetId, setMetaAssetId] = (0, react.useState)(null);
@@ -1505,11 +1525,11 @@ function MediaLibraryPanel({ onAddToTimeline, onRemoveAsset }) {
1505
1525
  }),
1506
1526
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
1507
1527
  label: "分辨率",
1508
- value: formatResolution(metaAsset.width, metaAsset.height)
1528
+ value: require_notify.formatResolution(metaAsset.width, metaAsset.height)
1509
1529
  }),
1510
1530
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
1511
1531
  label: "大小",
1512
- value: metaAsset.kind === "text" ? "—" : formatBytes(metaAsset.size)
1532
+ value: metaAsset.kind === "text" ? "—" : require_notify.formatBytes(metaAsset.size)
1513
1533
  }),
1514
1534
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
1515
1535
  label: "音轨",
@@ -1585,6 +1605,7 @@ function CreateTextDialog({ open, onOpenChange, onCreated }) {
1585
1605
  const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
1586
1606
  const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
1587
1607
  const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
1608
+ const { bridge } = require_notify.useWorkbench();
1588
1609
  const handleCreate = () => {
1589
1610
  const { asset } = (0, _cut_workbench_core_engine.createTextAsset)({
1590
1611
  registry: editor.runtime.clips,
@@ -1595,7 +1616,10 @@ function CreateTextDialog({ open, onOpenChange, onCreated }) {
1595
1616
  addAsset(asset);
1596
1617
  onCreated(asset.id);
1597
1618
  onOpenChange(false);
1598
- sonner.toast.success("已创建文字素材");
1619
+ require_notify.notify(bridge, {
1620
+ level: "success",
1621
+ title: "已创建文字素材"
1622
+ });
1599
1623
  };
1600
1624
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
1601
1625
  open,
@@ -1629,10 +1653,15 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
1629
1653
  const projectWidth = (0, _cut_workbench_react.useEditor)((s) => s.project.width);
1630
1654
  const projectHeight = (0, _cut_workbench_react.useEditor)((s) => s.project.height);
1631
1655
  const addAsset = (0, _cut_workbench_react.useEditor)((s) => s.addAsset);
1656
+ const { bridge } = require_notify.useWorkbench();
1632
1657
  const handleCreate = () => {
1633
1658
  const durationUs = (0, _cut_workbench_core.parseSrtDurationUs)(srt);
1634
1659
  if (durationUs <= 0) {
1635
- sonner.toast.error("SRT 解析失败", { description: "请检查时间轴格式:00:00:00,000 --> 00:00:02,000" });
1660
+ require_notify.notify(bridge, {
1661
+ level: "error",
1662
+ title: "SRT 解析失败",
1663
+ description: "请检查时间轴格式:00:00:00,000 --> 00:00:02,000"
1664
+ });
1636
1665
  return;
1637
1666
  }
1638
1667
  const { asset } = (0, _cut_workbench_core_engine.createSubtitleAsset)({
@@ -1645,7 +1674,10 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
1645
1674
  addAsset(asset);
1646
1675
  onCreated(asset.id);
1647
1676
  onOpenChange(false);
1648
- sonner.toast.success("已创建字幕素材");
1677
+ require_notify.notify(bridge, {
1678
+ level: "success",
1679
+ title: "已创建字幕素材"
1680
+ });
1649
1681
  };
1650
1682
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Dialog, {
1651
1683
  open,
@@ -1671,11 +1703,22 @@ function CreateSubtitleDialog({ open, onOpenChange, onCreated }) {
1671
1703
  });
1672
1704
  }
1673
1705
  // src/hooks/use-toast-safe.ts
1674
- /** 统一把异常转成可见提示,禁止静默吞掉 */
1706
+ /**
1707
+ * 把异常转成可见提示,禁止静默吞掉。
1708
+ *
1709
+ * 这是**非 hook** 版本,供事件回调 / 非组件代码调用(例如 `useAvCanvas` 的
1710
+ * 初始化失败)。它拿不到 React context,因此走 `notify` 注册的默认桥:
1711
+ * `WorkbenchProvider` 挂载时会把 `bridge` 注册进来,宿主实现了 `notify`
1712
+ * 就交给宿主,否则退回库内 toast。
1713
+ */
1675
1714
  function reportError(error, fallback = "操作失败") {
1676
1715
  const message = error instanceof Error ? error.message : String(error);
1677
1716
  console.error(error);
1678
- sonner.toast.error(fallback, { description: message });
1717
+ require_notify.notify(void 0, {
1718
+ level: "error",
1719
+ title: fallback,
1720
+ description: message
1721
+ });
1679
1722
  }
1680
1723
  // src/features/preview/hooks/useAvCanvas.ts
1681
1724
  /**
@@ -2305,7 +2348,7 @@ function PreviewStage({ attachRef, width, height, emptyHint, onCapture, onFullsc
2305
2348
  const scaleY = height > 0 ? box.h / height : 1;
2306
2349
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2307
2350
  ref: outerRef,
2308
- className: cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
2351
+ className: require_notify.cn("relative flex min-h-0 flex-1 items-center justify-center overflow-hidden bg-stage-bg", className),
2309
2352
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2310
2353
  className: "relative shrink-0 overflow-hidden rounded-md bg-black shadow-2xl ring-1 ring-border",
2311
2354
  style: {
@@ -2366,14 +2409,14 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
2366
2409
  value,
2367
2410
  min,
2368
2411
  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),
2412
+ className: require_notify.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
2413
  ...props,
2371
2414
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Track, {
2372
2415
  "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"),
2416
+ className: require_notify.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
2417
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Range, {
2375
2418
  "data-slot": "slider-range",
2376
- className: cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
2419
+ className: require_notify.cn("absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full")
2377
2420
  })
2378
2421
  }), Array.from({ length: _values.length }, (_, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Slider.Thumb, {
2379
2422
  "data-slot": "slider-thumb",
@@ -2388,7 +2431,7 @@ function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }
2388
2431
  */
2389
2432
  function Timecode({ us, showMs = true, className, muted, title }) {
2390
2433
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2391
- className: cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
2434
+ className: require_notify.cn("font-mono text-xs tabular-nums", muted && "text-muted-foreground", className),
2392
2435
  title: title ?? `${us} µs`,
2393
2436
  children: (0, _cut_workbench_core.formatTimecode)(us, showMs)
2394
2437
  });
@@ -2400,15 +2443,51 @@ var RATES = [
2400
2443
  1.5,
2401
2444
  2
2402
2445
  ];
2446
+ /**
2447
+ * 紧凑档位(按容器宽度,不是视口 —— 预览列宽由拖拽决定)。
2448
+ *
2449
+ * 阈值来自实测的控件总宽:完整形态约 680px,`< 700` 先去音量滑杆与速率、
2450
+ * `< 560` 再去时间码的「总时长」与进度条、`< 420` 再去跳首尾。
2451
+ * 只靠 `shrink-0` + `overflow:hidden` 会在 650px 左右把时间码裁掉,
2452
+ * 且**没有任何报错**,只有截图能看出来 —— 所以这里显式分级。
2453
+ */
2454
+ var FULL_PX = 700;
2455
+ var COMPACT_PX = 620;
2456
+ var TIGHT_PX = 520;
2457
+ var ULTRA_COMPACT_PX = 420;
2458
+ /** 低于此宽度连时间码都放不下:只留播放/逐帧,横向可滚 */
2459
+ var MICRO_PX = 200;
2403
2460
  /** 播放控制条:逐帧 / 跳首尾 / 播放 / 时间码 / 速率 / 音量 */
2404
2461
  function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playbackRate, disabled, onToggle, onJumpStart, onJumpEnd, onStepFrame, onSeek, onVolumeChange, onRateChange, className }) {
2462
+ const rootRef = (0, react.useRef)(null);
2463
+ const [width, setWidth] = (0, react.useState)(0);
2464
+ /**
2465
+ * 用容器宽度而不是 CSS 媒体查询。
2466
+ *
2467
+ * 预览面板宽度由左侧拖拽决定,和视口宽度无关 —— 用户把两侧拉宽时,
2468
+ * 视口一点没变,`max-[560px]:hidden` 这类媒体查询不会触发,时间码照样被压扁。
2469
+ */
2470
+ (0, react.useEffect)(() => {
2471
+ const el = rootRef.current;
2472
+ if (el == null) return;
2473
+ const ro = new ResizeObserver(() => setWidth(el.clientWidth));
2474
+ ro.observe(el);
2475
+ setWidth(el.clientWidth);
2476
+ return () => ro.disconnect();
2477
+ }, []);
2478
+ const compact = width > 0 && width < COMPACT_PX;
2479
+ const tight = width > 0 && width < TIGHT_PX;
2480
+ const ultraCompact = width > 0 && width < ULTRA_COMPACT_PX;
2481
+ const micro = width > 0 && width < MICRO_PX;
2482
+ const full = width === 0 || width >= FULL_PX;
2405
2483
  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),
2484
+ ref: rootRef,
2485
+ className: require_notify.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
2486
  children: [
2408
2487
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2409
- className: "flex items-center gap-0.5",
2488
+ className: "flex shrink-0 items-center gap-0.5",
2410
2489
  children: [
2411
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2490
+ !ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2412
2491
  asChild: true,
2413
2492
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2414
2493
  variant: "ghost",
@@ -2450,7 +2529,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
2450
2529
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.StepForward, { className: "size-3.5" })
2451
2530
  })
2452
2531
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "下一帧 (→)" })] }),
2453
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2532
+ !ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2454
2533
  asChild: true,
2455
2534
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2456
2535
  variant: "ghost",
@@ -2464,23 +2543,19 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
2464
2543
  ]
2465
2544
  }),
2466
2545
  /* @__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
- ]
2546
+ className: require_notify.cn("mx-2 flex shrink-0 items-baseline gap-1 whitespace-nowrap", ultraCompact && "mx-1", micro && "hidden"),
2547
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2548
+ us: currentUs,
2549
+ className: "text-[13px] font-medium"
2550
+ }), !tight && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2551
+ className: "text-xs text-muted-foreground",
2552
+ children: "/"
2553
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
2554
+ us: durationUs,
2555
+ muted: true
2556
+ })] })]
2482
2557
  }),
2483
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2558
+ !ultraCompact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2484
2559
  type: "range",
2485
2560
  className: "mx-1 h-1 min-w-16 flex-1 cursor-pointer appearance-none rounded-full bg-muted accent-primary",
2486
2561
  min: 0,
@@ -2492,14 +2567,14 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
2492
2567
  "aria-label": "播放进度"
2493
2568
  }),
2494
2569
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2495
- className: "flex items-center gap-1",
2570
+ className: require_notify.cn("flex shrink-0 items-center gap-1", compact && "ml-auto", micro && "hidden"),
2496
2571
  children: [
2497
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2572
+ !compact && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2498
2573
  className: "flex items-center rounded-md border p-0.5",
2499
2574
  children: RATES.map((rate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2500
2575
  type: "button",
2501
2576
  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"),
2577
+ className: require_notify.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
2578
  title: rate !== 1 ? "变速预览(不会改变导出结果)" : "正常速度",
2504
2579
  children: [rate, "×"]
2505
2580
  }, rate))
@@ -2514,7 +2589,7 @@ function PlaybackControls({ playing, currentUs, durationUs, masterVolume, playba
2514
2589
  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
2590
  })
2516
2591
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: masterVolume > 0 ? "静音预览" : "取消静音" })] }),
2517
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
2592
+ full && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Slider, {
2518
2593
  className: "w-20",
2519
2594
  min: 0,
2520
2595
  max: 1,
@@ -2563,6 +2638,7 @@ function PreviewPanel() {
2563
2638
  const setMasterVolume = (0, _cut_workbench_react.useEditor)((s) => s.setMasterVolume);
2564
2639
  const playheadUs = (0, _cut_workbench_react.useEditor)((s) => s.playheadUs);
2565
2640
  const selectedClipIds = (0, _cut_workbench_react.useEditor)((s) => s.selectedClipIds);
2641
+ const { bridge } = require_notify.useWorkbench();
2566
2642
  const canvas = useAvCanvas({
2567
2643
  width: project.width,
2568
2644
  height: project.height
@@ -2573,12 +2649,18 @@ function PreviewPanel() {
2573
2649
  if (errors.length === 0) return;
2574
2650
  const missing = errors.filter((e) => e.message.startsWith("素材文件未载入"));
2575
2651
  const other = errors.filter((e) => !e.message.startsWith("素材文件未载入"));
2576
- if (missing.length > 0) sonner.toast.warning(`${missing.length} 个片段缺少源文件,已跳过预览`, {
2652
+ if (missing.length > 0) require_notify.notify(bridge, {
2653
+ level: "warning",
2654
+ title: `${missing.length} 个片段缺少源文件,已跳过预览`,
2577
2655
  description: "刷新后素材二进制不会保留,请重新导入相同文件",
2578
2656
  duration: 8e3
2579
2657
  });
2580
- if (other.length > 0) sonner.toast.error("部分片段无法载入预览", { description: Array.from(new Set(other.map((e) => e.message))).slice(0, 3).join("\n") });
2581
- }, []);
2658
+ if (other.length > 0) require_notify.notify(bridge, {
2659
+ level: "error",
2660
+ title: "部分片段无法载入预览",
2661
+ description: Array.from(new Set(other.map((e) => e.message))).slice(0, 3).join("\n")
2662
+ });
2663
+ }, [bridge]);
2582
2664
  const clock = usePlaybackClock({
2583
2665
  project,
2584
2666
  assets,
@@ -2714,12 +2796,18 @@ function PreviewPanel() {
2714
2796
  const handleCapture = (0, react.useCallback)(() => {
2715
2797
  const dataUrl = canvas.captureImage();
2716
2798
  if (dataUrl == null) {
2717
- sonner.toast.error("截图失败:画布尚未就绪");
2799
+ require_notify.notify(bridge, {
2800
+ level: "error",
2801
+ title: "截图失败:画布尚未就绪"
2802
+ });
2718
2803
  return;
2719
2804
  }
2720
2805
  downloadDataUrl(dataUrl, `frame-${Date.now()}.png`);
2721
- sonner.toast.success("已导出当前帧");
2722
- }, [canvas]);
2806
+ require_notify.notify(bridge, {
2807
+ level: "success",
2808
+ title: "已导出当前帧"
2809
+ });
2810
+ }, [bridge, canvas]);
2723
2811
  const handleFullscreen = () => {
2724
2812
  const el = canvas.containerRef.current?.parentElement;
2725
2813
  if (el == null) return;
@@ -2798,143 +2886,148 @@ var DEFAULT_BITRATE = 8e6;
2798
2886
  /** 时间轴顶部工具条:分割 / 复制 / 删除 / 吸附开关 / 缩放 / 加轨道 */
2799
2887
  function TimelineToolbar({ playheadUs, durationUs, pxPerSecond, selectionCount, magnetEnabled, onToggleMagnet, onSplit, onDuplicate, onRemove, onZoomIn, onZoomOut, onZoomToFit, onAddTrack, canZoomIn, canZoomOut }) {
2800
2888
  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, {
2889
+ 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",
2890
+ role: "toolbar",
2891
+ "aria-label": "时间轴工具",
2892
+ "data-timeline-toolbar": true,
2893
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2894
+ className: "flex shrink-0 items-center gap-1",
2895
+ children: [
2896
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2897
+ asChild: true,
2898
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2899
+ variant: "ghost",
2900
+ size: "icon-sm",
2901
+ onClick: onSplit,
2902
+ "aria-label": "分割",
2903
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Scissors, { className: "size-3.5" })
2904
+ })
2905
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "在播放头分割 (S)" })] }),
2906
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2907
+ asChild: true,
2908
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2909
+ variant: "ghost",
2910
+ size: "icon-sm",
2911
+ onClick: onDuplicate,
2912
+ disabled: selectionCount === 0,
2913
+ "aria-label": "复制片段",
2914
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Copy, { className: "size-3.5" })
2915
+ })
2916
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "复制片段 (Ctrl+D)" })] }),
2917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2918
+ asChild: true,
2919
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2920
+ variant: "ghost",
2921
+ size: "icon-sm",
2922
+ onClick: onRemove,
2923
+ disabled: selectionCount === 0,
2924
+ "aria-label": "删除片段",
2925
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Trash2, { className: "size-3.5" })
2926
+ })
2927
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "删除片段 (Delete)" })] }),
2928
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2929
+ orientation: "vertical",
2930
+ className: "mx-1 h-4"
2931
+ }),
2932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2933
+ asChild: true,
2934
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2935
+ variant: magnetEnabled ? "secondary" : "ghost",
2936
+ size: "icon-sm",
2937
+ onClick: onToggleMagnet,
2938
+ "aria-label": "吸附",
2939
+ "aria-pressed": magnetEnabled,
2940
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Magnet, { className: "size-3.5" })
2941
+ })
2942
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "吸附对齐(阈值 15px)" })] }),
2943
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2944
+ orientation: "vertical",
2945
+ className: "mx-1 h-4"
2946
+ }),
2947
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2948
+ className: "flex items-center gap-0.5",
2949
+ children: [
2950
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2951
+ asChild: true,
2952
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2953
+ variant: "ghost",
2954
+ size: "icon-sm",
2955
+ onClick: onZoomOut,
2956
+ disabled: !canZoomOut,
2957
+ "aria-label": "缩小",
2958
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomOut, { className: "size-3.5" })
2959
+ })
2960
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "缩小 (-)" })] }),
2961
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2962
+ className: "w-11 text-center font-mono text-[10px] text-muted-foreground tabular-nums",
2963
+ children: Math.round(pxPerSecond)
2964
+ }),
2965
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
2966
+ asChild: true,
2967
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2968
+ variant: "ghost",
2969
+ size: "icon-sm",
2970
+ onClick: onZoomIn,
2971
+ disabled: !canZoomIn,
2972
+ "aria-label": "放大",
2973
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, { className: "size-3.5" })
2974
+ })
2975
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "放大 (+)" })] }),
2976
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2875
2977
  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" })
2978
+ size: "xs",
2979
+ onClick: onZoomToFit,
2980
+ className: "text-[10px]",
2981
+ children: "适应"
2881
2982
  })
2882
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: "放大 (+)" })] }),
2883
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
2983
+ ]
2984
+ }),
2985
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Separator, {
2986
+ orientation: "vertical",
2987
+ className: "mx-1 h-4"
2988
+ }),
2989
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2990
+ className: "flex items-center gap-0.5",
2991
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
2884
2992
  variant: "ghost",
2885
2993
  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
- ]
2994
+ onClick: () => onAddTrack("visual"),
2995
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "视频轨"]
2996
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Button, {
2997
+ variant: "ghost",
2998
+ size: "xs",
2999
+ onClick: () => onAddTrack("audio"),
3000
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Plus, { className: "size-3" }), "音频轨"]
3001
+ })]
3002
+ })
3003
+ ]
3004
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3005
+ className: "ml-auto hidden shrink-0 items-center gap-2 pl-2 text-[11px] text-muted-foreground @[720px]:flex",
3006
+ children: [
3007
+ selectionCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3008
+ className: "rounded bg-primary/15 px-1.5 py-0.5 text-primary",
3009
+ children: ["已选 ", selectionCount]
3010
+ }),
3011
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Info, { className: "size-3" }),
3012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3013
+ className: "tabular-nums whitespace-nowrap",
3014
+ children: [
3015
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
3016
+ us: playheadUs,
3017
+ className: "text-foreground"
3018
+ }),
3019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3020
+ className: "mx-1",
3021
+ children: "/"
3022
+ }),
3023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Timecode, {
3024
+ us: durationUs,
3025
+ muted: true
3026
+ })
3027
+ ]
3028
+ })
3029
+ ]
3030
+ })]
2938
3031
  });
2939
3032
  }
2940
3033
  // src/features/timeline/components/Ruler.tsx
@@ -2951,14 +3044,14 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
2951
3044
  widthPx
2952
3045
  ]);
2953
3046
  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),
3047
+ className: require_notify.cn("relative shrink-0 cursor-ew-resize border-b bg-timeline-bg select-none", className),
2955
3048
  style: {
2956
3049
  height: 28,
2957
3050
  width: widthPx
2958
3051
  },
2959
3052
  children: [
2960
3053
  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"),
3054
+ className: require_notify.cn("absolute bottom-0 w-px", tick.major ? "h-2 bg-muted-foreground/50" : "h-1 bg-muted-foreground/25"),
2962
3055
  style: { left: tick.px }
2963
3056
  }, `${tick.us}-${tick.major ? "M" : "m"}`)),
2964
3057
  ticks.filter((t) => t.major).map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -2986,7 +3079,7 @@ function Ruler({ pxPerSecond, widthPx, playheadPx, scrollLeftPx, className }) {
2986
3079
  */
2987
3080
  var TrackHeader = (0, react.memo)(function TrackHeader({ track, onToggleMute, onToggleLock, onToggleHidden, onRemove }) {
2988
3081
  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"),
3082
+ className: require_notify.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
3083
  style: { height: 56 },
2991
3084
  "data-track-header": true,
2992
3085
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -3041,7 +3134,7 @@ function IconToggle({ active, onClick, label, icon: Icon }) {
3041
3134
  onClick,
3042
3135
  "aria-label": label,
3043
3136
  "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"),
3137
+ className: require_notify.cn("flex size-[18px] items-center justify-center rounded transition-colors", active ? "bg-primary/20 text-primary" : "text-muted-foreground hover:text-foreground"),
3045
3138
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "size-3" })
3046
3139
  })
3047
3140
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, { children: label })] });
@@ -3132,7 +3225,7 @@ var ClipBlock = (0, react.memo)(function ClipBlock({ clip, asset, pxPerSecond, s
3132
3225
  handlers.beginMove(event, clip);
3133
3226
  },
3134
3227
  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"),
3228
+ className: require_notify.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
3229
  style: {
3137
3230
  left: leftPx,
3138
3231
  width: widthPx,
@@ -3211,7 +3304,7 @@ function ContextMenuPortal({ ...props }) {
3211
3304
  function ContextMenuContent({ className, ...props }) {
3212
3305
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextMenuPortal, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Content, {
3213
3306
  "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),
3307
+ className: require_notify.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
3308
  ...props
3216
3309
  }) });
3217
3310
  }
@@ -3220,21 +3313,21 @@ function ContextMenuItem({ className, inset, variant = "default", ...props }) {
3220
3313
  "data-slot": "context-menu-item",
3221
3314
  "data-inset": inset,
3222
3315
  "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),
3316
+ className: require_notify.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
3317
  ...props
3225
3318
  });
3226
3319
  }
3227
3320
  function ContextMenuSeparator({ className, ...props }) {
3228
3321
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.ContextMenu.Separator, {
3229
3322
  "data-slot": "context-menu-separator",
3230
- className: cn("-mx-1 my-1 h-px bg-border", className),
3323
+ className: require_notify.cn("-mx-1 my-1 h-px bg-border", className),
3231
3324
  ...props
3232
3325
  });
3233
3326
  }
3234
3327
  function ContextMenuShortcut({ className, ...props }) {
3235
3328
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3236
3329
  "data-slot": "context-menu-shortcut",
3237
- className: cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
3330
+ className: require_notify.cn("ml-auto text-xs tracking-widest text-muted-foreground", className),
3238
3331
  ...props
3239
3332
  });
3240
3333
  }
@@ -3291,14 +3384,14 @@ function ClipContextMenu({ children, onSplit, onDuplicate, onRemove, onShowInfo,
3291
3384
  /** 播放头:一条竖线 + 顶部把手,跨过所有轨道 */
3292
3385
  function Playhead({ px, playing, className }) {
3293
3386
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3294
- className: cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
3387
+ className: require_notify.cn("pointer-events-none absolute top-0 bottom-0 z-20 w-px", className),
3295
3388
  style: { left: px },
3296
3389
  "aria-hidden": true,
3297
3390
  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)]"),
3391
+ className: require_notify.cn("absolute inset-y-0 -left-px w-0.5 bg-playhead", playing && "shadow-[0_0_6px_var(--cw-playhead)]"),
3299
3392
  style: { top: 24 }
3300
3393
  }), /* @__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"),
3394
+ className: require_notify.cn("absolute -translate-x-1/2 rounded-sm bg-playhead", playing ? "size-3 rotate-45" : "h-2.5 w-2.5 rotate-45"),
3302
3395
  style: { top: 14 }
3303
3396
  })]
3304
3397
  });
@@ -3307,7 +3400,7 @@ function Playhead({ px, playing, className }) {
3307
3400
  /** 框选矩形 */
3308
3401
  function Marquee({ x1, y1, x2, y2, className }) {
3309
3402
  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),
3403
+ className: require_notify.cn("pointer-events-none absolute z-40 rounded-sm border border-primary/70 bg-primary/15", className),
3311
3404
  style: {
3312
3405
  left: Math.min(x1, x2),
3313
3406
  top: Math.min(y1, y2),
@@ -3325,10 +3418,10 @@ function Marquee({ x1, y1, x2, y2, className }) {
3325
3418
  * 调整回去,视觉上内容不会乱跳。
3326
3419
  */
3327
3420
  function useTimelineZoom(containerRef) {
3328
- const uiApi = useUiStoreApi();
3329
- const pxPerSecond = useUi((s) => s.pxPerSecond);
3330
- const setPxPerSecond = useUi((s) => s.setPxPerSecond);
3331
- const setScrollLeftPx = useUi((s) => s.setScrollLeftPx);
3421
+ const uiApi = require_notify.useUiStoreApi();
3422
+ const pxPerSecond = require_notify.useUi((s) => s.pxPerSecond);
3423
+ const setPxPerSecond = require_notify.useUi((s) => s.setPxPerSecond);
3424
+ const setScrollLeftPx = require_notify.useUi((s) => s.setScrollLeftPx);
3332
3425
  const applyZoom = (0, react.useCallback)((nextPps, anchorClientX) => {
3333
3426
  const container = containerRef.current;
3334
3427
  const prevPps = uiApi.getState().pxPerSecond;
@@ -3653,7 +3746,7 @@ function useTimelineDrag({ pxPerSecond, contentLeftPx, resolveTrackAt, snapEnabl
3653
3746
  */
3654
3747
  function Timeline() {
3655
3748
  const editor = (0, _cut_workbench_react.useCutEditor)();
3656
- const uiApi = useUiStoreApi();
3749
+ const uiApi = require_notify.useUiStoreApi();
3657
3750
  const project = (0, _cut_workbench_react.useEditor)((s) => s.project);
3658
3751
  const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
3659
3752
  const selectedClipIds = (0, _cut_workbench_react.useEditor)((s) => s.selectedClipIds);
@@ -3898,7 +3991,7 @@ function Timeline() {
3898
3991
  splitClip
3899
3992
  ]);
3900
3993
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3901
- className: "flex min-h-0 flex-1 flex-col",
3994
+ className: "flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
3902
3995
  children: [
3903
3996
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimelineToolbar, {
3904
3997
  playheadUs,
@@ -3918,7 +4011,7 @@ function Timeline() {
3918
4011
  canZoomOut: zoom.canZoomOut
3919
4012
  }),
3920
4013
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3921
- className: "relative flex min-h-0 flex-1",
4014
+ className: "relative flex min-h-0 min-w-0 flex-1 overflow-hidden",
3922
4015
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3923
4016
  className: "thin-scrollbar flex shrink-0 flex-col overflow-y-auto border-r",
3924
4017
  style: { width: 150 },
@@ -3982,7 +4075,7 @@ function Timeline() {
3982
4075
  "data-timeline-tracks": true,
3983
4076
  children: [
3984
4077
  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"),
4078
+ className: require_notify.cn("relative border-b border-timeline-grid", track.kind === "audio" ? "bg-timeline-track/30" : "bg-timeline-track/50", track.hidden && "opacity-40"),
3986
4079
  style: { height: 56 },
3987
4080
  "data-track-row": track.id,
3988
4081
  children: [
@@ -4053,7 +4146,7 @@ function Tabs({ className, orientation = "horizontal", ...props }) {
4053
4146
  "data-slot": "tabs",
4054
4147
  "data-orientation": orientation,
4055
4148
  orientation,
4056
- className: cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
4149
+ className: require_notify.cn("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", className),
4057
4150
  ...props
4058
4151
  });
4059
4152
  }
@@ -4068,21 +4161,21 @@ function TabsList({ className, variant = "default", ...props }) {
4068
4161
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.List, {
4069
4162
  "data-slot": "tabs-list",
4070
4163
  "data-variant": variant,
4071
- className: cn(tabsListVariants({ variant }), className),
4164
+ className: require_notify.cn(tabsListVariants({ variant }), className),
4072
4165
  ...props
4073
4166
  });
4074
4167
  }
4075
4168
  function TabsTrigger({ className, ...props }) {
4076
4169
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Trigger, {
4077
4170
  "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),
4171
+ className: require_notify.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
4172
  ...props
4080
4173
  });
4081
4174
  }
4082
4175
  function TabsContent({ className, ...props }) {
4083
4176
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Tabs.Content, {
4084
4177
  "data-slot": "tabs-content",
4085
- className: cn("flex-1 outline-none", className),
4178
+ className: require_notify.cn("flex-1 outline-none", className),
4086
4179
  ...props
4087
4180
  });
4088
4181
  }
@@ -4091,7 +4184,7 @@ function Input({ className, type, ...props }) {
4091
4184
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4092
4185
  type,
4093
4186
  "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),
4187
+ className: require_notify.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
4188
  ...props
4096
4189
  });
4097
4190
  }
@@ -4112,7 +4205,7 @@ function SelectTrigger({ className, size = "default", children, ...props }) {
4112
4205
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Trigger, {
4113
4206
  "data-slot": "select-trigger",
4114
4207
  "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),
4208
+ className: require_notify.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
4209
  ...props,
4117
4210
  children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.Icon, {
4118
4211
  asChild: true,
@@ -4126,14 +4219,14 @@ function SelectContent({ className, children, position = "item-aligned", align =
4126
4219
  container: container ?? void 0,
4127
4220
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Content, {
4128
4221
  "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),
4222
+ className: require_notify.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
4223
  position,
4131
4224
  align,
4132
4225
  ...props,
4133
4226
  children: [
4134
4227
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollUpButton, {}),
4135
4228
  /* @__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"),
4229
+ className: require_notify.cn("p-1", position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"),
4137
4230
  children
4138
4231
  }),
4139
4232
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectScrollDownButton, {})
@@ -4144,7 +4237,7 @@ function SelectContent({ className, children, position = "item-aligned", align =
4144
4237
  function SelectItem({ className, children, ...props }) {
4145
4238
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(radix_ui.Select.Item, {
4146
4239
  "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),
4240
+ className: require_notify.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
4241
  ...props,
4149
4242
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4150
4243
  "data-slot": "select-item-indicator",
@@ -4156,7 +4249,7 @@ function SelectItem({ className, children, ...props }) {
4156
4249
  function SelectScrollUpButton({ className, ...props }) {
4157
4250
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollUpButton, {
4158
4251
  "data-slot": "select-scroll-up-button",
4159
- className: cn("flex cursor-default items-center justify-center py-1", className),
4252
+ className: require_notify.cn("flex cursor-default items-center justify-center py-1", className),
4160
4253
  ...props,
4161
4254
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronUpIcon, { className: "size-4" })
4162
4255
  });
@@ -4164,7 +4257,7 @@ function SelectScrollUpButton({ className, ...props }) {
4164
4257
  function SelectScrollDownButton({ className, ...props }) {
4165
4258
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Select.ScrollDownButton, {
4166
4259
  "data-slot": "select-scroll-down-button",
4167
- className: cn("flex cursor-default items-center justify-center py-1", className),
4260
+ className: require_notify.cn("flex cursor-default items-center justify-center py-1", className),
4168
4261
  ...props,
4169
4262
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronDownIcon, { className: "size-4" })
4170
4263
  });
@@ -4173,7 +4266,7 @@ function SelectScrollDownButton({ className, ...props }) {
4173
4266
  /** 属性面板的一行:标签 + 控件(标签在左,控件占满剩余宽度) */
4174
4267
  function Field({ label, hint, children, className, htmlFor }) {
4175
4268
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4176
- className: cn("flex items-center gap-2", className),
4269
+ className: require_notify.cn("flex items-center gap-2", className),
4177
4270
  children: [
4178
4271
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Label, {
4179
4272
  htmlFor,
@@ -4194,7 +4287,7 @@ function Field({ label, hint, children, className, htmlFor }) {
4194
4287
  /** 属性分组 */
4195
4288
  function Section({ title, children, action, className }) {
4196
4289
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4197
- className: cn("space-y-2", className),
4290
+ className: require_notify.cn("space-y-2", className),
4198
4291
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4199
4292
  className: "flex items-center justify-between",
4200
4293
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
@@ -4389,7 +4482,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
4389
4482
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
4390
4483
  type: "number",
4391
4484
  className: "h-7 text-xs tabular-nums",
4392
- value: formatSeconds(clip.startUs),
4485
+ value: require_notify.formatSeconds(clip.startUs),
4393
4486
  step: .1,
4394
4487
  min: 0,
4395
4488
  onChange: (e) => {
@@ -4403,7 +4496,7 @@ function VideoProps({ clip, onPatch, onPatchTransform, onPatchSize }) {
4403
4496
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
4404
4497
  type: "number",
4405
4498
  className: "h-7 text-xs tabular-nums",
4406
- value: formatSeconds(clip.durationUs),
4499
+ value: require_notify.formatSeconds(clip.durationUs),
4407
4500
  step: .1,
4408
4501
  min: .1,
4409
4502
  onChange: (e) => {
@@ -4510,7 +4603,7 @@ function AudioProps({ clip, onPatch }) {
4510
4603
  className: "text-[10px] text-muted-foreground",
4511
4604
  children: [
4512
4605
  "片段时长 ",
4513
- formatSeconds(clip.durationUs),
4606
+ require_notify.formatSeconds(clip.durationUs),
4514
4607
  "s,淡入使用 setAnimation 线性插值。"
4515
4608
  ]
4516
4609
  })
@@ -4765,7 +4858,7 @@ function SubtitleProps({ clip, onPatch, onRebuild }) {
4765
4858
  className: "text-[10px] text-muted-foreground",
4766
4859
  children: [
4767
4860
  "片段时长 ",
4768
- formatSeconds(clip.durationUs),
4861
+ require_notify.formatSeconds(clip.durationUs),
4769
4862
  "s",
4770
4863
  cues.length > 0 && ` · 最后一条结束于 ${(cues[cues.length - 1].endMs / 1e3).toFixed(2)}s`
4771
4864
  ]
@@ -5037,7 +5130,7 @@ function InspectorPanel() {
5037
5130
  title: asset.name,
5038
5131
  children: asset.name
5039
5132
  }),
5040
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Badge, {
5133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.Badge, {
5041
5134
  variant: "outline",
5042
5135
  className: "text-[10px]",
5043
5136
  children: KIND_LABEL[asset.kind]
@@ -5060,7 +5153,7 @@ function InspectorPanel() {
5060
5153
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
5061
5154
  (0, _cut_workbench_core.formatDuration)(clip.durationUs),
5062
5155
  " · ",
5063
- formatResolution(asset.width, asset.height)
5156
+ require_notify.formatResolution(asset.width, asset.height)
5064
5157
  ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
5065
5158
  "入点 ",
5066
5159
  (clip.inPointUs / 1e6).toFixed(2),
@@ -5165,11 +5258,11 @@ function Switch({ className, size = "default", ...props }) {
5165
5258
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Root, {
5166
5259
  "data-slot": "switch",
5167
5260
  "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),
5261
+ className: require_notify.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
5262
  ...props,
5170
5263
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Switch.Thumb, {
5171
5264
  "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")
5265
+ className: require_notify.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
5266
  })
5174
5267
  });
5175
5268
  }
@@ -5177,7 +5270,7 @@ function Switch({ className, size = "default", ...props }) {
5177
5270
  function Progress({ className, value, ...props }) {
5178
5271
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Root, {
5179
5272
  "data-slot": "progress",
5180
- className: cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
5273
+ className: require_notify.cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className),
5181
5274
  ...props,
5182
5275
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(radix_ui.Progress.Indicator, {
5183
5276
  "data-slot": "progress-indicator",
@@ -5708,8 +5801,8 @@ var FPS_OPTIONS = [
5708
5801
  * 所有重活都在 Worker 里;这里只负责 UI 与状态机。
5709
5802
  */
5710
5803
  function ExportDialog() {
5711
- const open = useUi((s) => s.dialog === "export");
5712
- const closeDialog = useUi((s) => s.closeDialog);
5804
+ const open = require_notify.useUi((s) => s.dialog === "export");
5805
+ const closeDialog = require_notify.useUi((s) => s.closeDialog);
5713
5806
  const project = (0, _cut_workbench_react.useEditor)((s) => s.project);
5714
5807
  const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
5715
5808
  const [preset, setPreset] = (0, react.useState)("1080p");
@@ -5855,7 +5948,7 @@ function ExportDialog() {
5855
5948
  className: "flex justify-between",
5856
5949
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "预估大小" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5857
5950
  className: "font-mono tabular-nums",
5858
- children: formatBytes(estimatedBytes)
5951
+ children: require_notify.formatBytes(estimatedBytes)
5859
5952
  })]
5860
5953
  }),
5861
5954
  hasTextClips && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -5892,10 +5985,10 @@ function ExportDialog() {
5892
5985
  children: [
5893
5986
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.CheckCircle2, { className: "size-3.5" }),
5894
5987
  "导出完成 · 耗时 ",
5895
- formatElapsed(state.result.elapsedMs / 1e3),
5988
+ require_notify.formatElapsed(state.result.elapsedMs / 1e3),
5896
5989
  " ·",
5897
5990
  " ",
5898
- formatBytes(state.result.blob.size),
5991
+ require_notify.formatBytes(state.result.blob.size),
5899
5992
  state.result.mode === "main" && "(主线程)"
5900
5993
  ]
5901
5994
  }),
@@ -5958,7 +6051,7 @@ function ExportProgress({ progress, label = "正在合成…", className }) {
5958
6051
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Loader2, { className: "size-3 animate-spin" }), label]
5959
6052
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5960
6053
  className: "font-mono tabular-nums",
5961
- children: formatPercent(progress)
6054
+ children: require_notify.formatPercent(progress)
5962
6055
  })]
5963
6056
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Progress, { value: progress * 100 })]
5964
6057
  });
@@ -6034,7 +6127,7 @@ function CanvasSettingsBody({ onDone }) {
6034
6127
  setDraftW(String(preset.width));
6035
6128
  setDraftH(String(preset.height));
6036
6129
  },
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"),
6130
+ className: require_notify.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
6131
  title: preset.hint,
6039
6132
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6040
6133
  className: "block rounded-[2px] border border-current opacity-70",
@@ -6095,7 +6188,7 @@ function CanvasSettingsBody({ onDone }) {
6095
6188
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6096
6189
  type: "button",
6097
6190
  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"),
6191
+ className: require_notify.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
6192
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6100
6193
  className: "block font-medium text-foreground",
6101
6194
  children: "随画布缩放"
@@ -6106,7 +6199,7 @@ function CanvasSettingsBody({ onDone }) {
6106
6199
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6107
6200
  type: "button",
6108
6201
  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"),
6202
+ className: require_notify.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
6203
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6111
6204
  className: "block font-medium text-foreground",
6112
6205
  children: "保持原位置"
@@ -6168,16 +6261,23 @@ function useAddToTimeline() {
6168
6261
  const assets = (0, _cut_workbench_react.useEditor)((s) => s.assets);
6169
6262
  const playheadUs = (0, _cut_workbench_react.useEditor)((s) => s.playheadUs);
6170
6263
  const addClip = (0, _cut_workbench_react.useEditor)((s) => s.addClip);
6264
+ const { bridge } = require_notify.useWorkbench();
6171
6265
  return (0, react.useCallback)((assetId) => {
6172
6266
  const asset = assets.find((a) => a.id === assetId);
6173
6267
  if (!asset) {
6174
- sonner.toast.error("素材不存在,可能已被删除");
6268
+ require_notify.notify(bridge, {
6269
+ level: "error",
6270
+ title: "素材不存在,可能已被删除"
6271
+ });
6175
6272
  return;
6176
6273
  }
6177
6274
  const kindIsAudio = asset.kind === "audio";
6178
6275
  const track = project.tracks.find((t) => t.kind === (kindIsAudio ? "audio" : "visual")) ?? project.tracks[0];
6179
6276
  if (!track) {
6180
- sonner.toast.error("没有可用轨道");
6277
+ require_notify.notify(bridge, {
6278
+ level: "error",
6279
+ title: "没有可用轨道"
6280
+ });
6181
6281
  return;
6182
6282
  }
6183
6283
  const trackEnd = (0, _cut_workbench_core.getTrackEnd)(project, track.id);
@@ -6210,6 +6310,7 @@ function useAddToTimeline() {
6210
6310
  }, [
6211
6311
  addClip,
6212
6312
  assets,
6313
+ bridge,
6213
6314
  playheadUs,
6214
6315
  project
6215
6316
  ]);
@@ -6246,1340 +6347,173 @@ var FRAME_US = 1e6 / 30;
6246
6347
  * 全局快捷键。
6247
6348
  *
6248
6349
  * | 键 | 行为 |
6249
- * |---|---|
6250
- * | Space | 播放 / 暂停 |
6251
- * | S | 在播放头分割选中片段(未选中则分割命中的全部片段) |
6252
- * | Delete / Backspace | 删除选中片段 |
6253
- * | Ctrl+Z / Ctrl+Shift+Z | 撤销 / 重做 |
6254
- * | Ctrl+D | 复制选中片段 |
6255
- * | Ctrl+A | 全选 |
6256
- * | ← / → | 逐帧(1/30 s) |
6257
- * | Home / End | 跳首 / 跳尾 |
6258
- * | + / - | 时间轴缩放 |
6259
- * | Esc | 清空选择 |
6260
- *
6261
- * 设计:编辑类操作直接调 store;播放类操作通过 `cut-test:*` 自定义事件通知预览面板,
6262
- * 避免 store → features 的反向依赖。
6263
- */
6264
- function useKeyboardShortcuts() {
6265
- const editor = (0, _cut_workbench_react.useCutEditor)();
6266
- const uiApi = useUiStoreApi();
6267
- const handler = (0, react.useCallback)((event) => {
6268
- const target = event.target;
6269
- const isEditable = target != null && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || target.getAttribute("role") === "textbox" || target.closest("[role=menu]") != null);
6270
- const mod = event.ctrlKey || event.metaKey;
6271
- if (mod && event.key.toLowerCase() === "z") {
6272
- event.preventDefault();
6273
- if (event.shiftKey) editor.store.getState().redo();
6274
- else editor.store.getState().undo();
6275
- return;
6276
- }
6277
- if (mod && event.key.toLowerCase() === "d") {
6278
- event.preventDefault();
6279
- editor.store.getState().duplicateSelection(5e5);
6280
- return;
6281
- }
6282
- if (mod && event.key.toLowerCase() === "a" && !isEditable) {
6283
- event.preventDefault();
6284
- const state = editor.store.getState();
6285
- state.selectClips(state.project.clips.map((c) => c.id));
6286
- return;
6287
- }
6288
- if (mod && (event.key === "+" || event.key === "=" || event.key === "-" || event.key === "_")) {
6289
- event.preventDefault();
6290
- const ui = uiApi.getState();
6291
- const factor = event.key === "-" || event.key === "_" ? 1 / 1.25 : 1.25;
6292
- ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond * factor, 10, 500));
6293
- return;
6294
- }
6295
- if (isEditable || mod) return;
6296
- switch (event.key) {
6297
- case " ":
6298
- event.preventDefault();
6299
- window.dispatchEvent(new CustomEvent("cut-test:toggle-play"));
6300
- break;
6301
- case "s":
6302
- case "S": {
6303
- event.preventDefault();
6304
- const state = editor.store.getState();
6305
- state.splitClipsAt(state.playheadUs, state.selectedClipIds.length > 0 ? state.selectedClipIds : void 0);
6306
- break;
6307
- }
6308
- case "Delete":
6309
- case "Backspace": {
6310
- const state = editor.store.getState();
6311
- if (state.selectedClipIds.length === 0) return;
6312
- event.preventDefault();
6313
- state.removeClips(state.selectedClipIds);
6314
- break;
6315
- }
6316
- case "ArrowLeft":
6317
- case "ArrowRight": {
6318
- event.preventDefault();
6319
- const direction = event.key === "ArrowLeft" ? -1 : 1;
6320
- const state = editor.store.getState();
6321
- const total = state.project.clips.reduce((max, c) => Math.max(max, c.startUs + c.durationUs), 0);
6322
- const next = Math.max(0, Math.min(total, state.playheadUs + direction * FRAME_US));
6323
- state.setPlayhead(next);
6324
- window.dispatchEvent(new CustomEvent("cut-test:step-frame", { detail: { direction } }));
6325
- break;
6326
- }
6327
- case "Home":
6328
- event.preventDefault();
6329
- editor.store.getState().setPlayhead(0);
6330
- window.dispatchEvent(new CustomEvent("cut-test:seek", { detail: { us: 0 } }));
6331
- break;
6332
- case "End": {
6333
- event.preventDefault();
6334
- const state = editor.store.getState();
6335
- const total = state.project.clips.reduce((max, c) => Math.max(max, c.startUs + c.durationUs), 0);
6336
- state.setPlayhead(total);
6337
- window.dispatchEvent(new CustomEvent("cut-test:seek", { detail: { us: total } }));
6338
- break;
6339
- }
6340
- case "+":
6341
- case "=": {
6342
- event.preventDefault();
6343
- const ui = uiApi.getState();
6344
- ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond * 1.25, 10, 500));
6345
- break;
6346
- }
6347
- case "-":
6348
- case "_": {
6349
- event.preventDefault();
6350
- const ui = uiApi.getState();
6351
- ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond / 1.25, 10, 500));
6352
- break;
6353
- }
6354
- case "Escape": editor.store.getState().selectClips([]);
6355
- }
6356
- }, []);
6357
- (0, react.useEffect)(() => {
6358
- window.addEventListener("keydown", handler);
6359
- return () => window.removeEventListener("keydown", handler);
6360
- }, [handler]);
6361
- }
6362
- // src/hooks/use-capability.ts
6363
- var cached = null;
6364
- /**
6365
- * 启动时探测一次浏览器能力(结果在会话内缓存)。
6366
- *
6367
- * 必须在渲染任何 WebAV 组件前完成,否则 opfs-tools 的模块副作用可能直接抛错。
6368
- */
6369
- function useCapability() {
6370
- const [state, setState] = (0, react.useState)({
6371
- report: cached,
6372
- loading: cached == null,
6373
- error: null
6374
- });
6375
- (0, react.useEffect)(() => {
6376
- if (cached != null) return;
6377
- let cancelled = false;
6378
- (0, _cut_workbench_core_engine.detectEnvironment)().then((report) => {
6379
- cached = report;
6380
- if (!cancelled) setState({
6381
- report,
6382
- loading: false,
6383
- error: null
6384
- });
6385
- }).catch((err) => {
6386
- if (!cancelled) setState({
6387
- report: null,
6388
- loading: false,
6389
- error: err instanceof Error ? err.message : String(err)
6390
- });
6391
- });
6392
- return () => {
6393
- cancelled = true;
6394
- };
6395
- }, []);
6396
- return state;
6397
- }
6398
- // src/components/layout/use-capability-info.ts
6399
- function deriveStatus(report, loading) {
6400
- if (loading) return "checking";
6401
- if (report == null) return "unsupported";
6402
- if (report.opfs && report.composerSupported && report.issues.length === 0) return "ready";
6403
- if (report.opfs && report.composerSupported) return "degraded";
6404
- return "unsupported";
6405
- }
6406
- /** 能力探测的 UI 适配层:把 core 的报告转成状态与文案 */
6407
- function useCapabilityInfo() {
6408
- const { report, loading, error } = useCapability();
6409
- return {
6410
- report,
6411
- loading,
6412
- error,
6413
- status: deriveStatus(report, loading),
6414
- summary: (0, _cut_workbench_core_engine.summarizeCapabilities)(report)
6415
- };
6416
- }
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:屏幕 / 画布录制。
6350
+ * |---|---|
6351
+ * | Space | 播放 / 暂停 |
6352
+ * | S | 在播放头分割选中片段(未选中则分割命中的全部片段) |
6353
+ * | Delete / Backspace | 删除选中片段 |
6354
+ * | Ctrl+Z / Ctrl+Shift+Z | 撤销 / 重做 |
6355
+ * | Ctrl+D | 复制选中片段 |
6356
+ * | Ctrl+A | 全选 |
6357
+ * | ← / → | 逐帧(1/30 s) |
6358
+ * | Home / End | 跳首 / 跳尾 |
6359
+ * | + / - | 时间轴缩放 |
6360
+ * | Esc | 清空选择 |
7200
6361
  *
7201
- * `AVRecorder` 接收任意 MediaStream 并输出 MP4(H.264 + AAC)。
7202
- * 这里提供两种源:
7203
- * - 程序化画布(Canvas.captureStream,无需授权);
7204
- * - 摄像头(getUserMedia,需要用户授权)。
6362
+ * 设计:编辑类操作直接调 store;播放类操作通过 `cut-test:*` 自定义事件通知预览面板,
6363
+ * 避免 store → features 的反向依赖。
7205
6364
  */
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;
6365
+ function useKeyboardShortcuts() {
6366
+ const editor = (0, _cut_workbench_react.useCutEditor)();
6367
+ const uiApi = require_notify.useUiStoreApi();
6368
+ const handler = (0, react.useCallback)((event) => {
6369
+ const target = event.target;
6370
+ const isEditable = target != null && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || target.getAttribute("role") === "textbox" || target.closest("[role=menu]") != null);
6371
+ const mod = event.ctrlKey || event.metaKey;
6372
+ if (mod && event.key.toLowerCase() === "z") {
6373
+ event.preventDefault();
6374
+ if (event.shiftKey) editor.store.getState().redo();
6375
+ else editor.store.getState().undo();
6376
+ return;
7245
6377
  }
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;
6378
+ if (mod && event.key.toLowerCase() === "d") {
6379
+ event.preventDefault();
6380
+ editor.store.getState().duplicateSelection(5e5);
6381
+ return;
7258
6382
  }
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);
6383
+ if (mod && event.key.toLowerCase() === "a" && !isEditable) {
6384
+ event.preventDefault();
6385
+ const state = editor.store.getState();
6386
+ state.selectClips(state.project.clips.map((c) => c.id));
6387
+ return;
7281
6388
  }
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`);
6389
+ if (mod && (event.key === "+" || event.key === "=" || event.key === "-" || event.key === "_")) {
6390
+ event.preventDefault();
6391
+ const ui = uiApi.getState();
6392
+ const factor = event.key === "-" || event.key === "_" ? 1 / 1.25 : 1.25;
6393
+ ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond * factor, 10, 500));
6394
+ return;
7298
6395
  }
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());
6396
+ if (isEditable || mod) return;
6397
+ switch (event.key) {
6398
+ case " ":
6399
+ event.preventDefault();
6400
+ window.dispatchEvent(new CustomEvent("cut-test:toggle-play"));
6401
+ break;
6402
+ case "s":
6403
+ case "S": {
6404
+ event.preventDefault();
6405
+ const state = editor.store.getState();
6406
+ state.splitClipsAt(state.playheadUs, state.selectedClipIds.length > 0 ? state.selectedClipIds : void 0);
6407
+ break;
6408
+ }
6409
+ case "Delete":
6410
+ case "Backspace": {
6411
+ const state = editor.store.getState();
6412
+ if (state.selectedClipIds.length === 0) return;
6413
+ event.preventDefault();
6414
+ state.removeClips(state.selectedClipIds);
6415
+ break;
6416
+ }
6417
+ case "ArrowLeft":
6418
+ case "ArrowRight": {
6419
+ event.preventDefault();
6420
+ const direction = event.key === "ArrowLeft" ? -1 : 1;
6421
+ const state = editor.store.getState();
6422
+ const total = state.project.clips.reduce((max, c) => Math.max(max, c.startUs + c.durationUs), 0);
6423
+ const next = Math.max(0, Math.min(total, state.playheadUs + direction * FRAME_US));
6424
+ state.setPlayhead(next);
6425
+ window.dispatchEvent(new CustomEvent("cut-test:step-frame", { detail: { direction } }));
6426
+ break;
6427
+ }
6428
+ case "Home":
6429
+ event.preventDefault();
6430
+ editor.store.getState().setPlayhead(0);
6431
+ window.dispatchEvent(new CustomEvent("cut-test:seek", { detail: { us: 0 } }));
6432
+ break;
6433
+ case "End": {
6434
+ event.preventDefault();
6435
+ const state = editor.store.getState();
6436
+ const total = state.project.clips.reduce((max, c) => Math.max(max, c.startUs + c.durationUs), 0);
6437
+ state.setPlayhead(total);
6438
+ window.dispatchEvent(new CustomEvent("cut-test:seek", { detail: { us: total } }));
6439
+ break;
6440
+ }
6441
+ case "+":
6442
+ case "=": {
6443
+ event.preventDefault();
6444
+ const ui = uiApi.getState();
6445
+ ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond * 1.25, 10, 500));
6446
+ break;
6447
+ }
6448
+ case "-":
6449
+ case "_": {
6450
+ event.preventDefault();
6451
+ const ui = uiApi.getState();
6452
+ ui.setPxPerSecond((0, _cut_workbench_core.clamp)(ui.pxPerSecond / 1.25, 10, 500));
6453
+ break;
6454
+ }
6455
+ case "Escape": editor.store.getState().selectClips([]);
7390
6456
  }
7391
- };
6457
+ }, []);
6458
+ (0, react.useEffect)(() => {
6459
+ window.addEventListener("keydown", handler);
6460
+ return () => window.removeEventListener("keydown", handler);
6461
+ }, [handler]);
7392
6462
  }
7393
- // src/features/lab/components/CustomClipLab.tsx
6463
+ // src/hooks/use-capability.ts
6464
+ var cached = null;
7394
6465
  /**
7395
- * Lab 8:自定义 IClip。
7396
- *
7397
- * `FrameCounterClip` 是本项目实现的 IClip:不依赖任何媒体文件,
7398
- * 逐帧用 OffscreenCanvas 画出帧号 / 进度条 / 色相变化。
6466
+ * 启动时探测一次浏览器能力(结果在会话内缓存)。
7399
6467
  *
7400
- * 它证明 IClip 接口(tick / ready / meta / clone / split / destroy)可以被任意扩展,
7401
- * 且能直接被 Combinator 编码成 MP4。
6468
+ * 必须在渲染任何 WebAV 组件前完成,否则 opfs-tools 的模块副作用可能直接抛错。
7402
6469
  */
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]);
6470
+ function useCapability() {
6471
+ const [state, setState] = (0, react.useState)({
6472
+ report: cached,
6473
+ loading: cached == null,
6474
+ error: null
6475
+ });
7435
6476
  (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);
6477
+ if (cached != null) return;
6478
+ let cancelled = false;
6479
+ (0, _cut_workbench_core_engine.detectEnvironment)().then((report) => {
6480
+ cached = report;
6481
+ if (!cancelled) setState({
6482
+ report,
6483
+ loading: false,
6484
+ error: null
6485
+ });
6486
+ }).catch((err) => {
6487
+ if (!cancelled) setState({
6488
+ report: null,
6489
+ loading: false,
6490
+ error: err instanceof Error ? err.message : String(err)
6491
+ });
6492
+ });
7452
6493
  return () => {
7453
- if (rafRef.current != null) cancelAnimationFrame(rafRef.current);
6494
+ cancelled = true;
7454
6495
  };
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
- });
6496
+ }, []);
6497
+ return state;
7540
6498
  }
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
- });
6499
+ // src/components/layout/use-capability-info.ts
6500
+ function deriveStatus(report, loading) {
6501
+ if (loading) return "checking";
6502
+ if (report == null) return "unsupported";
6503
+ if (report.opfs && report.composerSupported && report.issues.length === 0) return "ready";
6504
+ if (report.opfs && report.composerSupported) return "degraded";
6505
+ return "unsupported";
6506
+ }
6507
+ /** 能力探测的 UI 适配层:把 core 的报告转成状态与文案 */
6508
+ function useCapabilityInfo() {
6509
+ const { report, loading, error } = useCapability();
6510
+ return {
6511
+ report,
6512
+ loading,
6513
+ error,
6514
+ status: deriveStatus(report, loading),
6515
+ summary: (0, _cut_workbench_core_engine.summarizeCapabilities)(report)
6516
+ };
7583
6517
  }
7584
6518
  // src/CutWorkbench.tsx
7585
6519
  /**
@@ -7592,24 +6526,35 @@ function LabPage() {
7592
6526
  * - 不强制能力阻断页(宿主可自行决定)。
7593
6527
  */
7594
6528
  /**
6529
+ * Lab 的懒加载器。
6530
+ *
6531
+ * ⚠️ 用**字面量**模块说明符做动态 import,而不是先从 `./features/lab` 拿组件:
6532
+ * 前者会被打包成 external(`import('@cut-workbench/workbench/lab')`),由宿主
6533
+ * 打包器解析;后者会把 lab 整棵子树(含 `@webav/av-recorder`)拉回本包的模块图。
6534
+ *
6535
+ * 解析到 lab 还是 stub,由宿主的 `resolve.alias` 决定 —— 这是移除 lab 的
6536
+ * 唯一可靠方式(运行时开关删不掉已产出的 chunk,见 src/lab.ts 顶部说明)。
6537
+ */
6538
+ var LabPage = (0, react.lazy)(() => import("@cut-workbench/workbench/lab").then((m) => ({ default: m.LabPage })));
6539
+ /**
7595
6540
  * 工作台主体。
7596
6541
  *
7597
6542
  * 必须在 `<WorkbenchProvider>` + `<CutEditorProvider>` 之内使用。
7598
6543
  */
7599
- function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceCapability = true }) {
6544
+ function CutWorkbench({ slots, toolbar: toolbarOptions, className, onSaveProject, onOpenProject, enforceCapability = true }) {
7600
6545
  const editor = (0, _cut_workbench_react.useCutEditor)();
7601
6546
  const capability = useCapabilityInfo();
7602
- const leftWidth = useUi((s) => s.leftWidth);
7603
- const rightWidth = useUi((s) => s.rightWidth);
7604
- const timelineHeight = useUi((s) => s.timelineHeight);
7605
- const setLeftWidth = useUi((s) => s.setLeftWidth);
7606
- const setRightWidth = useUi((s) => s.setRightWidth);
7607
- const setTimelineHeight = useUi((s) => s.setTimelineHeight);
7608
- const openDialog = useUi((s) => s.openDialog);
7609
- const closeDialog = useUi((s) => s.closeDialog);
7610
- const dialog = useUi((s) => s.dialog);
7611
- const { config, theme } = useWorkbench();
7612
- const route = useUi((s) => s.route);
6547
+ const leftWidth = require_notify.useUi((s) => s.leftWidth);
6548
+ const rightWidth = require_notify.useUi((s) => s.rightWidth);
6549
+ const timelineHeight = require_notify.useUi((s) => s.timelineHeight);
6550
+ const setLeftWidth = require_notify.useUi((s) => s.setLeftWidth);
6551
+ const setRightWidth = require_notify.useUi((s) => s.setRightWidth);
6552
+ const setTimelineHeight = require_notify.useUi((s) => s.setTimelineHeight);
6553
+ const openDialog = require_notify.useUi((s) => s.openDialog);
6554
+ const closeDialog = require_notify.useUi((s) => s.closeDialog);
6555
+ const dialog = require_notify.useUi((s) => s.dialog);
6556
+ const { config, theme } = require_notify.useWorkbench();
6557
+ const route = require_notify.useUi((s) => s.route);
7613
6558
  const addToTimeline = useAddToTimeline();
7614
6559
  const removeAsset = (0, _cut_workbench_react.useEditor)((s) => s.removeAsset);
7615
6560
  const [busy, setBusy] = (0, react.useState)(null);
@@ -7660,10 +6605,18 @@ function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceC
7660
6605
  onExport: handleExport,
7661
6606
  onSaveProject: onSaveProject != null ? handleSave : void 0,
7662
6607
  onOpenProject: onOpenProject != null ? handleOpen : void 0,
6608
+ brand: toolbarOptions?.brand,
6609
+ title: toolbarOptions?.title,
6610
+ status: toolbarOptions?.status,
6611
+ actions: toolbarOptions?.actions,
6612
+ showCapabilityBadge: toolbarOptions?.showCapabilityBadge,
6613
+ editableTitle: toolbarOptions?.editableTitle,
6614
+ labels: toolbarOptions?.labels,
7663
6615
  showLab: config.features.lab
7664
6616
  });
7665
6617
  }, [
7666
6618
  slots?.topBar,
6619
+ toolbarOptions,
7667
6620
  capability,
7668
6621
  handleExport,
7669
6622
  handleSave,
@@ -7690,7 +6643,17 @@ function CutWorkbench({ slots, className, onSaveProject, onOpenProject, enforceC
7690
6643
  toolbar,
7691
6644
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7692
6645
  className: "min-h-0 flex-1 overflow-auto",
7693
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabPage, {})
6646
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Suspense, {
6647
+ fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6648
+ className: "flex h-full items-center justify-center",
6649
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoadingOverlay, {
6650
+ visible: true,
6651
+ title: "正在加载能力实验室…",
6652
+ fullscreen: false
6653
+ })
6654
+ }),
6655
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LabPage, {})
6656
+ })
7694
6657
  }),
7695
6658
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ExportDialog, {}),
7696
6659
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CanvasSettingsDialog, {
@@ -8638,11 +7601,15 @@ function CutWorkbenchProvider({ children, bridge, theme = "dark", tokens, config
8638
7601
  (0, _cut_workbench_react.useCutEditor)();
8639
7602
  const [ui] = (0, react.useState)(() => createUiStore());
8640
7603
  const resolvedBridge = (0, react.useMemo)(() => bridge ?? createDefaultBridge(), [bridge]);
7604
+ (0, react.useEffect)(() => {
7605
+ require_notify.setDefaultBridge(resolvedBridge);
7606
+ return () => require_notify.setDefaultBridge(null);
7607
+ }, [resolvedBridge]);
8641
7608
  const mergedConfig = (0, react.useMemo)(() => ({
8642
- ...DEFAULT_CONFIG,
7609
+ ...require_notify.DEFAULT_CONFIG,
8643
7610
  ...config,
8644
7611
  features: {
8645
- ...DEFAULT_CONFIG.features,
7612
+ ...require_notify.DEFAULT_CONFIG.features,
8646
7613
  ...config?.features ?? {}
8647
7614
  }
8648
7615
  }), [config]);
@@ -8671,7 +7638,7 @@ function CutWorkbenchProvider({ children, bridge, theme = "dark", tokens, config
8671
7638
  resolvedTheme,
8672
7639
  portalContainer
8673
7640
  ]);
8674
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkbenchContext.Provider, {
7641
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_notify.WorkbenchContext.Provider, {
8675
7642
  value,
8676
7643
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PortalContainerProvider, {
8677
7644
  container: portalContainer,
@@ -8768,6 +7735,7 @@ function initCutWorkbench(container, options = {}) {
8768
7735
  }, (0, react.createElement)(CutWorkbench, {
8769
7736
  className: options.className ?? "h-full w-full",
8770
7737
  enforceCapability: options.enforceCapability ?? true,
7738
+ toolbar: options.toolbar,
8771
7739
  onSaveProject: () => void instance.saveProject(),
8772
7740
  onOpenProject: async () => {
8773
7741
  const saved = await storage.load(namespace);
@@ -8923,8 +7891,8 @@ exports.ClipBlock = ClipBlock;
8923
7891
  exports.ClipContextMenu = ClipContextMenu;
8924
7892
  exports.CutWorkbench = CutWorkbench;
8925
7893
  exports.CutWorkbenchProvider = CutWorkbenchProvider;
8926
- exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
8927
- exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
7894
+ exports.DEFAULT_CONFIG = require_notify.DEFAULT_CONFIG;
7895
+ exports.DEFAULT_FEATURES = require_notify.DEFAULT_FEATURES;
8928
7896
  exports.DropZone = DropZone;
8929
7897
  exports.EmptyState = EmptyState;
8930
7898
  exports.ExportDialog = ExportDialog;
@@ -8972,8 +7940,8 @@ exports.useExport = useExport;
8972
7940
  exports.useHostDarkClass = useHostDarkClass;
8973
7941
  exports.useMediaImport = useMediaImport;
8974
7942
  exports.usePortalContainer = usePortalContainer;
8975
- exports.useUi = useUi;
8976
- exports.useUiStoreApi = useUiStoreApi;
8977
- exports.useWorkbench = useWorkbench;
7943
+ exports.useUi = require_notify.useUi;
7944
+ exports.useUiStoreApi = require_notify.useUiStoreApi;
7945
+ exports.useWorkbench = require_notify.useWorkbench;
8978
7946
 
8979
7947
  //# sourceMappingURL=index.cjs.map