@rightkit/video-ui 0.0.0-stage → 0.1.1
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/CHANGELOG.md +10 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +47 -2
- package/dist/appName.d.ts +2 -0
- package/dist/appName.js +8 -0
- package/dist/components/DesktopWorkspace.d.ts +4 -0
- package/dist/components/DesktopWorkspace.js +527 -0
- package/dist/components/Player.d.ts +49 -0
- package/dist/components/Player.js +335 -0
- package/dist/components/RangePreview.d.ts +9 -0
- package/dist/components/RangePreview.js +22 -0
- package/dist/components/Workspace.css +15 -0
- package/dist/demo/DemoSettings.d.ts +1 -0
- package/dist/demo/DemoSettings.js +43 -0
- package/dist/demo/bindings.d.ts +121 -0
- package/dist/demo/bindings.js +57 -0
- package/dist/demo/demo.css +124 -0
- package/dist/demo/useDemo.d.ts +8 -0
- package/dist/demo/useDemo.js +37 -0
- package/dist/editor/CaptionInspector.d.ts +8 -0
- package/dist/editor/CaptionInspector.js +33 -0
- package/dist/editor/ChangesPanel.d.ts +13 -0
- package/dist/editor/ChangesPanel.js +87 -0
- package/dist/editor/ColourInspector.d.ts +5 -0
- package/dist/editor/ColourInspector.js +341 -0
- package/dist/editor/Inspector.d.ts +15 -0
- package/dist/editor/Inspector.js +202 -0
- package/dist/editor/Timeline.d.ts +39 -0
- package/dist/editor/Timeline.js +506 -0
- package/dist/editor/TimelineEditorView.d.ts +9 -0
- package/dist/editor/TimelineEditorView.js +237 -0
- package/dist/editor/TranscriptPanel.d.ts +20 -0
- package/dist/editor/TranscriptPanel.js +180 -0
- package/dist/editor/actions.d.ts +63 -0
- package/dist/editor/actions.js +124 -0
- package/dist/editor/adapter.d.ts +76 -0
- package/dist/editor/adapter.js +181 -0
- package/dist/editor/capabilities.d.ts +37 -0
- package/dist/editor/capabilities.js +33 -0
- package/dist/editor/gesture.d.ts +29 -0
- package/dist/editor/gesture.js +119 -0
- package/dist/editor/index.d.ts +10 -0
- package/dist/editor/index.js +7 -0
- package/dist/editor/keys.d.ts +15 -0
- package/dist/editor/keys.js +42 -0
- package/dist/editor/model.d.ts +88 -0
- package/dist/editor/model.js +87 -0
- package/dist/editor/transcript.d.ts +27 -0
- package/dist/editor/transcript.js +59 -0
- package/dist/editor/vendor/hyperframes/PlayheadIndicator.d.ts +9 -0
- package/dist/editor/vendor/hyperframes/PlayheadIndicator.js +38 -0
- package/dist/editor/vendor/hyperframes/audioWaveformPeaks.d.ts +7 -0
- package/dist/editor/vendor/hyperframes/audioWaveformPeaks.js +47 -0
- package/dist/editor/vendor/hyperframes/rulerGeometry.d.ts +13 -0
- package/dist/editor/vendor/hyperframes/rulerGeometry.js +133 -0
- package/dist/editor/vendor/hyperframes/useInspectorGestureTransaction.d.ts +33 -0
- package/dist/editor/vendor/hyperframes/useInspectorGestureTransaction.js +126 -0
- package/dist/env.d.ts +66 -0
- package/dist/hooks/useEngine.d.ts +10 -0
- package/dist/hooks/useEngine.js +68 -0
- package/dist/hooks/useHdrToSdr.d.ts +11 -0
- package/dist/hooks/useHdrToSdr.js +19 -0
- package/dist/hooks/useProgress.d.ts +10 -0
- package/dist/hooks/useProgress.js +32 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +57 -0
- package/dist/legal/legalConfig.d.ts +38 -0
- package/dist/legal/legalConfig.js +77 -0
- package/dist/lib/assemble.d.ts +100 -0
- package/dist/lib/assemble.js +72 -0
- package/dist/lib/asset.d.ts +2 -0
- package/dist/lib/asset.js +3 -0
- package/dist/lib/dialog.d.ts +4 -0
- package/dist/lib/dialog.js +44 -0
- package/dist/lib/engine.d.ts +44 -0
- package/dist/lib/engine.js +51 -0
- package/dist/lib/genright.d.ts +90 -0
- package/dist/lib/genright.js +160 -0
- package/dist/lib/media.d.ts +1045 -0
- package/dist/lib/media.js +171 -0
- package/dist/lib/mediaAdapters.d.ts +18 -0
- package/dist/lib/mediaAdapters.js +46 -0
- package/dist/lib/mic.d.ts +38 -0
- package/dist/lib/mic.js +14 -0
- package/dist/lib/models.d.ts +53 -0
- package/dist/lib/models.js +72 -0
- package/dist/lib/modelsBackend.d.ts +3 -0
- package/dist/lib/modelsBackend.js +68 -0
- package/dist/lib/progress.d.ts +77 -0
- package/dist/lib/progress.js +192 -0
- package/dist/lib/project.d.ts +138 -0
- package/dist/lib/project.js +48 -0
- package/dist/lib/services.d.ts +27 -0
- package/dist/lib/services.js +38 -0
- package/dist/lib/settingsBackend.d.ts +2 -0
- package/dist/lib/settingsBackend.js +57 -0
- package/dist/lib/shell.d.ts +172 -0
- package/dist/lib/shell.js +32 -0
- package/dist/styles.css +24 -0
- package/dist/ui/icons.d.ts +7 -0
- package/dist/ui/icons.js +62 -0
- package/dist/ui/index.d.ts +45 -0
- package/dist/ui/index.js +43 -0
- package/dist/ui/ui.css +104 -0
- package/dist/views/Assets.css +99 -0
- package/dist/views/AssetsView.d.ts +6 -0
- package/dist/views/AssetsView.js +371 -0
- package/dist/views/AutoEditView.d.ts +16 -0
- package/dist/views/AutoEditView.js +452 -0
- package/dist/views/BatchDelivery.d.ts +3 -0
- package/dist/views/BatchDelivery.js +331 -0
- package/dist/views/BundleExport.d.ts +3 -0
- package/dist/views/BundleExport.js +81 -0
- package/dist/views/Exports.css +117 -0
- package/dist/views/ExportsView.d.ts +12 -0
- package/dist/views/ExportsView.js +354 -0
- package/dist/views/FinishedEdit.css +546 -0
- package/dist/views/Home.css +97 -0
- package/dist/views/Home.d.ts +14 -0
- package/dist/views/Home.js +192 -0
- package/dist/views/Media.css +65 -0
- package/dist/views/OnboardingGate.d.ts +10 -0
- package/dist/views/OnboardingGate.js +54 -0
- package/dist/views/Phrases.css +45 -0
- package/dist/views/PhrasesView.d.ts +4 -0
- package/dist/views/PhrasesView.js +104 -0
- package/dist/views/ProgressView.d.ts +19 -0
- package/dist/views/ProgressView.js +36 -0
- package/dist/views/Prompt.css +84 -0
- package/dist/views/PromptPanel.d.ts +8 -0
- package/dist/views/PromptPanel.js +169 -0
- package/dist/views/Recipe.css +84 -0
- package/dist/views/RecipeLibrary.d.ts +7 -0
- package/dist/views/RecipeLibrary.js +104 -0
- package/dist/views/Retention.css +49 -0
- package/dist/views/RetentionView.d.ts +7 -0
- package/dist/views/RetentionView.js +145 -0
- package/dist/views/SettingsView.d.ts +9 -0
- package/dist/views/SettingsView.js +235 -0
- package/dist/views/Shorts.css +112 -0
- package/dist/views/ShortsView.d.ts +4 -0
- package/dist/views/ShortsView.js +205 -0
- package/dist/views/SourcesView.d.ts +6 -0
- package/dist/views/SourcesView.js +417 -0
- package/dist/views/TimelineView.d.ts +20 -0
- package/dist/views/TimelineView.js +12 -0
- package/package.json +68 -3
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge, Button, Card, ContextMenu, EmptyState, IconButton, PageShell, cx } from "@rightkit/react-shell/react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { pickFolder } from "../lib/dialog.js";
|
|
5
|
+
import { errorCode, errorMessage, revealPath } from "../lib/engine.js";
|
|
6
|
+
import { SETTINGS_CHANGED, mediaScopeAllow, projectsList, thumbnails, } from "../lib/media.js";
|
|
7
|
+
import { assetUrl } from "../lib/asset.js";
|
|
8
|
+
import { baseName, projectShow } from "../lib/project.js";
|
|
9
|
+
import { createHostSettingsBackend } from "../lib/settingsBackend.js";
|
|
10
|
+
import { ErrorPanel, Thumbnail, timecode, Icon } from "../ui/index.js";
|
|
11
|
+
const recentSettings = createHostSettingsBackend();
|
|
12
|
+
let recentQueue = Promise.resolve();
|
|
13
|
+
const recentDirs = (value) => Array.isArray(value) ? value.filter((dir) => typeof dir === "string").slice(0, 8) : [];
|
|
14
|
+
function recentProjects(update) {
|
|
15
|
+
const run = recentQueue.then(async () => {
|
|
16
|
+
for (let attempt = 0;; attempt += 1) {
|
|
17
|
+
const snapshot = await recentSettings.load();
|
|
18
|
+
let dirs = recentDirs(snapshot.values.recent_projects);
|
|
19
|
+
if (!update)
|
|
20
|
+
return dirs;
|
|
21
|
+
dirs = update(dirs);
|
|
22
|
+
try {
|
|
23
|
+
await recentSettings.patch({ recent_projects: dirs }, snapshot.revision);
|
|
24
|
+
return dirs;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (attempt > 0 || errorCode(error) !== "revision_conflict")
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
recentQueue = run.catch(() => undefined);
|
|
33
|
+
return run;
|
|
34
|
+
}
|
|
35
|
+
export const loadRecent = () => recentProjects();
|
|
36
|
+
export const rememberProject = (dir) => recentProjects((dirs) => [dir, ...dirs.filter((d) => d !== dir)].slice(0, 8));
|
|
37
|
+
export const forgetProject = (dir) => recentProjects((dirs) => dirs.filter((d) => d !== dir));
|
|
38
|
+
const STATUS = {
|
|
39
|
+
finished: { label: "Finished", tone: "ok" },
|
|
40
|
+
needs_review: { label: "Needs review", tone: "accent" },
|
|
41
|
+
media_only: { label: "Media only", tone: "neutral" },
|
|
42
|
+
missing_media: { label: "Missing media", tone: "warn" },
|
|
43
|
+
};
|
|
44
|
+
function editedAgo(edited) {
|
|
45
|
+
if (!edited)
|
|
46
|
+
return "";
|
|
47
|
+
const s = Math.max(0, Date.now() / 1000 - edited);
|
|
48
|
+
if (s < 90)
|
|
49
|
+
return "edited just now";
|
|
50
|
+
if (s < 3600)
|
|
51
|
+
return `edited ${Math.round(s / 60)}m ago`;
|
|
52
|
+
if (s < 86400)
|
|
53
|
+
return `edited ${Math.round(s / 3600)}h ago`;
|
|
54
|
+
return `edited ${Math.round(s / 86400)}d ago`;
|
|
55
|
+
}
|
|
56
|
+
function metaLine(p) {
|
|
57
|
+
const parts = [];
|
|
58
|
+
if (p.duration_ms)
|
|
59
|
+
parts.push(timecode(p.duration_ms));
|
|
60
|
+
if (p.aspect)
|
|
61
|
+
parts.push(p.aspect);
|
|
62
|
+
if (p.sources !== undefined)
|
|
63
|
+
parts.push(`${p.sources} clip${p.sources === 1 ? "" : "s"}`);
|
|
64
|
+
const ago = editedAgo(p.edited);
|
|
65
|
+
if (ago)
|
|
66
|
+
parts.push(ago);
|
|
67
|
+
return parts.join(" · ");
|
|
68
|
+
}
|
|
69
|
+
/** Owner review: stored `project.json` name, then the folder name de-slugged
|
|
70
|
+
* (`finding-your-voice` → `Finding your voice`). */
|
|
71
|
+
function displayName(p) {
|
|
72
|
+
if (p.name)
|
|
73
|
+
return p.name;
|
|
74
|
+
const folder = baseName(p.dir).replace(/\.cutright$/, "");
|
|
75
|
+
const words = folder.replace(/[-_]+/g, " ").trim();
|
|
76
|
+
return words.length > 0 ? words[0].toUpperCase() + words.slice(1) : folder;
|
|
77
|
+
}
|
|
78
|
+
function ProjectCardView({ card, onOpen, onRemove, }) {
|
|
79
|
+
const [thumb, setThumb] = useState();
|
|
80
|
+
const [menu, setMenu] = useState(false);
|
|
81
|
+
const menuRef = useRef(null);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
let live = true;
|
|
84
|
+
setThumb(undefined);
|
|
85
|
+
const t = card.thumb;
|
|
86
|
+
if (!t)
|
|
87
|
+
return;
|
|
88
|
+
const target = t.render ? { render: t.render } : t.source ? { source: t.source } : null;
|
|
89
|
+
if (!target)
|
|
90
|
+
return;
|
|
91
|
+
// Render targets have no engine-side duration; frame the render at 20% of
|
|
92
|
+
// the card duration (per owner review), sources default to 20% inside.
|
|
93
|
+
const at_ms = t.render && card.duration_ms ? Math.round(card.duration_ms / 5) : undefined;
|
|
94
|
+
thumbnails(card.dir, target, { width: 480, ...(at_ms ? { at_ms } : {}) })
|
|
95
|
+
.then((r) => {
|
|
96
|
+
if (live && r.frames[0])
|
|
97
|
+
setThumb(assetUrl(r.frames[0].path));
|
|
98
|
+
})
|
|
99
|
+
.catch(() => undefined);
|
|
100
|
+
return () => {
|
|
101
|
+
live = false;
|
|
102
|
+
};
|
|
103
|
+
}, [card]);
|
|
104
|
+
const st = STATUS[card.status ?? ""] ?? (card.error ? STATUS.missing_media : STATUS.media_only);
|
|
105
|
+
const name = displayName(card);
|
|
106
|
+
return (_jsxs("div", { className: "Home-cardWrap", children: [_jsxs(Card, { flush: true, className: "Home-card", onClick: () => onOpen(card), label: `Open ${name}`, children: [_jsx(Thumbnail, { src: thumb, badge: card.duration_ms ? timecode(card.duration_ms) : undefined }), _jsxs("div", { className: "Home-cardBody", children: [_jsx("div", { className: "Home-cardName", children: name }), _jsx("div", { className: "Home-cardMeta", title: card.dir, children: metaLine(card) }), _jsx(Badge, { tone: st.tone, children: st.label })] })] }), _jsxs("div", { className: cx("Home-cardMenu", menu && "Home-cardMenu--open"), ref: menuRef, children: [_jsx(IconButton, { label: "Project actions", onClick: () => setMenu((m) => !m), children: _jsx(Icon, { name: "more-horizontal" }) }), menu && menuRef.current && (_jsx(ContextMenu, { anchor: menuRef.current, placement: "bottom-end", label: "Project actions", itemAs: "button", keyboard: "escape-only", autoFocus: false, showLead: false, onClose: () => setMenu(false), items: [
|
|
107
|
+
{ id: "reveal", label: "Reveal in Finder", run: () => void revealPath(card.dir).catch(() => undefined) },
|
|
108
|
+
{ id: "remove", label: "Remove from list", run: () => onRemove(card.dir) },
|
|
109
|
+
] }))] })] }));
|
|
110
|
+
}
|
|
111
|
+
export function Home({ ready, dropActive, onOpen, onCreate, onOpenFolder, onImportBundle, onFiles, }) {
|
|
112
|
+
const [cards, setCards] = useState(null);
|
|
113
|
+
const [error, setError] = useState(null);
|
|
114
|
+
const [broken, setBroken] = useState(null);
|
|
115
|
+
const [importError, setImportError] = useState(null);
|
|
116
|
+
const load = useCallback(async () => {
|
|
117
|
+
try {
|
|
118
|
+
setError(null);
|
|
119
|
+
const recents = await loadRecent();
|
|
120
|
+
if (recents.length === 0) {
|
|
121
|
+
setCards([]);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const r = await projectsList(recents);
|
|
125
|
+
const byDir = new Map(r.projects.map((p) => [p.dir, p]));
|
|
126
|
+
// Recents the engine did not return (folder deleted) stay as warn cards.
|
|
127
|
+
const merged = recents.map((dir) => byDir.get(dir) ??
|
|
128
|
+
{ dir, name: baseName(dir), status: "missing_media", error: "Folder not found" });
|
|
129
|
+
// Plus anything else discovered under the recent roots (subfolders).
|
|
130
|
+
for (const p of r.projects)
|
|
131
|
+
if (!merged.some((m) => m.dir === p.dir))
|
|
132
|
+
merged.push(p);
|
|
133
|
+
// Card thumbnails stream through the asset protocol — grant scope
|
|
134
|
+
// before the cards mount so their <img> requests aren't refused.
|
|
135
|
+
await mediaScopeAllow(merged.filter((c) => !c.error).map((c) => c.dir), true).catch(() => undefined);
|
|
136
|
+
setCards(merged);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
setError(errorMessage(e));
|
|
140
|
+
setCards([]);
|
|
141
|
+
}
|
|
142
|
+
}, []);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!ready)
|
|
145
|
+
return;
|
|
146
|
+
void load();
|
|
147
|
+
const changed = () => void load();
|
|
148
|
+
window.addEventListener(SETTINGS_CHANGED, changed);
|
|
149
|
+
return () => window.removeEventListener(SETTINGS_CHANGED, changed);
|
|
150
|
+
}, [load, ready]);
|
|
151
|
+
const openCard = (card) => {
|
|
152
|
+
setBroken(null);
|
|
153
|
+
if (card.error || card.status === "missing_media") {
|
|
154
|
+
void projectShow(card.dir)
|
|
155
|
+
.then(() => onOpen(card.dir))
|
|
156
|
+
.catch(() => setBroken(card));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
onOpen(card.dir);
|
|
160
|
+
};
|
|
161
|
+
const locate = async () => {
|
|
162
|
+
const dir = await pickFolder("Locate the project folder");
|
|
163
|
+
if (!dir)
|
|
164
|
+
return;
|
|
165
|
+
void projectShow(dir)
|
|
166
|
+
.then(async () => {
|
|
167
|
+
await rememberProject(dir);
|
|
168
|
+
setBroken(null);
|
|
169
|
+
onOpen(dir);
|
|
170
|
+
})
|
|
171
|
+
.catch((e) => setError(errorMessage(e)));
|
|
172
|
+
};
|
|
173
|
+
const remove = (dir) => {
|
|
174
|
+
void forgetProject(dir).then(() => {
|
|
175
|
+
if (broken?.dir === dir)
|
|
176
|
+
setBroken(null);
|
|
177
|
+
return load();
|
|
178
|
+
}).catch((e) => setError(errorMessage(e)));
|
|
179
|
+
};
|
|
180
|
+
const pickFiles = async () => {
|
|
181
|
+
const { pickMedia } = await import("../lib/dialog.js");
|
|
182
|
+
const files = await pickMedia();
|
|
183
|
+
if (files.length > 0)
|
|
184
|
+
onFiles(files);
|
|
185
|
+
};
|
|
186
|
+
const importBundle = () => {
|
|
187
|
+
setImportError(null);
|
|
188
|
+
void onImportBundle().then((message) => setImportError(message));
|
|
189
|
+
};
|
|
190
|
+
const grid = (cards?.length ?? 0) > 0;
|
|
191
|
+
return (_jsx(PageShell, { title: "Projects", actions: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", onClick: importBundle, disabled: !ready, children: "Import bundle\u2026" }), _jsxs(Button, { variant: "secondary", onClick: onOpenFolder, disabled: !ready, children: [_jsx(Icon, { name: "folder-open" }), "Open folder\u2026"] }), _jsxs(Button, { variant: "primary", onClick: onCreate, disabled: !ready, children: [_jsx(Icon, { name: "plus" }), "New project"] })] }), maxWidth: 0, children: _jsxs("div", { className: cx("Home-body", dropActive && "Home-body--drop"), children: [!ready && _jsx("p", { className: "Home-dim", children: "Waiting for the engine\u2026" }), error && _jsx(ErrorPanel, { title: "Something went wrong", detail: error }), importError && _jsx(ErrorPanel, { title: "Couldn't import the bundle", message: importError }), broken && (_jsx(ErrorPanel, { title: `Can't open ${broken.name ?? baseName(broken.dir)}`, detail: broken.error ?? "The project folder or media is missing.", actions: _jsx(Button, { onClick: () => void locate(), children: "Locate folder\u2026" }) })), !grid && cards !== null && (_jsx("div", { className: "Home-dropzone", children: _jsx(EmptyState, { title: "Drop footage to start", action: _jsx(Button, { variant: "primary", onClick: () => void pickFiles(), disabled: !ready, children: "Choose files\u2026" }), icon: _jsx(Icon, { name: "clapperboard", width: 28, height: 28 }), children: "Talking head, screen recordings or any clips. Originals are never modified." }) })), grid && (_jsx("div", { className: "Home-grid", children: cards.map((c) => (_jsx(ProjectCardView, { card: c, onOpen: openCard, onRemove: remove }, c.dir))) }))] }) }));
|
|
192
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/* Media screen — docs/design/app-ui.md §3.2. Tokens only. */
|
|
2
|
+
|
|
3
|
+
.media-grid {
|
|
4
|
+
display: grid;
|
|
5
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
6
|
+
gap: var(--s4);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.media-card { display: flex; flex-direction: column; gap: var(--s2); }
|
|
10
|
+
.media-card__name {
|
|
11
|
+
font: 500 13px/1.3 var(--rk-font-body);
|
|
12
|
+
color: var(--rk-ink);
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
text-overflow: ellipsis;
|
|
15
|
+
white-space: nowrap;
|
|
16
|
+
}
|
|
17
|
+
.media-card__meta {
|
|
18
|
+
font-size: 12px;
|
|
19
|
+
color: var(--rk-ink-2);
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
text-overflow: ellipsis;
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
}
|
|
24
|
+
.media-card__chips { display: flex; flex-wrap: nowrap; gap: var(--s1); overflow: hidden; }
|
|
25
|
+
.media-card__missing {
|
|
26
|
+
position: absolute;
|
|
27
|
+
inset: 0;
|
|
28
|
+
display: grid;
|
|
29
|
+
place-items: center;
|
|
30
|
+
color: var(--rk-warn);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.media-drop {
|
|
34
|
+
position: fixed;
|
|
35
|
+
inset: var(--s4);
|
|
36
|
+
z-index: 30;
|
|
37
|
+
display: grid;
|
|
38
|
+
place-items: center;
|
|
39
|
+
border: 2px dashed var(--rk-accent);
|
|
40
|
+
border-radius: var(--r-xl);
|
|
41
|
+
background: color-mix(in srgb, var(--rk-plane) 82%, transparent);
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
.media-drop span {
|
|
45
|
+
font: 600 15px/1 var(--rk-font-body);
|
|
46
|
+
color: var(--rk-accent);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.media-meta {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: auto 1fr;
|
|
52
|
+
gap: var(--s1) var(--s4);
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
}
|
|
55
|
+
.media-meta dt { color: var(--rk-ink-2); margin: 0; }
|
|
56
|
+
.media-meta dd { margin: 0; color: var(--rk-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
|
+
.media-meta dd.mono { font-family: var(--rk-font-mono); font-size: 11px; }
|
|
58
|
+
|
|
59
|
+
.media-excerpt {
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
line-height: 1.65;
|
|
62
|
+
color: var(--rk-ink-2);
|
|
63
|
+
max-height: 120px;
|
|
64
|
+
overflow: auto;
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* First-run gate: @rightkit/onboarding owns the legal agreement (documents expand inline
|
|
4
|
+
* in the shared gate) and the model step. The workspace mounts only after the current legal
|
|
5
|
+
* documents are accepted and setup is completed or skipped; a changed document re-prompts.
|
|
6
|
+
* A broken settings store never wedges the app.
|
|
7
|
+
*/
|
|
8
|
+
export declare function OnboardingGate({ children }: {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Onboarding, isLegalAccepted, onboardingStateKey } from "@rightkit/onboarding";
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { LEGAL_MATERIAL_SHA256, REQUIRED_LEGAL_ACCEPTANCE_VERSION, legalStepConfig } from "../legal/legalConfig.js";
|
|
5
|
+
import { createModelsBackend } from "../lib/modelsBackend.js";
|
|
6
|
+
import { createHostSettingsBackend } from "../lib/settingsBackend.js";
|
|
7
|
+
/**
|
|
8
|
+
* First-run gate: @rightkit/onboarding owns the legal agreement (documents expand inline
|
|
9
|
+
* in the shared gate) and the model step. The workspace mounts only after the current legal
|
|
10
|
+
* documents are accepted and setup is completed or skipped; a changed document re-prompts.
|
|
11
|
+
* A broken settings store never wedges the app.
|
|
12
|
+
*/
|
|
13
|
+
export function OnboardingGate({ children }) {
|
|
14
|
+
const [phase, setPhase] = useState("loading");
|
|
15
|
+
const settings = useMemo(() => createHostSettingsBackend(), []);
|
|
16
|
+
const models = useMemo(() => createModelsBackend(), []);
|
|
17
|
+
const legal = useMemo(() => legalStepConfig(), []);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let live = true;
|
|
20
|
+
// One failed read must not send a set-up user back through first-run setup, so the
|
|
21
|
+
// read is tried again before the gate gives up and shows setup.
|
|
22
|
+
void (async () => {
|
|
23
|
+
for (let attempt = 0;; attempt += 1) {
|
|
24
|
+
try {
|
|
25
|
+
const snap = await settings.load();
|
|
26
|
+
if (!live)
|
|
27
|
+
return;
|
|
28
|
+
const st = snap.values[onboardingStateKey()];
|
|
29
|
+
const done = st && isLegalAccepted(st, REQUIRED_LEGAL_ACCEPTANCE_VERSION, undefined, LEGAL_MATERIAL_SHA256)
|
|
30
|
+
&& (st.skipped === true || (st.completed ?? []).includes("models") || (st.skippedSteps ?? []).includes("models"));
|
|
31
|
+
setPhase(done ? "done" : "setup");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
if (!live)
|
|
36
|
+
return;
|
|
37
|
+
if (attempt === 2) {
|
|
38
|
+
setPhase("setup");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
await new Promise((next) => setTimeout(next, 1000));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
return () => {
|
|
46
|
+
live = false;
|
|
47
|
+
};
|
|
48
|
+
}, [settings]);
|
|
49
|
+
if (phase === "loading")
|
|
50
|
+
return null;
|
|
51
|
+
if (phase === "done")
|
|
52
|
+
return _jsx(_Fragment, { children: children });
|
|
53
|
+
return (_jsx(Onboarding, { settings: settings, legal: legal, models: models, requiredVersion: REQUIRED_LEGAL_ACCEPTANCE_VERSION, requiredDocumentsHash: LEGAL_MATERIAL_SHA256, documentsHash: LEGAL_MATERIAL_SHA256, onComplete: () => setPhase("done"), onSkip: () => setPhase("done") }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Find phrases — docs/design/app-ui.md §3.6 */
|
|
2
|
+
|
|
3
|
+
.phrases-body { display: flex; flex-direction: column; gap: var(--s4); }
|
|
4
|
+
|
|
5
|
+
.phrases-controls {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: flex-end;
|
|
8
|
+
gap: var(--s3);
|
|
9
|
+
}
|
|
10
|
+
.phrases-controls .rk-field:first-child { flex: 1; }
|
|
11
|
+
.phrases-controls .rk-field:nth-child(2) { width: 200px; flex: none; }
|
|
12
|
+
|
|
13
|
+
.phrases-results { display: flex; flex-direction: column; gap: var(--s2); }
|
|
14
|
+
.phrases-row {
|
|
15
|
+
background: var(--rk-elevated);
|
|
16
|
+
border: 1px solid var(--rk-line);
|
|
17
|
+
border-radius: var(--r-md);
|
|
18
|
+
padding: var(--s3) var(--s4);
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: var(--s2);
|
|
22
|
+
}
|
|
23
|
+
.phrases-row__top { display: flex; align-items: center; gap: var(--s2); }
|
|
24
|
+
.phrases-row__top > .cr-mono { font-size: 11px; }
|
|
25
|
+
.phrases-row__spacer { flex: 1; }
|
|
26
|
+
.phrases-row__ctx { margin: 0; font-size: 13px; line-height: 1.5; }
|
|
27
|
+
.phrases-mark { background: var(--rk-accent-soft); color: var(--rk-ink); border-radius: 2px; padding: 0 1px; }
|
|
28
|
+
|
|
29
|
+
.phrases-pop {
|
|
30
|
+
margin-top: var(--s2);
|
|
31
|
+
background: var(--rk-elevated);
|
|
32
|
+
border: 1px solid var(--rk-line);
|
|
33
|
+
border-radius: var(--r-md);
|
|
34
|
+
box-shadow: var(--rk-pop);
|
|
35
|
+
padding: var(--s2);
|
|
36
|
+
max-width: 420px;
|
|
37
|
+
}
|
|
38
|
+
.phrases-pop__head { display: flex; align-items: center; justify-content: space-between; }
|
|
39
|
+
.phrases-pop video {
|
|
40
|
+
width: 100%;
|
|
41
|
+
max-height: 240px;
|
|
42
|
+
background: var(--cr-stage);
|
|
43
|
+
border-radius: var(--r-sm);
|
|
44
|
+
display: block;
|
|
45
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Find phrases — docs/design/app-ui.md §3.6.
|
|
3
|
+
import { Badge, Button, EmptyState, Field, IconButton, Input, PageShell, SearchIcon, useShortcuts, useToast } from "@rightkit/react-shell/react";
|
|
4
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { errorMessage } from "../lib/engine.js";
|
|
6
|
+
import { clipExport, phraseSearch, sourceList } from "../lib/media.js";
|
|
7
|
+
import { appendClip } from "../lib/project.js";
|
|
8
|
+
import { RangePreview } from "../components/RangePreview.js";
|
|
9
|
+
import { ErrorPanel, timecode, Icon } from "../ui/index.js";
|
|
10
|
+
/** Inline preview popover: plays the match's context range on the real file. */
|
|
11
|
+
function PreviewPop({ path, startMs, endMs, onClose }) {
|
|
12
|
+
return (_jsxs("div", { className: "phrases-pop", children: [_jsxs("div", { className: "phrases-pop__head", children: [_jsx("span", { className: "cr-overline", children: "Preview" }), _jsx(IconButton, { label: "Close preview", onClick: onClose, children: _jsx(Icon, { name: "x" }) })] }), _jsx(RangePreview, { path: path, startMs: startMs, endMs: endMs })] }));
|
|
13
|
+
}
|
|
14
|
+
/** Highlight the query inside the context sentence (`--accent-soft` mark). */
|
|
15
|
+
function Context({ text, query }) {
|
|
16
|
+
const i = text.toLowerCase().indexOf(query.toLowerCase());
|
|
17
|
+
if (i < 0 || !query.trim())
|
|
18
|
+
return _jsx("span", { children: text });
|
|
19
|
+
return (_jsxs("span", { children: [text.slice(0, i), _jsx("mark", { className: "phrases-mark", children: text.slice(i, i + query.length) }), text.slice(i + query.length)] }));
|
|
20
|
+
}
|
|
21
|
+
export function PhrasesView({ dir, onChanged }) {
|
|
22
|
+
const toast = useToast();
|
|
23
|
+
const [sources, setSources] = useState([]);
|
|
24
|
+
const [filter, setFilter] = useState("");
|
|
25
|
+
const [query, setQuery] = useState("");
|
|
26
|
+
const [matches, setMatches] = useState(null);
|
|
27
|
+
const [busy, setBusy] = useState(null);
|
|
28
|
+
const [error, setError] = useState(null);
|
|
29
|
+
const [preview, setPreview] = useState(null);
|
|
30
|
+
const inputRef = useRef(null);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
sourceList(dir)
|
|
33
|
+
.then((s) => setSources(s.sources))
|
|
34
|
+
.catch((e) => setError(errorMessage(e)));
|
|
35
|
+
}, [dir]);
|
|
36
|
+
// ⌘F focuses the search field.
|
|
37
|
+
useShortcuts([
|
|
38
|
+
{
|
|
39
|
+
id: "cr.phrases.search",
|
|
40
|
+
chord: "mod+f",
|
|
41
|
+
label: "Search phrases",
|
|
42
|
+
group: "Find phrases",
|
|
43
|
+
run: () => {
|
|
44
|
+
inputRef.current?.focus();
|
|
45
|
+
inputRef.current?.select();
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
const search = useCallback(async () => {
|
|
50
|
+
if (!query.trim())
|
|
51
|
+
return;
|
|
52
|
+
setBusy("search");
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
const r = await phraseSearch(dir, query.trim());
|
|
56
|
+
setMatches(r.matches);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
setError(errorMessage(e));
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
setBusy(null);
|
|
63
|
+
}
|
|
64
|
+
}, [dir, query]);
|
|
65
|
+
const insert = async (m) => {
|
|
66
|
+
setBusy(`${m.source}:${m.word_index}`);
|
|
67
|
+
try {
|
|
68
|
+
await appendClip(dir, m.source, m.context.start_ms, m.context.end_ms);
|
|
69
|
+
toast("Added to the timeline", { kind: "success" });
|
|
70
|
+
await onChanged();
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
toast(errorMessage(e), { kind: "error", ttl: 0 });
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
setBusy(null);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const exportClip = async (m) => {
|
|
80
|
+
setBusy(`export-${m.source}:${m.word_index}`);
|
|
81
|
+
try {
|
|
82
|
+
const r = await clipExport(dir, {
|
|
83
|
+
source: m.source,
|
|
84
|
+
start_ms: m.context.start_ms,
|
|
85
|
+
end_ms: m.context.end_ms,
|
|
86
|
+
});
|
|
87
|
+
toast(`Exported ${r.name} — find it under Exports`, { kind: "success" });
|
|
88
|
+
await onChanged();
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
toast(errorMessage(e), { kind: "error", ttl: 0 });
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
setBusy(null);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const srcById = new Map(sources.map((s) => [s.id, s]));
|
|
98
|
+
const shown = (matches ?? []).filter((m) => !filter || m.source === filter);
|
|
99
|
+
return (_jsx(PageShell, { title: "Find phrases", maxWidth: 0, subtitle: "Search spoken words across this project's transcripts", children: _jsxs("div", { className: "phrases-body", children: [_jsxs("div", { className: "phrases-controls", children: [_jsx(Field, { label: "Search", note: "Local search — indexes your own footage only.", children: (ids) => (_jsx(Input, { ...ids, ref: inputRef, placeholder: "Find spoken words\u2026", value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => e.key === "Enter" && void search() })) }), _jsx(Field, { label: "Source", children: (ids) => (_jsxs("select", { ...ids, className: "rk-input cr-select", value: filter, onChange: (e) => setFilter(e.target.value), children: [_jsx("option", { value: "", children: "All sources" }), sources.map((s) => (_jsx("option", { value: s.id, children: s.name }, s.id)))] })) }), _jsxs(Button, { variant: "primary", onClick: () => void search(), disabled: (!query.trim()) || (busy === "search"), "aria-busy": (busy === "search") || undefined, children: [!(busy === "search") && _jsx("span", { className: "cr-glyph", children: _jsx(SearchIcon, {}) }), busy === "search" ? "Searching…" : "Search"] })] }), error && _jsx(ErrorPanel, { title: "Search failed", detail: error }), matches !== null && shown.length === 0 && (_jsx(EmptyState, { title: "No matches", icon: _jsx("span", { className: "cr-glyph cr-glyph--lg", children: _jsx(SearchIcon, {}) }), children: "Nothing in this project's transcripts matches that phrase." })), matches === null && !error && (_jsx(EmptyState, { title: "Find what's said", icon: _jsx(Icon, { name: "text-quote", width: 28, height: 28 }), children: "Search every transcript for a phrase, then preview or drop it on the timeline." })), _jsx("div", { className: "phrases-results", children: shown.map((m, i) => {
|
|
100
|
+
const src = srcById.get(m.source);
|
|
101
|
+
const key = `${m.source}:${m.word_index}:${i}`;
|
|
102
|
+
return (_jsxs("div", { className: "phrases-row", children: [_jsxs("div", { className: "phrases-row__top", children: [_jsx("span", { className: "cr-mono cr-muted", children: timecode(m.start_ms) }), _jsx(Badge, { children: src?.name ?? m.source }), _jsx("span", { className: "phrases-row__spacer" }), src?.path ? (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => setPreview(preview === m ? null : m), children: "Preview" })) : null, _jsx(Button, { size: "sm", variant: "ghost", title: "Render this phrase range to Exports", onClick: () => void exportClip(m), disabled: (busy === `export-${m.source}:${m.word_index}`), "aria-busy": (busy === `export-${m.source}:${m.word_index}`) || undefined, children: busy === `export-${m.source}:${m.word_index}` ? "Exporting…" : "Export clip" }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => void insert(m), disabled: (busy === `${m.source}:${m.word_index}`), "aria-busy": (busy === `${m.source}:${m.word_index}`) || undefined, children: busy === `${m.source}:${m.word_index}` ? "Adding…" : "Add to timeline" })] }), _jsxs("p", { className: "phrases-row__ctx cr-muted", children: ["\u2026", _jsx(Context, { text: m.context.text, query: query }), "\u2026"] }), preview === m && src?.path && (_jsx(PreviewPop, { path: src.path, startMs: m.context.start_ms, endMs: m.context.end_ms, onClose: () => setPreview(null) }))] }, key));
|
|
103
|
+
}) })] }) }));
|
|
104
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ProgressState } from "../hooks/useProgress.js";
|
|
2
|
+
/** Inline render bar kept for screens that embed it (Finished edit). */
|
|
3
|
+
export declare function RenderBar({ progress }: {
|
|
4
|
+
progress: ProgressState;
|
|
5
|
+
}): import("react").JSX.Element | null;
|
|
6
|
+
export declare function ProgressView({ progress, jobError, onCancel, assembling, resumable, renderMissing, recovered, onResume, }: {
|
|
7
|
+
progress: ProgressState;
|
|
8
|
+
jobError: string | null;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
assembling: boolean;
|
|
11
|
+
/** An unfinished assemble exists for this project (engine says running/failed/cancelled). */
|
|
12
|
+
resumable: boolean;
|
|
13
|
+
/** The recovered run's stored render file is gone; it must run again. */
|
|
14
|
+
renderMissing?: boolean;
|
|
15
|
+
/** Labels of the recovered assemble's jobs (shot prompts) when the engine
|
|
16
|
+
* has no seeded request rows to show. */
|
|
17
|
+
recovered?: string[];
|
|
18
|
+
onResume: () => void;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge, Button, Card, EmptyState, PageShell } from "@rightkit/react-shell/react";
|
|
3
|
+
import { ErrorPanel, ProgressRow, timecode, Icon } from "../ui/index.js";
|
|
4
|
+
import { hostAppName } from "../appName.js";
|
|
5
|
+
function elapsedOf(progress) {
|
|
6
|
+
// Frames don't carry start times; show the render's out-time as elapsed.
|
|
7
|
+
const r = progress.render;
|
|
8
|
+
if (r?.out_time_ms !== undefined)
|
|
9
|
+
return timecode(r.out_time_ms);
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Inline render bar kept for screens that embed it (Finished edit). */
|
|
13
|
+
export function RenderBar({ progress }) {
|
|
14
|
+
const r = progress.render;
|
|
15
|
+
if (!r)
|
|
16
|
+
return null;
|
|
17
|
+
const pct = r.total_ms && r.out_time_ms !== undefined
|
|
18
|
+
? Math.max(0, Math.min(1, r.out_time_ms / r.total_ms))
|
|
19
|
+
: undefined;
|
|
20
|
+
return (_jsx(ProgressRow, { label: "Render", status: pct === undefined ? "Rendering" : `${Math.round(pct * 100)}%`, tone: "info", fraction: pct, elapsed: r.frame !== undefined ? `frame ${r.frame}` : undefined }));
|
|
21
|
+
}
|
|
22
|
+
export function ProgressView({ progress, jobError, onCancel, assembling, resumable, renderMissing, recovered, onResume, }) {
|
|
23
|
+
const jobs = Object.values(progress.jobs);
|
|
24
|
+
const error = progress.error?.message ?? jobError;
|
|
25
|
+
const running = progress.running || assembling;
|
|
26
|
+
// Recovered run: resumable and not currently executing (owner review: show
|
|
27
|
+
// recovered jobs as ProgressRows with a `Recovered` chip, never "Nothing
|
|
28
|
+
// running" + a floating Resume).
|
|
29
|
+
const showRecovered = resumable && !running && !progress.done;
|
|
30
|
+
const recoveredLabels = jobs.length > 0 ? jobs.map((j) => j.label) : (recovered ?? []);
|
|
31
|
+
const idle = !running && !showRecovered && jobs.length === 0 && !progress.render && !error && !progress.done && !progress.cancelled;
|
|
32
|
+
const renderPct = progress.render?.total_ms && progress.render.out_time_ms !== undefined
|
|
33
|
+
? Math.max(0, Math.min(1, progress.render.out_time_ms / progress.render.total_ms))
|
|
34
|
+
: undefined;
|
|
35
|
+
return (_jsx(PageShell, { title: "Progress", actions: _jsxs(_Fragment, { children: [running && (_jsx(Button, { variant: "secondary", onClick: onCancel, children: progress.render ? "Cancel render" : "Cancel assembly" })), showRecovered && (_jsxs(Button, { variant: "secondary", onClick: onResume, children: [_jsx(Icon, { name: "rotate-ccw" }), "Resume"] }))] }), maxWidth: 0, subtitle: running ? "Generation running" : progress.done ? "Finished" : showRecovered ? "Recovered job" : undefined, children: _jsxs("div", { className: "shell-stack", children: [idle && (_jsx(EmptyState, { title: "Nothing running", icon: _jsx(Icon, { name: "check", width: 28, height: 28 }), children: "Generation and render jobs show up here while they run." })), showRecovered && (_jsx(Card, { children: recoveredLabels.length === 0 ? (_jsx(ProgressRow, { label: "Assembly", status: "Recovered", tone: "info", fraction: null })) : (recoveredLabels.map((label, i) => (_jsx(ProgressRow, { label: label, status: "Recovered", tone: "info", fraction: null }, i)))) })), renderMissing && !running && (_jsx(ErrorPanel, { title: "This render is missing", detail: "The export file no longer exists. Resume to render it again." })), error && (_jsx(ErrorPanel, { title: "The job failed", detail: error, actions: resumable && !running ? (_jsxs(Button, { variant: "secondary", onClick: onResume, children: [_jsx(Icon, { name: "rotate-ccw" }), "Resume"] })) : undefined })), progress.done && (_jsx("div", { children: _jsx(Badge, { tone: "ok", children: "Assembly complete" }) })), progress.cancelled && (_jsxs("p", { className: "m-0 text-[13px]", style: { color: "var(--rk-ink-2)" }, children: ["Cancelled. Finished ", hostAppName(), " jobs are kept; Resume continues with the same assemble id and does not submit them again."] })), progress.resumed && (_jsx("p", { className: "m-0 text-[12px]", style: { color: "var(--rk-ink-2)" }, children: "Resumed an earlier run: existing jobs are reused." })), progress.stages.length > 0 && (_jsx(Card, { children: progress.stages.map((s, i) => (_jsx(ProgressRow, { label: s, status: i === progress.stages.length - 1 && running ? "Running" : "Done", tone: i === progress.stages.length - 1 && running ? "info" : "ok", fraction: i === progress.stages.length - 1 && running ? undefined : null }, s))) })), jobs.length > 0 && !showRecovered && (_jsx(Card, { children: jobs.map((j) => (_jsx(ProgressRow, { label: j.label, status: j.status ?? j.request ?? "queued", tone: j.status === "failed" ? "danger" : j.imported ? "ok" : "info", elapsed: j.stage ?? undefined, error: j.status === "failed" ? (j.message ?? "failed") : undefined }, j.id))) })), progress.render && (_jsx(Card, { children: _jsx(ProgressRow, { label: "Render", status: renderPct === undefined ? "Rendering" : `${Math.round(renderPct * 100)}%`, tone: "info", fraction: renderPct, elapsed: elapsedOf(progress) }) })), progress.timeline && (_jsxs("p", { className: "m-0 text-[12px]", style: { color: "var(--rk-ink-2)" }, children: ["Timeline built: ", progress.timeline.video_clips ?? 0, " clips,", " ", progress.timeline.titles ?? 0, " titles"] })), progress.imports > 0 && (_jsxs("p", { className: "m-0 text-[13px]", "aria-live": "polite", children: ["Imported ", progress.imports, " asset", progress.imports === 1 ? "" : "s"] }))] }) }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/* From prompt — app-ui.md §3.8 */
|
|
2
|
+
|
|
3
|
+
.Prompt {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: var(--s4);
|
|
7
|
+
max-width: 720px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.Prompt .rk-card {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: var(--s3);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* Header controls sit left-aligned after the title (owner review). */
|
|
17
|
+
.Prompt-cardHead {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: var(--s4);
|
|
21
|
+
}
|
|
22
|
+
.Prompt-cardHead .rk-seg { margin-left: var(--s2); }
|
|
23
|
+
.Prompt-cardHead > :last-child:not(h2):not(.rk-seg) { margin-left: auto; }
|
|
24
|
+
.Prompt-cardHead h2 {
|
|
25
|
+
margin: 0;
|
|
26
|
+
font: 600 15px/1.3 var(--rk-font-body);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.Prompt-grRow {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: var(--s2);
|
|
33
|
+
}
|
|
34
|
+
.Prompt-label {
|
|
35
|
+
font: 500 11px/1 var(--rk-font-body);
|
|
36
|
+
text-transform: uppercase;
|
|
37
|
+
letter-spacing: 0.08em;
|
|
38
|
+
color: var(--rk-ink-2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.Prompt-error {
|
|
42
|
+
margin: 0;
|
|
43
|
+
font-size: 13px;
|
|
44
|
+
color: var(--rk-warn);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* One card of shot rows divided by 1px lines (owner review: no nested cards). */
|
|
48
|
+
.Prompt-shotRow {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: column;
|
|
51
|
+
gap: var(--s2);
|
|
52
|
+
padding: var(--s3) 0;
|
|
53
|
+
}
|
|
54
|
+
.Prompt-shotRow + .Prompt-shotRow { border-top: 1px solid var(--rk-line); }
|
|
55
|
+
|
|
56
|
+
/* kind | provider | model | duration — the model column stays wide enough
|
|
57
|
+
that model names don't truncate. */
|
|
58
|
+
.Prompt-shotGrid {
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-template-columns: 1fr 1fr 2fr 120px;
|
|
61
|
+
gap: var(--s2);
|
|
62
|
+
align-items: end;
|
|
63
|
+
}
|
|
64
|
+
.Prompt-shotGrid .rk-field { min-width: 0; }
|
|
65
|
+
.Prompt-shotGrid .rk-input, .Prompt-shotGrid .rk-input { width: 100%; }
|
|
66
|
+
|
|
67
|
+
.Prompt-shotGrid--auto { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
|
|
68
|
+
|
|
69
|
+
.Prompt-titleRow {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-wrap: wrap;
|
|
72
|
+
align-items: end;
|
|
73
|
+
gap: var(--s2);
|
|
74
|
+
}
|
|
75
|
+
.Prompt-titleRow .rk-field { flex: 1; min-width: 7rem; }
|
|
76
|
+
|
|
77
|
+
.Prompt-actions {
|
|
78
|
+
display: flex;
|
|
79
|
+
gap: var(--s2);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.Prompt-confirm { border-color: var(--rk-accent); }
|
|
83
|
+
.Prompt-confirmInner { display: flex; flex-direction: column; gap: var(--s3); font-size: 13px; }
|
|
84
|
+
.Prompt-confirmInner p { margin: 0; }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProgressState } from "../hooks/useProgress.js";
|
|
2
|
+
import type { AssembleForm } from "../lib/assemble.js";
|
|
3
|
+
export declare function PromptPanel({ busy, progress, onSubmit, onOpenFinished, }: {
|
|
4
|
+
busy: boolean;
|
|
5
|
+
progress?: ProgressState;
|
|
6
|
+
onSubmit: (form: AssembleForm) => void;
|
|
7
|
+
onOpenFinished?: () => void;
|
|
8
|
+
}): import("react").JSX.Element;
|