@lokvis/ui-react 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/components/AssetPanel.d.ts +2 -2
- package/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +80 -7
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts +9 -1
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +41 -3
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CommandPalette.d.ts +48 -0
- package/dist/components/CommandPalette.d.ts.map +1 -0
- package/dist/components/CommandPalette.js +154 -0
- package/dist/components/CommandPalette.js.map +1 -0
- package/dist/components/CompareSlider.d.ts +25 -0
- package/dist/components/CompareSlider.d.ts.map +1 -0
- package/dist/components/CompareSlider.js +106 -0
- package/dist/components/CompareSlider.js.map +1 -0
- package/dist/components/DownloadPanel.d.ts +20 -0
- package/dist/components/DownloadPanel.d.ts.map +1 -0
- package/dist/components/DownloadPanel.js +169 -0
- package/dist/components/DownloadPanel.js.map +1 -0
- package/dist/components/ErrorBanner.d.ts +22 -0
- package/dist/components/ErrorBanner.d.ts.map +1 -0
- package/dist/components/ErrorBanner.js +47 -0
- package/dist/components/ErrorBanner.js.map +1 -0
- package/dist/components/ExifPanel.d.ts +26 -0
- package/dist/components/ExifPanel.d.ts.map +1 -0
- package/dist/components/ExifPanel.js +104 -0
- package/dist/components/ExifPanel.js.map +1 -0
- package/dist/components/GlobalDropzone.d.ts +37 -0
- package/dist/components/GlobalDropzone.d.ts.map +1 -0
- package/dist/components/GlobalDropzone.js +154 -0
- package/dist/components/GlobalDropzone.js.map +1 -0
- package/dist/components/HistoryPanel.d.ts +17 -7
- package/dist/components/HistoryPanel.d.ts.map +1 -1
- package/dist/components/HistoryPanel.js +74 -62
- package/dist/components/HistoryPanel.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +3 -1
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +4 -3
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/ProgressBar.d.ts +18 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +35 -0
- package/dist/components/ProgressBar.js.map +1 -0
- package/dist/components/StatusBar.d.ts +8 -2
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +59 -4
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/ThemeToggle.d.ts +20 -0
- package/dist/components/ThemeToggle.d.ts.map +1 -0
- package/dist/components/ThemeToggle.js +60 -0
- package/dist/components/ThemeToggle.js.map +1 -0
- package/dist/components/WorkflowEditor.d.ts +24 -0
- package/dist/components/WorkflowEditor.d.ts.map +1 -0
- package/dist/components/WorkflowEditor.js +160 -0
- package/dist/components/WorkflowEditor.js.map +1 -0
- package/dist/components/WorkflowTemplates.d.ts +23 -0
- package/dist/components/WorkflowTemplates.d.ts.map +1 -0
- package/dist/components/WorkflowTemplates.js +32 -0
- package/dist/components/WorkflowTemplates.js.map +1 -0
- package/dist/components/Workspace.d.ts +48 -2
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +80 -5
- package/dist/components/Workspace.js.map +1 -1
- package/dist/data/workflow-templates.d.ts +42 -0
- package/dist/data/workflow-templates.d.ts.map +1 -0
- package/dist/data/workflow-templates.js +112 -0
- package/dist/data/workflow-templates.js.map +1 -0
- package/dist/hooks/useDebouncedRun.d.ts +49 -0
- package/dist/hooks/useDebouncedRun.d.ts.map +1 -0
- package/dist/hooks/useDebouncedRun.js +117 -0
- package/dist/hooks/useDebouncedRun.js.map +1 -0
- package/dist/hooks/useLokvis.js +1 -1
- package/dist/hooks/useMediaQuery.d.ts +27 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.js +41 -0
- package/dist/hooks/useMediaQuery.js.map +1 -0
- package/dist/hooks/useShareLink.d.ts +58 -0
- package/dist/hooks/useShareLink.d.ts.map +1 -0
- package/dist/hooks/useShareLink.js +120 -0
- package/dist/hooks/useShareLink.js.map +1 -0
- package/dist/hooks/useTheme.d.ts +30 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/hooks/useTheme.js +105 -0
- package/dist/hooks/useTheme.js.map +1 -0
- package/dist/hooks/useWorkflowProgress.d.ts +28 -0
- package/dist/hooks/useWorkflowProgress.d.ts.map +1 -0
- package/dist/hooks/useWorkflowProgress.js +25 -0
- package/dist/hooks/useWorkflowProgress.js.map +1 -0
- package/dist/hooks/useWorkflows.d.ts +86 -0
- package/dist/hooks/useWorkflows.d.ts.map +1 -0
- package/dist/hooks/useWorkflows.js +247 -0
- package/dist/hooks/useWorkflows.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/store/assets-slice.d.ts.map +1 -1
- package/dist/store/assets-slice.js +19 -0
- package/dist/store/assets-slice.js.map +1 -1
- package/dist/store/history-slice.d.ts +18 -0
- package/dist/store/history-slice.d.ts.map +1 -0
- package/dist/store/history-slice.js +42 -0
- package/dist/store/history-slice.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +3 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/runtime-slice.d.ts +10 -2
- package/dist/store/runtime-slice.d.ts.map +1 -1
- package/dist/store/runtime-slice.js +77 -28
- package/dist/store/runtime-slice.js.map +1 -1
- package/dist/store/types.d.ts +70 -1
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/types.js +10 -0
- package/dist/store/types.js.map +1 -1
- package/dist/store/workflow-slice.d.ts +1 -1
- package/dist/store/workflow-slice.d.ts.map +1 -1
- package/dist/store/workflow-slice.js +167 -20
- package/dist/store/workflow-slice.js.map +1 -1
- package/dist/store.d.ts +76 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +217 -0
- package/dist/store.js.map +1 -0
- package/package.json +9 -6
- package/src/components/AssetPanel.tsx +154 -12
- package/src/components/Canvas.tsx +85 -7
- package/src/components/CommandPalette.tsx +300 -0
- package/src/components/CompareSlider.tsx +186 -0
- package/src/components/DownloadPanel.tsx +252 -0
- package/src/components/ErrorBanner.tsx +95 -0
- package/src/components/ExifPanel.tsx +187 -0
- package/src/components/GlobalDropzone.tsx +220 -0
- package/src/components/HistoryPanel.tsx +231 -104
- package/src/components/Inspector.tsx +7 -2
- package/src/components/PipelineBar.tsx +13 -12
- package/src/components/ProgressBar.tsx +104 -0
- package/src/components/StatusBar.tsx +124 -5
- package/src/components/ThemeToggle.tsx +122 -0
- package/src/components/WorkflowEditor.tsx +382 -0
- package/src/components/WorkflowTemplates.tsx +101 -0
- package/src/components/Workspace.tsx +207 -9
- package/src/data/workflow-templates.ts +137 -0
- package/src/hooks/useDebouncedRun.ts +151 -0
- package/src/hooks/useLokvis.ts +1 -1
- package/src/hooks/useMediaQuery.ts +53 -0
- package/src/hooks/useShareLink.ts +158 -0
- package/src/hooks/useTheme.ts +116 -0
- package/src/hooks/useWorkflowProgress.ts +41 -0
- package/src/hooks/useWorkflows.ts +326 -0
- package/src/index.ts +32 -0
- package/src/store/assets-slice.ts +19 -0
- package/src/store/history-slice.ts +65 -0
- package/src/store/index.ts +3 -0
- package/src/store/runtime-slice.ts +89 -32
- package/src/store/types.ts +71 -1
- package/src/store/workflow-slice.ts +178 -20
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useShareLink - 分享链接生成与解析(W11.5)
|
|
3
|
+
*
|
|
4
|
+
* 把当前工作流节点序列编码为 base64 URL 参数,生成可分享的链接。
|
|
5
|
+
* 接收方打开链接即可还原工作流(本地 base64,无需 cloud 短链)。
|
|
6
|
+
*
|
|
7
|
+
* 编码格式:
|
|
8
|
+
* `<origin><pathname>?workflow=<base64url-encoded-json>`
|
|
9
|
+
*
|
|
10
|
+
* 其中 JSON 结构为 `{ v: 1, nodes: [{c, p}] }`(v=版本号,向前兼容)。
|
|
11
|
+
* 使用 base64url 编码(URL 安全,无 +/= 字符)。
|
|
12
|
+
*
|
|
13
|
+
* 注意:
|
|
14
|
+
* - base64 编码会膨胀约 33%,长工作流可能超过 URL 长度限制(浏览器 ~2KB 安全,~8KB 极限)
|
|
15
|
+
* - 5 步工作流通常远小于限制
|
|
16
|
+
* - 不包含 asset 引用(asset 是本地文件,无法跨设备分享)
|
|
17
|
+
*
|
|
18
|
+
* @module useShareLink
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useCallback } from 'react';
|
|
22
|
+
import { useWorkspaceStore } from '../store/index.js';
|
|
23
|
+
|
|
24
|
+
/** 分享链接编码的数据结构 */
|
|
25
|
+
interface ShareableWorkflow {
|
|
26
|
+
/** 版本号(向前兼容) */
|
|
27
|
+
v: 1;
|
|
28
|
+
/** 节点序列(精简字段:c=capability, p=params) */
|
|
29
|
+
nodes: Array<{ c: string; p: Record<string, unknown> }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 标准 base64 → base64url(URL 安全: + → -, / → _, 去除 = 填充) */
|
|
33
|
+
function toBase64Url(b64: string): string {
|
|
34
|
+
return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** base64url → 标准 base64 */
|
|
38
|
+
function fromBase64Url(b64url: string): string {
|
|
39
|
+
let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
40
|
+
// 补齐 padding
|
|
41
|
+
const pad = b64.length % 4;
|
|
42
|
+
if (pad) b64 += '='.repeat(4 - pad);
|
|
43
|
+
return b64;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** UTF-8 安全的 base64 编码(处理非 ASCII 字符如中文水印) */
|
|
47
|
+
function encodeUtf8Base64Url(str: string): string {
|
|
48
|
+
const bytes = new TextEncoder().encode(str);
|
|
49
|
+
let binary = '';
|
|
50
|
+
for (const byte of bytes) {
|
|
51
|
+
binary += String.fromCharCode(byte);
|
|
52
|
+
}
|
|
53
|
+
return toBase64Url(btoa(binary));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** UTF-8 安全的 base64url 解码 */
|
|
57
|
+
function decodeUtf8Base64Url(b64url: string): string {
|
|
58
|
+
const b64 = fromBase64Url(b64url);
|
|
59
|
+
const binary = atob(b64);
|
|
60
|
+
const bytes = new Uint8Array(binary.length);
|
|
61
|
+
for (let i = 0; i < binary.length; i++) {
|
|
62
|
+
bytes[i] = binary.charCodeAt(i);
|
|
63
|
+
}
|
|
64
|
+
return new TextDecoder().decode(bytes);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 编码工作流节点为 base64url 字符串(用于分享链接)。
|
|
69
|
+
* 导出供测试与外部直接使用。
|
|
70
|
+
*/
|
|
71
|
+
export function encodeWorkflowForShare(
|
|
72
|
+
nodes: ReadonlyArray<{ capability: string; params: Record<string, unknown> }>
|
|
73
|
+
): string {
|
|
74
|
+
const data: ShareableWorkflow = {
|
|
75
|
+
v: 1,
|
|
76
|
+
nodes: nodes.map((n) => ({ c: n.capability, p: n.params })),
|
|
77
|
+
};
|
|
78
|
+
return encodeUtf8Base64Url(JSON.stringify(data));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 解码 base64url 字符串为工作流节点。
|
|
83
|
+
* @returns 节点数组;解析失败返回 null。
|
|
84
|
+
*/
|
|
85
|
+
export function decodeWorkflowFromShare(
|
|
86
|
+
encoded: string
|
|
87
|
+
): Array<{ capability: string; params: Record<string, unknown> }> | null {
|
|
88
|
+
try {
|
|
89
|
+
const json = decodeUtf8Base64Url(encoded);
|
|
90
|
+
const data = JSON.parse(json) as ShareableWorkflow;
|
|
91
|
+
if (!data || typeof data !== 'object' || data.v !== 1 || !Array.isArray(data.nodes)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return data.nodes.map((n) => ({
|
|
95
|
+
capability: String(n.c),
|
|
96
|
+
params: (n.p && typeof n.p === 'object' ? n.p : {}) as Record<string, unknown>,
|
|
97
|
+
}));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface UseShareLinkResult {
|
|
104
|
+
/**
|
|
105
|
+
* 生成分享链接(基于当前 store 中的 nodes)。
|
|
106
|
+
* 返回完整 URL;若 nodes 为空返回 null。
|
|
107
|
+
*/
|
|
108
|
+
generateShareUrl(): string | null;
|
|
109
|
+
/**
|
|
110
|
+
* 从 URL 解析工作流节点。
|
|
111
|
+
* @param url 完整 URL 或仅 search 部分
|
|
112
|
+
* @returns 节点数组;无 workflow 参数或解析失败返回 null
|
|
113
|
+
*/
|
|
114
|
+
parseShareUrl(url: string): Array<{ capability: string; params: Record<string, unknown> }> | null;
|
|
115
|
+
/**
|
|
116
|
+
* 从当前页面 URL 加载工作流(若有 ?workflow=)。
|
|
117
|
+
* @returns true 表示成功加载并应用到 store
|
|
118
|
+
*/
|
|
119
|
+
loadFromCurrentUrl(): boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function useShareLink(): UseShareLinkResult {
|
|
123
|
+
const nodes = useWorkspaceStore((s) => s.nodes);
|
|
124
|
+
const loadWorkflowTemplate = useWorkspaceStore((s) => s.loadWorkflowTemplate);
|
|
125
|
+
|
|
126
|
+
const generateShareUrl = useCallback((): string | null => {
|
|
127
|
+
if (nodes.length === 0) return null;
|
|
128
|
+
const encoded = encodeWorkflowForShare(nodes);
|
|
129
|
+
if (typeof window === 'undefined') return null;
|
|
130
|
+
const { origin, pathname } = window.location;
|
|
131
|
+
return `${origin}${pathname}?workflow=${encoded}`;
|
|
132
|
+
}, [nodes]);
|
|
133
|
+
|
|
134
|
+
const parseShareUrl = useCallback(
|
|
135
|
+
(url: string): Array<{ capability: string; params: Record<string, unknown> }> | null => {
|
|
136
|
+
const searchPart = url.includes('?') ? url.slice(url.indexOf('?') + 1) : '';
|
|
137
|
+
const params = new URLSearchParams(searchPart);
|
|
138
|
+
const encoded = params.get('workflow');
|
|
139
|
+
if (!encoded) return null;
|
|
140
|
+
return decodeWorkflowFromShare(encoded);
|
|
141
|
+
},
|
|
142
|
+
[]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const loadFromCurrentUrl = useCallback((): boolean => {
|
|
146
|
+
if (typeof window === 'undefined') return false;
|
|
147
|
+
const parsed = parseShareUrl(window.location.href);
|
|
148
|
+
if (!parsed || parsed.length === 0) return false;
|
|
149
|
+
loadWorkflowTemplate(parsed);
|
|
150
|
+
return true;
|
|
151
|
+
}, [parseShareUrl, loadWorkflowTemplate]);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
generateShareUrl,
|
|
155
|
+
parseShareUrl,
|
|
156
|
+
loadFromCurrentUrl,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTheme - 暗色模式切换 + 持久化(W9.7)
|
|
3
|
+
*
|
|
4
|
+
* 监听并切换 `<html>` 的 `.dark` / `.light` 类,与 `tokens.css` 双触发机制对齐:
|
|
5
|
+
* - 显式设置:`document.documentElement.classList` 添加 `dark` 或 `light`
|
|
6
|
+
* - 系统兜底:未显式设置时,`@media prefers-color-scheme` 自动生效
|
|
7
|
+
*
|
|
8
|
+
* 持久化:`localStorage.theme = 'dark' | 'light'`,刷新后恢复。
|
|
9
|
+
* 跨 tab 同步:监听 `storage` 事件,一个 tab 切换,其他 tab 同步生效。
|
|
10
|
+
*
|
|
11
|
+
* 系统偏好变化:未显式设置主题时,跟随 `matchMedia('(prefers-color-scheme: dark)')`。
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const { theme, resolvedTheme, toggleTheme, setTheme } = useTheme();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
20
|
+
|
|
21
|
+
export type ThemeMode = 'light' | 'dark';
|
|
22
|
+
|
|
23
|
+
const STORAGE_KEY = 'lokvis-theme';
|
|
24
|
+
|
|
25
|
+
function readStoredTheme(): ThemeMode | null {
|
|
26
|
+
if (typeof window === 'undefined') return null;
|
|
27
|
+
try {
|
|
28
|
+
const v = window.localStorage.getItem(STORAGE_KEY);
|
|
29
|
+
if (v === 'light' || v === 'dark') return v;
|
|
30
|
+
} catch {
|
|
31
|
+
// localStorage 不可用(隐私模式 / iframe 沙箱)— 退回系统偏好
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readSystemTheme(): ThemeMode {
|
|
37
|
+
if (typeof window === 'undefined' || !window.matchMedia) return 'light';
|
|
38
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 把显式主题应用到 <html>(null 表示清空,回退到系统偏好) */
|
|
42
|
+
function applyTheme(theme: ThemeMode | null) {
|
|
43
|
+
if (typeof document === 'undefined') return;
|
|
44
|
+
const root = document.documentElement;
|
|
45
|
+
root.classList.remove('dark', 'light');
|
|
46
|
+
if (theme === 'dark') root.classList.add('dark');
|
|
47
|
+
else if (theme === 'light') root.classList.add('light');
|
|
48
|
+
// theme === null: 两个类都不加 → 由 @media prefers-color-scheme 接管
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface UseThemeResult {
|
|
52
|
+
/** 用户显式选择的主题(未设置时为 null,跟随系统) */
|
|
53
|
+
theme: ThemeMode | null;
|
|
54
|
+
/** 实际生效的主题(显式选择或系统偏好) */
|
|
55
|
+
resolvedTheme: ThemeMode;
|
|
56
|
+
/** 设置指定主题(传 null 表示跟随系统) */
|
|
57
|
+
setTheme(theme: ThemeMode | null): void;
|
|
58
|
+
/** 在 light / dark 之间切换(无显式主题时基于 resolvedTheme 决定下一态) */
|
|
59
|
+
toggleTheme(): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useTheme(): UseThemeResult {
|
|
63
|
+
const [theme, setThemeState] = useState<ThemeMode | null>(() => readStoredTheme());
|
|
64
|
+
const [systemTheme, setSystemTheme] = useState<ThemeMode>(() => readSystemTheme());
|
|
65
|
+
|
|
66
|
+
// 监听系统偏好变化(仅在用户未显式选择时影响 resolvedTheme)
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (typeof window === 'undefined' || !window.matchMedia) return;
|
|
69
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
70
|
+
const onChange = () => setSystemTheme(mq.matches ? 'dark' : 'light');
|
|
71
|
+
mq.addEventListener('change', onChange);
|
|
72
|
+
return () => mq.removeEventListener('change', onChange);
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
// 应用主题到 <html>
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
applyTheme(theme);
|
|
78
|
+
}, [theme]);
|
|
79
|
+
|
|
80
|
+
// 跨 tab 同步:监听 storage 事件
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (typeof window === 'undefined') return;
|
|
83
|
+
const onStorage = (e: StorageEvent) => {
|
|
84
|
+
if (e.key !== STORAGE_KEY) return;
|
|
85
|
+
setThemeState(readStoredTheme());
|
|
86
|
+
};
|
|
87
|
+
window.addEventListener('storage', onStorage);
|
|
88
|
+
return () => window.removeEventListener('storage', onStorage);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
const setTheme = useCallback((next: ThemeMode | null) => {
|
|
92
|
+
setThemeState(next);
|
|
93
|
+
try {
|
|
94
|
+
if (typeof window === 'undefined') return;
|
|
95
|
+
if (next === null) {
|
|
96
|
+
window.localStorage.removeItem(STORAGE_KEY);
|
|
97
|
+
} else {
|
|
98
|
+
window.localStorage.setItem(STORAGE_KEY, next);
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// 写入失败(隐私模式 / quota)— 不影响运行时切换,只是无法持久化
|
|
102
|
+
}
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const toggleTheme = useCallback(() => {
|
|
106
|
+
const resolved = theme ?? systemTheme;
|
|
107
|
+
setTheme(resolved === 'dark' ? 'light' : 'dark');
|
|
108
|
+
}, [theme, systemTheme, setTheme]);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
theme,
|
|
112
|
+
resolvedTheme: theme ?? systemTheme,
|
|
113
|
+
setTheme,
|
|
114
|
+
toggleTheme,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWorkflowProgress - 工作流执行进度(W11.6,review 抽取)
|
|
3
|
+
*
|
|
4
|
+
* 单一来源计算节点执行进度,供 ProgressBar / StatusBar 共用,
|
|
5
|
+
* 避免两组件各自从 nodes 重复计算 done/total/pct。
|
|
6
|
+
*
|
|
7
|
+
* 计算口径:
|
|
8
|
+
* - done: 已结束节点(success + failed + cancelled)
|
|
9
|
+
* - failedCount: 失败节点数(用于红色高亮)
|
|
10
|
+
* - pct: 完成百分比(0-100),total=0 时为 0
|
|
11
|
+
*
|
|
12
|
+
* @module useWorkflowProgress
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useWorkspaceStore } from '../store/index.js';
|
|
16
|
+
|
|
17
|
+
export interface WorkflowProgress {
|
|
18
|
+
/** 节点总数 */
|
|
19
|
+
total: number;
|
|
20
|
+
/** 已结束节点数(success + failed + cancelled) */
|
|
21
|
+
done: number;
|
|
22
|
+
/** 失败节点数 */
|
|
23
|
+
failedCount: number;
|
|
24
|
+
/** 完成百分比(0-100) */
|
|
25
|
+
pct: number;
|
|
26
|
+
/** 是否有失败节点 */
|
|
27
|
+
hasFailure: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 计算工作流执行进度(单一来源,供 ProgressBar / StatusBar 共用) */
|
|
31
|
+
export function useWorkflowProgress(): WorkflowProgress {
|
|
32
|
+
const nodes = useWorkspaceStore((s) => s.nodes);
|
|
33
|
+
const total = nodes.length;
|
|
34
|
+
const done = nodes.filter(
|
|
35
|
+
(n) => n.status === 'success' || n.status === 'failed' || n.status === 'cancelled'
|
|
36
|
+
).length;
|
|
37
|
+
const failedCount = nodes.filter((n) => n.status === 'failed').length;
|
|
38
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
39
|
+
const hasFailure = failedCount > 0;
|
|
40
|
+
return { total, done, failedCount, pct, hasFailure };
|
|
41
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWorkflows - 工作流槽位持久化 hook(W10.6 + W10.7)
|
|
3
|
+
*
|
|
4
|
+
* 提供 5 个工作流槽位的本地保存能力(免费 5 / Pro 无限),以及 JSON 导入/导出。
|
|
5
|
+
* 数据存储在 localStorage 的 `lokvis.workflows` key,JSON 数组形式。
|
|
6
|
+
*
|
|
7
|
+
* 与 useCustomPresets 的区别:
|
|
8
|
+
* - 自定义预设是尺寸配置(width/height/fit)
|
|
9
|
+
* - 工作流槽位是完整 Workflow 定义(含 nodes/edges/inputs/outputs)
|
|
10
|
+
*
|
|
11
|
+
* 序列化策略:
|
|
12
|
+
* - 保存时用 workflowSchema.safeParse() 防御损坏数据(数据完整性)
|
|
13
|
+
* - 加载时用 workflowSchema.safeParse() 过滤无效条目(向前兼容)
|
|
14
|
+
* - JSON 导出:Workflow JSON + $schema 字段
|
|
15
|
+
* - JSON 导入:validateWorkflow() 完整校验(含 capability 兼容性若提供回调)
|
|
16
|
+
*
|
|
17
|
+
* 多 tab / 多实例同步:
|
|
18
|
+
* - 原生 storage 事件:跨 tab 同步
|
|
19
|
+
* - 自定义 SYNC_EVENT:同 tab 多实例同步
|
|
20
|
+
*
|
|
21
|
+
* @module useWorkflows
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
25
|
+
import type { Workflow } from '@lokvis/schema';
|
|
26
|
+
import { workflowSchema, validateWorkflow, type ValidateWorkflowOptions } from '@lokvis/schema';
|
|
27
|
+
|
|
28
|
+
const STORAGE_KEY = 'lokvis.workflows';
|
|
29
|
+
/** 同 tab 多实例同步用的自定义事件名 */
|
|
30
|
+
const SYNC_EVENT = 'lokvis:workflows-change';
|
|
31
|
+
|
|
32
|
+
/** 免费用户工作流槽位上限 */
|
|
33
|
+
export const FREE_WORKFLOW_LIMIT = 5;
|
|
34
|
+
/** Pro 用户无上限 */
|
|
35
|
+
export const PRO_WORKFLOW_LIMIT = Infinity;
|
|
36
|
+
|
|
37
|
+
/** 工作流槽位存储格式 */
|
|
38
|
+
export interface WorkflowSlot {
|
|
39
|
+
/** 唯一 ID */
|
|
40
|
+
id: string;
|
|
41
|
+
/** 用户自定义名称(默认取 workflow.name) */
|
|
42
|
+
name: string;
|
|
43
|
+
/** 完整 Workflow 定义 */
|
|
44
|
+
workflow: Workflow;
|
|
45
|
+
/** 创建时间戳 */
|
|
46
|
+
createdAt: number;
|
|
47
|
+
/** 最后更新时间戳 */
|
|
48
|
+
updatedAt: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 保存工作流时的输入 */
|
|
52
|
+
export interface SaveWorkflowInput {
|
|
53
|
+
/** 工作流名称(可选,默认取 workflow.name) */
|
|
54
|
+
name?: string;
|
|
55
|
+
/** 完整 Workflow 定义 */
|
|
56
|
+
workflow: Workflow;
|
|
57
|
+
/** 若指定则更新该 id 的槽位,否则新增 */
|
|
58
|
+
id?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface UseWorkflowsResult {
|
|
62
|
+
/** 当前所有工作流槽位(按 updatedAt 降序) */
|
|
63
|
+
slots: WorkflowSlot[];
|
|
64
|
+
/** 保存或更新工作流;超限时抛 Error */
|
|
65
|
+
save(input: SaveWorkflowInput): WorkflowSlot;
|
|
66
|
+
/** 按 id 删除槽位 */
|
|
67
|
+
remove(id: string): void;
|
|
68
|
+
/** 按 id 加载工作流到 store(返回 Workflow) */
|
|
69
|
+
load(id: string): Workflow | null;
|
|
70
|
+
/** 当前上限(免费 5 / Pro 无限) */
|
|
71
|
+
limit: number;
|
|
72
|
+
/** 是否还能再保存 */
|
|
73
|
+
canSaveMore: boolean;
|
|
74
|
+
/** 还能再保存多少个 */
|
|
75
|
+
remaining: number;
|
|
76
|
+
/** 导出工作流为 JSON 字符串(可直接下载) */
|
|
77
|
+
exportToJson(id: string): string | null;
|
|
78
|
+
/** 导出所有工作流为 JSON 数组字符串 */
|
|
79
|
+
exportAllToJson(): string;
|
|
80
|
+
/** 从 JSON 字符导入工作流(单个或数组);返回导入结果(含跳过原因) */
|
|
81
|
+
importFromJson(
|
|
82
|
+
json: string,
|
|
83
|
+
options?: { resolveCapability?: ValidateWorkflowOptions['resolveCapability']; maxSteps?: number }
|
|
84
|
+
): { imported: number; skipped: Array<{ reason: string }> };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 类型守卫:判断 unknown 是否为 Record<string, unknown> */
|
|
88
|
+
function isRecord(x: unknown): x is Record<string, unknown> {
|
|
89
|
+
return typeof x === 'object' && x !== null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 从 localStorage 读取工作流槽位。
|
|
93
|
+
* 容错:JSON 解析失败 / 非数组 / 字段缺失时返回空数组。 */
|
|
94
|
+
function readFromStorage(): WorkflowSlot[] {
|
|
95
|
+
if (typeof window === 'undefined') return [];
|
|
96
|
+
try {
|
|
97
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
98
|
+
if (!raw) return [];
|
|
99
|
+
const parsed: unknown = JSON.parse(raw);
|
|
100
|
+
if (!Array.isArray(parsed)) return [];
|
|
101
|
+
// 字段白名单过滤 + workflowSchema 校验(防御损坏数据)
|
|
102
|
+
return parsed.filter((it): it is WorkflowSlot => {
|
|
103
|
+
if (!isRecord(it)) return false;
|
|
104
|
+
if (
|
|
105
|
+
typeof it.id !== 'string' ||
|
|
106
|
+
typeof it.name !== 'string' ||
|
|
107
|
+
typeof it.createdAt !== 'number' ||
|
|
108
|
+
typeof it.updatedAt !== 'number'
|
|
109
|
+
) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// workflow 字段必须通过 schema 校验
|
|
113
|
+
const result = workflowSchema.safeParse(it.workflow);
|
|
114
|
+
return result.success;
|
|
115
|
+
});
|
|
116
|
+
} catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 写入 localStorage。
|
|
123
|
+
* @returns true 成功;false 失败(隐私模式 / 配额超限 / 序列化异常)
|
|
124
|
+
* 失败时不静默:调用方需感知以便提示用户"保存失败,数据未持久化"
|
|
125
|
+
*/
|
|
126
|
+
function writeToStorage(slots: WorkflowSlot[]): boolean {
|
|
127
|
+
if (typeof window === 'undefined') return false;
|
|
128
|
+
try {
|
|
129
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(slots));
|
|
130
|
+
window.dispatchEvent(new CustomEvent(SYNC_EVENT));
|
|
131
|
+
return true;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
// 隐私模式 / Safari 配额 / QuotaExceededError:不静默吞错,
|
|
134
|
+
// 但也不抛错(保存是 fire-and-forget 操作);返回 false 让调用方决策。
|
|
135
|
+
console.warn('[lokvis] Failed to persist workflows to localStorage:', err);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 生成唯一 ID */
|
|
141
|
+
function genId(): string {
|
|
142
|
+
return `wf-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param isPro 是否为 Pro 模式。父组件从 runtime.isPro 传入。
|
|
147
|
+
* 影响 limit:免费 5 / Pro 无上限。
|
|
148
|
+
*/
|
|
149
|
+
export function useWorkflows(isPro = false): UseWorkflowsResult {
|
|
150
|
+
const limit = isPro ? PRO_WORKFLOW_LIMIT : FREE_WORKFLOW_LIMIT;
|
|
151
|
+
const [slots, setSlots] = useState<WorkflowSlot[]>(() => readFromStorage());
|
|
152
|
+
|
|
153
|
+
// 监听跨 tab storage + 同 tab SYNC_EVENT
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
const handler = () => setSlots(readFromStorage());
|
|
156
|
+
window.addEventListener('storage', handler);
|
|
157
|
+
window.addEventListener(SYNC_EVENT, handler);
|
|
158
|
+
return () => {
|
|
159
|
+
window.removeEventListener('storage', handler);
|
|
160
|
+
window.removeEventListener(SYNC_EVENT, handler);
|
|
161
|
+
};
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const save = useCallback(
|
|
165
|
+
(input: SaveWorkflowInput): WorkflowSlot => {
|
|
166
|
+
const current = readFromStorage();
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
// 更新已有
|
|
169
|
+
if (input.id) {
|
|
170
|
+
const existing = current.find((s) => s.id === input.id);
|
|
171
|
+
if (existing) {
|
|
172
|
+
const updated: WorkflowSlot = {
|
|
173
|
+
...existing,
|
|
174
|
+
name: input.name ?? input.workflow.name,
|
|
175
|
+
workflow: input.workflow,
|
|
176
|
+
updatedAt: now,
|
|
177
|
+
};
|
|
178
|
+
const next = current.map((s) => (s.id === input.id ? updated : s));
|
|
179
|
+
const ok = writeToStorage(next);
|
|
180
|
+
if (!ok) {
|
|
181
|
+
// 写入失败仍更新内存 state(用户当前会话可见),但提示未持久化
|
|
182
|
+
console.warn('[lokvis] 工作流未持久化到 localStorage,仅当前会话有效');
|
|
183
|
+
}
|
|
184
|
+
setSlots(next);
|
|
185
|
+
return updated;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 新增
|
|
189
|
+
if (current.length >= limit) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`工作流槽位已达上限(${limit} 个)${isPro ? '' : ',升级 Pro 可无限制保存'}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const slot: WorkflowSlot = {
|
|
195
|
+
id: genId(),
|
|
196
|
+
name: input.name ?? input.workflow.name,
|
|
197
|
+
workflow: input.workflow,
|
|
198
|
+
createdAt: now,
|
|
199
|
+
updatedAt: now,
|
|
200
|
+
};
|
|
201
|
+
const next = [...current, slot];
|
|
202
|
+
const ok = writeToStorage(next);
|
|
203
|
+
if (!ok) {
|
|
204
|
+
console.warn('[lokvis] 工作流未持久化到 localStorage,仅当前会话有效');
|
|
205
|
+
}
|
|
206
|
+
setSlots(next);
|
|
207
|
+
return slot;
|
|
208
|
+
},
|
|
209
|
+
[limit, isPro]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const remove = useCallback((id: string) => {
|
|
213
|
+
const current = readFromStorage();
|
|
214
|
+
const next = current.filter((s) => s.id !== id);
|
|
215
|
+
writeToStorage(next);
|
|
216
|
+
setSlots(next);
|
|
217
|
+
}, []);
|
|
218
|
+
|
|
219
|
+
const load = useCallback((id: string): Workflow | null => {
|
|
220
|
+
const current = readFromStorage();
|
|
221
|
+
const slot = current.find((s) => s.id === id);
|
|
222
|
+
return slot ? slot.workflow : null;
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
const exportToJson = useCallback((id: string): string | null => {
|
|
226
|
+
const current = readFromStorage();
|
|
227
|
+
const slot = current.find((s) => s.id === id);
|
|
228
|
+
if (!slot) return null;
|
|
229
|
+
return JSON.stringify(slot.workflow, null, 2);
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
const exportAllToJson = useCallback((): string => {
|
|
233
|
+
const current = readFromStorage();
|
|
234
|
+
return JSON.stringify(current.map((s) => s.workflow), null, 2);
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
237
|
+
const importFromJson = useCallback(
|
|
238
|
+
(
|
|
239
|
+
json: string,
|
|
240
|
+
options?: {
|
|
241
|
+
resolveCapability?: ValidateWorkflowOptions['resolveCapability'];
|
|
242
|
+
maxSteps?: number;
|
|
243
|
+
}
|
|
244
|
+
): { imported: number; skipped: Array<{ reason: string }> } => {
|
|
245
|
+
let parsed: unknown;
|
|
246
|
+
try {
|
|
247
|
+
parsed = JSON.parse(json);
|
|
248
|
+
} catch {
|
|
249
|
+
throw new Error('JSON 格式错误,无法解析');
|
|
250
|
+
}
|
|
251
|
+
const list: unknown[] = Array.isArray(parsed) ? parsed : [parsed];
|
|
252
|
+
const current = readFromStorage();
|
|
253
|
+
const imported: WorkflowSlot[] = [];
|
|
254
|
+
// 收集被跳过条目的原因,供调用方提示用户(不再静默吞错)
|
|
255
|
+
const skipped: Array<{ reason: string }> = [];
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
|
|
258
|
+
for (const item of list) {
|
|
259
|
+
// 先做 zod 形状校验
|
|
260
|
+
const shapeResult = workflowSchema.safeParse(item);
|
|
261
|
+
if (!shapeResult.success) {
|
|
262
|
+
// 跳过无效条目而非抛错,继续处理后续;记录原因供调用方提示
|
|
263
|
+
const detail = shapeResult.error.issues
|
|
264
|
+
.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`)
|
|
265
|
+
.join('; ');
|
|
266
|
+
skipped.push({ reason: `shape validation failed: ${detail}` });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// 再做完整结构校验(含 capability 兼容性若提供回调)
|
|
270
|
+
const fullResult = validateWorkflow(item, options);
|
|
271
|
+
if (!fullResult.success) {
|
|
272
|
+
// 结构问题也跳过(导入是宽容操作,不阻断有效条目),但记录原因
|
|
273
|
+
const detail = fullResult.error.issues
|
|
274
|
+
.map((i) => i.message)
|
|
275
|
+
.join('; ');
|
|
276
|
+
skipped.push({ reason: `structural validation failed: ${detail}` });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// 检查上限
|
|
280
|
+
if (current.length + imported.length >= limit) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`导入会超过上限(${limit} 个),已导入 ${imported.length} 条,请先删除部分工作流`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
imported.push({
|
|
286
|
+
id: genId(),
|
|
287
|
+
name: shapeResult.data.name,
|
|
288
|
+
workflow: shapeResult.data,
|
|
289
|
+
createdAt: now,
|
|
290
|
+
updatedAt: now,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (imported.length === 0) {
|
|
295
|
+
// 不再以"没有有效工作流"作为唯一信息;把跳过原因一并抛出,
|
|
296
|
+
// 让用户知道为什么一个都没导入(而非笼统失败)
|
|
297
|
+
const skipSummary = skipped.length > 0
|
|
298
|
+
? `; skipped: ${skipped.map((s) => s.reason).join(' | ')}`
|
|
299
|
+
: '';
|
|
300
|
+
throw new Error(`JSON 中没有有效的工作流定义${skipSummary}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const next = [...current, ...imported];
|
|
304
|
+
writeToStorage(next);
|
|
305
|
+
setSlots(next);
|
|
306
|
+
return { imported: imported.length, skipped };
|
|
307
|
+
},
|
|
308
|
+
[limit]
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const canSaveMore = slots.length < limit;
|
|
312
|
+
const remaining = Math.max(0, limit - slots.length);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
slots,
|
|
316
|
+
save,
|
|
317
|
+
remove,
|
|
318
|
+
load,
|
|
319
|
+
limit,
|
|
320
|
+
canSaveMore,
|
|
321
|
+
remaining,
|
|
322
|
+
exportToJson,
|
|
323
|
+
exportAllToJson,
|
|
324
|
+
importFromJson,
|
|
325
|
+
};
|
|
326
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,20 +11,52 @@
|
|
|
11
11
|
* - `<PipelineBar />` - Workflow pipeline step bar
|
|
12
12
|
* - `<Toolbar />` - Top toolbar
|
|
13
13
|
* - `<StatusBar />` - Bottom status bar
|
|
14
|
+
* - `<HistoryPanel />` - History panel (W7.1, supports horizontal variant W9.1)
|
|
15
|
+
* - `<CommandPalette />` - ⌘K command palette (W9.2)
|
|
16
|
+
* - `<GlobalDropzone />` - Full-screen drag-drop with MIME validation (W9.3)
|
|
17
|
+
* - `<CompareSlider />` - before/after comparison slider (W9.4)
|
|
18
|
+
* - `<DownloadPanel />` - Workflow outputs download panel (W9.5)
|
|
19
|
+
* - `<ThemeToggle />` - Dark mode toggle button (W9.7)
|
|
14
20
|
* - `useLokvis()` - React hook for initializing Runtime + plugins
|
|
21
|
+
* - `useTheme()` - Dark mode hook with localStorage + system preference (W9.7)
|
|
22
|
+
* - `useMediaQuery()` / `useBreakpoints()` - Responsive hooks (W9.8)
|
|
23
|
+
* - `useCommandPalette()` - ⌘K shortcut registration hook (W9.2)
|
|
15
24
|
*/
|
|
16
25
|
|
|
17
26
|
export { useLokvis, type UseLokvisOptions, type UseLokvisResult } from './hooks/useLokvis.js';
|
|
27
|
+
export { useTheme, type UseThemeResult, type ThemeMode } from './hooks/useTheme.js';
|
|
28
|
+
export { useMediaQuery, useBreakpoints, type Breakpoints } from './hooks/useMediaQuery.js';
|
|
29
|
+
export { useWorkflows, FREE_WORKFLOW_LIMIT, PRO_WORKFLOW_LIMIT, type UseWorkflowsResult, type WorkflowSlot, type SaveWorkflowInput } from './hooks/useWorkflows.js';
|
|
30
|
+
export { useDebouncedRun, type UseDebouncedRunOptions, type UseDebouncedRunResult } from './hooks/useDebouncedRun.js';
|
|
31
|
+
export { useShareLink, type UseShareLinkResult, encodeWorkflowForShare, decodeWorkflowFromShare } from './hooks/useShareLink.js';
|
|
18
32
|
export { useWorkspaceStore, type WorkspaceState, type WorkspaceActions } from './store/index.js';
|
|
19
33
|
|
|
20
34
|
export { Workspace, type WorkspaceProps } from './components/Workspace.js';
|
|
21
35
|
export { AssetPanel } from './components/AssetPanel.js';
|
|
22
36
|
export { Canvas } from './components/Canvas.js';
|
|
23
37
|
export { Inspector } from './components/Inspector.js';
|
|
38
|
+
export { ExifPanel, type ExifPanelProps } from './components/ExifPanel.js';
|
|
24
39
|
export { PipelineBar } from './components/PipelineBar.js';
|
|
40
|
+
export { WorkflowEditor, type WorkflowEditorProps } from './components/WorkflowEditor.js';
|
|
41
|
+
export { ProgressBar, type ProgressBarProps } from './components/ProgressBar.js';
|
|
42
|
+
export { ErrorBanner, type ErrorBannerProps } from './components/ErrorBanner.js';
|
|
43
|
+
export { WorkflowTemplates, type WorkflowTemplatesProps } from './components/WorkflowTemplates.js';
|
|
25
44
|
export { Toolbar } from './components/Toolbar.js';
|
|
26
45
|
export { StatusBar } from './components/StatusBar.js';
|
|
27
46
|
export { ParamForm, type ParamFormProps } from './components/ParamForm.js';
|
|
47
|
+
export { HistoryPanel, type HistoryPanelProps } from './components/HistoryPanel.js';
|
|
48
|
+
export { CommandPalette, useCommandPalette, type CommandPaletteProps, type UseCommandPaletteOptions } from './components/CommandPalette.js';
|
|
49
|
+
export { GlobalDropzone, type GlobalDropzoneProps } from './components/GlobalDropzone.js';
|
|
50
|
+
export { CompareSlider, type CompareSliderProps } from './components/CompareSlider.js';
|
|
51
|
+
export { DownloadPanel, type DownloadPanelProps } from './components/DownloadPanel.js';
|
|
52
|
+
export { ThemeToggle, type ThemeToggleProps } from './components/ThemeToggle.js';
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
WORKFLOW_TEMPLATES,
|
|
56
|
+
type WorkflowTemplate,
|
|
57
|
+
type WorkflowTemplateNode,
|
|
58
|
+
findTemplate,
|
|
59
|
+
} from './data/workflow-templates.js';
|
|
28
60
|
|
|
29
61
|
export type {
|
|
30
62
|
WorkspaceEvent,
|