@liqvid/studio 1.0.0-alpha.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 +9 -0
- package/dist/esm/LiqvidDevToolsProvider.js +49 -0
- package/dist/esm/api/contract.mjs +48 -0
- package/dist/esm/api/project-meta.mjs +33 -0
- package/dist/esm/api/recording.mjs +156 -0
- package/dist/esm/api/root.mjs +4 -0
- package/dist/esm/api/static-file.mjs +82 -0
- package/dist/esm/api/types.mjs +1 -0
- package/dist/esm/client.mjs +98 -0
- package/dist/esm/conventions.mjs +3 -0
- package/dist/esm/index.mjs +20 -0
- package/dist/esm/initialize.mjs +50 -0
- package/dist/esm/jobs/watch-assets.mjs +99 -0
- package/dist/esm/jobs/watch-project-files.mjs +216 -0
- package/dist/esm/next/api.mjs +48 -0
- package/dist/esm/next/page.js +15 -0
- package/dist/esm/pages/NewProjectButton.js +62 -0
- package/dist/esm/pages/RebuildButton.js +24 -0
- package/dist/esm/pages/root-actions.js +151 -0
- package/dist/esm/pages/root.js +25 -0
- package/dist/esm/pages/root.module.css +326 -0
- package/dist/esm/palette.css +279 -0
- package/dist/esm/providers/hosting/github-pages.mjs +10 -0
- package/dist/esm/providers/hosting/liqvid-studio.mjs +8 -0
- package/dist/esm/providers/hosting/s3.mjs +9 -0
- package/dist/esm/providers/hosting/sftp.mjs +22 -0
- package/dist/esm/providers/index.mjs +10 -0
- package/dist/esm/providers/social/bluesky.mjs +8 -0
- package/dist/esm/providers/social/facebook.mjs +8 -0
- package/dist/esm/providers/social/instagram.mjs +8 -0
- package/dist/esm/providers/social/twitter.mjs +7 -0
- package/dist/esm/providers/social/youtube.mjs +7 -0
- package/dist/esm/providers/types.mjs +1 -0
- package/dist/esm/publish.mjs +37 -0
- package/dist/esm/recording/RecordingControl.js +110 -0
- package/dist/esm/recording/RecordingControl.module.css +0 -0
- package/dist/esm/recording/RecordingDialog.js +114 -0
- package/dist/esm/recording/RecordingDialog.module.css +194 -0
- package/dist/esm/schemas/liqvid-config.mjs +32 -0
- package/dist/esm/schemas/project.mjs +27 -0
- package/dist/esm/schemas/recording-meta.mjs +11 -0
- package/dist/esm/types/assets.mjs +1 -0
- package/dist/esm/types.mjs +12 -0
- package/dist/esm/ui/Dialog.js +71 -0
- package/dist/esm/ui/DockableDialog.js +131 -0
- package/dist/esm/ui/DockableDialog.module.css +63 -0
- package/dist/esm/ui/RadioTabs.js +13 -0
- package/dist/esm/ui/RadioTabs.module.css +54 -0
- package/dist/esm/ui/Tabs.js +29 -0
- package/dist/esm/ui/Tabs.module.css +31 -0
- package/dist/esm/ui/Toast.js +64 -0
- package/dist/esm/ui/Toast.module.css +50 -0
- package/dist/esm/ui/Toaster.js +13 -0
- package/dist/esm/ui/Toaster.module.css +9 -0
- package/dist/esm/ui/test.js +14 -0
- package/dist/esm/utils/dom.mjs +6 -0
- package/dist/esm/utils/fs.mjs +94 -0
- package/dist/esm/utils/misc.mjs +15 -0
- package/dist/esm/utils/react.mjs +8 -0
- package/dist/esm/utils/rsync.mjs +57 -0
- package/dist/templates/project.json.hbs +5 -0
- package/dist/templates/projects/code/page.tsx.hbs +23 -0
- package/dist/templates/projects/code/src/assets.ts.hbs +9 -0
- package/dist/templates/projects/code/src/client.tsx.hbs +22 -0
- package/dist/templates/projects/code/src/helpers.ts.hbs +21 -0
- package/dist/templates/projects/code/src/highlights.ts.hbs +3 -0
- package/dist/templates/projects/code/src/markers.ts.hbs +13 -0
- package/dist/templates/projects/code/src/project.ts.hbs +23 -0
- package/dist/templates/projects/code/template.json +3 -0
- package/dist/templates/projects/default/page.tsx.hbs +23 -0
- package/dist/templates/projects/default/src/assets.ts.hbs +9 -0
- package/dist/templates/projects/default/src/client.tsx.hbs +22 -0
- package/dist/templates/projects/default/src/helpers.ts.hbs +21 -0
- package/dist/templates/projects/default/src/highlights.ts.hbs +3 -0
- package/dist/templates/projects/default/src/markers.ts.hbs +13 -0
- package/dist/templates/projects/default/src/project.ts.hbs +23 -0
- package/dist/templates/projects/default/template.json +4 -0
- package/dist/templates/types.ts.hbs +20 -0
- package/dist/types/LiqvidDevToolsProvider.d.ts +12 -0
- package/dist/types/api/contract.d.mts +66 -0
- package/dist/types/api/project-meta.d.mts +1 -0
- package/dist/types/api/recording.d.mts +5 -0
- package/dist/types/api/root.d.mts +1 -0
- package/dist/types/api/static-file.d.mts +6 -0
- package/dist/types/api/types.d.mts +1 -0
- package/dist/types/client.d.mts +43 -0
- package/dist/types/conventions.d.mts +3 -0
- package/dist/types/index.d.mts +19 -0
- package/dist/types/initialize.d.mts +14 -0
- package/dist/types/jobs/watch-assets.d.mts +18 -0
- package/dist/types/jobs/watch-project-files.d.mts +4 -0
- package/dist/types/next/api.d.mts +14 -0
- package/dist/types/next/page.d.ts +4 -0
- package/dist/types/pages/NewProjectButton.d.ts +1 -0
- package/dist/types/pages/RebuildButton.d.ts +1 -0
- package/dist/types/pages/root-actions.d.ts +29 -0
- package/dist/types/pages/root.d.ts +1 -0
- package/dist/types/providers/hosting/github-pages.d.mts +11 -0
- package/dist/types/providers/hosting/liqvid-studio.d.mts +10 -0
- package/dist/types/providers/hosting/s3.d.mts +11 -0
- package/dist/types/providers/hosting/sftp.d.mts +13 -0
- package/dist/types/providers/index.d.mts +10 -0
- package/dist/types/providers/social/bluesky.d.mts +10 -0
- package/dist/types/providers/social/facebook.d.mts +10 -0
- package/dist/types/providers/social/instagram.d.mts +10 -0
- package/dist/types/providers/social/twitter.d.mts +9 -0
- package/dist/types/providers/social/youtube.d.mts +9 -0
- package/dist/types/providers/types.d.mts +9 -0
- package/dist/types/publish.d.mts +1 -0
- package/dist/types/recording/RecordingControl.d.ts +16 -0
- package/dist/types/recording/RecordingDialog.d.ts +10 -0
- package/dist/types/schemas/liqvid-config.d.mts +51 -0
- package/dist/types/schemas/project.d.mts +51 -0
- package/dist/types/schemas/recording-meta.d.mts +17 -0
- package/dist/types/types/assets.d.mts +3 -0
- package/dist/types/types.d.mts +20 -0
- package/dist/types/ui/Dialog.d.ts +31 -0
- package/dist/types/ui/DockableDialog.d.ts +25 -0
- package/dist/types/ui/RadioTabs.d.ts +18 -0
- package/dist/types/ui/Tabs.d.ts +9 -0
- package/dist/types/ui/Toast.d.ts +23 -0
- package/dist/types/ui/Toaster.d.ts +6 -0
- package/dist/types/ui/test.d.ts +9 -0
- package/dist/types/utils/dom.d.mts +3 -0
- package/dist/types/utils/fs.d.mts +32 -0
- package/dist/types/utils/misc.d.mts +4 -0
- package/dist/types/utils/react.d.mts +5 -0
- package/dist/types/utils/rsync.d.mts +10 -0
- package/package.json +94 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { providersMap } from "./providers/index.mjs";
|
|
2
|
+
import { LiqvidConfig } from "./schemas/liqvid-config.mjs";
|
|
3
|
+
import { loadJson } from "./utils/fs.mjs";
|
|
4
|
+
// Example Usage (Demonstration - NOT production ready)
|
|
5
|
+
async function main() {
|
|
6
|
+
// load config file
|
|
7
|
+
const configFileName = "./liqvid.config.json";
|
|
8
|
+
const $config = await loadJson(LiqvidConfig, configFileName);
|
|
9
|
+
if (!$config.isOk) {
|
|
10
|
+
console.error("failed to parse config file", $config.unwrapErr());
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const config = $config.unwrap();
|
|
14
|
+
// initialize providers
|
|
15
|
+
const contentProviderConfig = config.providers[config.backend.content];
|
|
16
|
+
if (!contentProviderConfig) {
|
|
17
|
+
throw new Error(`missing configuration for content provider "${config.backend.content}"`);
|
|
18
|
+
}
|
|
19
|
+
const contentProvider = new providersMap[config.backend.content](contentProviderConfig);
|
|
20
|
+
// const mediaProviderConfig = config.providers[config.backend.media];
|
|
21
|
+
// if (!mediaProviderConfig) {
|
|
22
|
+
// throw new Error(
|
|
23
|
+
// `missing configuration for media provider "${config.backend.media}"`,
|
|
24
|
+
// );
|
|
25
|
+
// }
|
|
26
|
+
// const _mediaProvider = new providersMap[config.backend.media](
|
|
27
|
+
// mediaProviderConfig as any,
|
|
28
|
+
// );
|
|
29
|
+
try {
|
|
30
|
+
await contentProvider.publishContent("./build/client");
|
|
31
|
+
console.log("Directory synchronization completed successfully!");
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error("Directory synchronization failed:", error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
main();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEventListener } from "@liqvid/event-emitter/react";
|
|
4
|
+
import { useKeyboardShortcut } from "@liqvid/keymap/react";
|
|
5
|
+
import { useRecordingApi } from "@liqvid/recording";
|
|
6
|
+
import { useForceUpdate } from "@liqvid/utils";
|
|
7
|
+
import { useCallback, useRef, useState } from "react";
|
|
8
|
+
import { saveRecording } from "../client.mjs";
|
|
9
|
+
import { useProjectContext } from "../LiqvidDevToolsProvider";
|
|
10
|
+
import { DockableDialog } from "../ui/DockableDialog";
|
|
11
|
+
import { RecordingDialog } from "./RecordingDialog";
|
|
12
|
+
/**
|
|
13
|
+
* Liqvid recording control.
|
|
14
|
+
*/
|
|
15
|
+
export function RecordingControl({ shortcuts }) {
|
|
16
|
+
const { manager, discard, pauseResume, startStop } = useRecordingApi();
|
|
17
|
+
const { projectPath } = useProjectContext();
|
|
18
|
+
const forceUpdate = useForceUpdate();
|
|
19
|
+
// Collect finalized data from all plugins
|
|
20
|
+
const finalizedDataRef = useRef([]);
|
|
21
|
+
// recording manager
|
|
22
|
+
useEventListener(manager, "finalize", useCallback((args) => {
|
|
23
|
+
if (args === undefined) {
|
|
24
|
+
// End signal - save all collected data to server
|
|
25
|
+
const recordingData = finalizedDataRef.current;
|
|
26
|
+
finalizedDataRef.current = [];
|
|
27
|
+
if (recordingData.length > 0) {
|
|
28
|
+
saveRecording({
|
|
29
|
+
body: {
|
|
30
|
+
durationMs: manager.duration.inMilliseconds(),
|
|
31
|
+
plugins: recordingData,
|
|
32
|
+
},
|
|
33
|
+
search: { url: projectPath },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Individual plugin data - collect it
|
|
39
|
+
finalizedDataRef.current.push(args);
|
|
40
|
+
}
|
|
41
|
+
forceUpdate();
|
|
42
|
+
}, [forceUpdate, manager, projectPath]));
|
|
43
|
+
useEventListener(manager, "start", forceUpdate);
|
|
44
|
+
useEventListener(manager, "pause", forceUpdate);
|
|
45
|
+
useEventListener(manager, "resume", forceUpdate);
|
|
46
|
+
// warn before closing if recordings exist
|
|
47
|
+
useWarnBeforeClosingIfRecordingsExist();
|
|
48
|
+
// active plugins
|
|
49
|
+
const activePlugins = useRef(null);
|
|
50
|
+
if (activePlugins.current === null) {
|
|
51
|
+
activePlugins.current = {};
|
|
52
|
+
// for (const plugin of plugins) {
|
|
53
|
+
// activePlugins.current[plugin.package] = false;
|
|
54
|
+
// }
|
|
55
|
+
}
|
|
56
|
+
// plugins dictionary
|
|
57
|
+
const [pluginsByKey] = useState(() => {
|
|
58
|
+
const dict = {};
|
|
59
|
+
// for (const plugin of plugins) {
|
|
60
|
+
// dict[plugin.package] = plugin;
|
|
61
|
+
// }
|
|
62
|
+
return dict;
|
|
63
|
+
});
|
|
64
|
+
/* keyboard controls */
|
|
65
|
+
useKeyboardShortcut(shortcuts?.discard, discard);
|
|
66
|
+
useKeyboardShortcut(shortcuts?.pause, pauseResume);
|
|
67
|
+
useKeyboardShortcut(shortcuts?.startStop, startStop);
|
|
68
|
+
/* render */
|
|
69
|
+
return (_jsxs(DockableDialog.Root, { name: "recording", shortcut: shortcuts?.toggle, children: [_jsx(DockableDialog.Trigger, { asChild: true, children: _jsx(RecordingIndicatorIcon, {}) }), _jsx(RecordingDialog, { shortcuts: shortcuts })] }));
|
|
70
|
+
}
|
|
71
|
+
function useWarnBeforeClosingIfRecordingsExist() {
|
|
72
|
+
const { recordings } = useRecordingApi();
|
|
73
|
+
const warn = useRef(false);
|
|
74
|
+
warn.current = recordings.length > 0;
|
|
75
|
+
useEventListener(globalThis?.window, "beforeunload", useCallback((e) => {
|
|
76
|
+
if (warn.current)
|
|
77
|
+
e.returnValue = "You have recording data";
|
|
78
|
+
}, []));
|
|
79
|
+
}
|
|
80
|
+
const colors = {
|
|
81
|
+
active: "red",
|
|
82
|
+
inactive: "#666",
|
|
83
|
+
paused: "yellow",
|
|
84
|
+
};
|
|
85
|
+
const labels = {
|
|
86
|
+
active: "Recording in progress",
|
|
87
|
+
inactive: "No recording in progress",
|
|
88
|
+
paused: "Recording is paused",
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Red when playback is active
|
|
92
|
+
*/
|
|
93
|
+
function RecordingIndicatorIcon(props) {
|
|
94
|
+
const state = useRecordingState();
|
|
95
|
+
return (_jsxs("svg", { height: "36", viewBox: "-50 -50 100 100", width: "36", ...props, children: [_jsx("title", { children: labels[state] }), _jsx("circle", { cx: "0", cy: "0", fill: colors[state], r: "35", stroke: "white", strokeWidth: "5" })] }));
|
|
96
|
+
}
|
|
97
|
+
function useRecordingState() {
|
|
98
|
+
const { manager } = useRecordingApi();
|
|
99
|
+
const forceUpdate = useForceUpdate();
|
|
100
|
+
// recording manager
|
|
101
|
+
useEventListener(manager, "cancel", forceUpdate);
|
|
102
|
+
useEventListener(manager, "finalize", forceUpdate);
|
|
103
|
+
useEventListener(manager, "pause", forceUpdate);
|
|
104
|
+
useEventListener(manager, "resume", forceUpdate);
|
|
105
|
+
useEventListener(manager, "start", forceUpdate);
|
|
106
|
+
if (manager?.active) {
|
|
107
|
+
return manager.paused ? "paused" : "active";
|
|
108
|
+
}
|
|
109
|
+
return "inactive";
|
|
110
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Collapsible } from "@base-ui/react/collapsible";
|
|
3
|
+
import { Keymap } from "@liqvid/keymap";
|
|
4
|
+
import { useRecordingApi } from "@liqvid/recording";
|
|
5
|
+
import { usePluginApi } from "@liqvid/studio-plugin-api";
|
|
6
|
+
import { formatTime, formatTimeDuration } from "@liqvid/utils";
|
|
7
|
+
import { Fragment, useCallback, useEffect, useState } from "react";
|
|
8
|
+
import { listRecordings } from "../client.mjs";
|
|
9
|
+
import { useProjectContext } from "../LiqvidDevToolsProvider";
|
|
10
|
+
import { DockableDialog } from "../ui/DockableDialog";
|
|
11
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/Tabs";
|
|
12
|
+
import { useToggle } from "../utils/react.mjs";
|
|
13
|
+
import styles from "./RecordingDialog.module.css";
|
|
14
|
+
export function RecordingDialog({ shortcuts, onShortcutChange, }) {
|
|
15
|
+
const { projectPath } = useProjectContext();
|
|
16
|
+
const { enabledPlugins, togglePlugin } = useRecordingApi();
|
|
17
|
+
const { plugins } = usePluginApi();
|
|
18
|
+
const [recordings, setRecordings] = useState([]);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
listRecordings({ search: { url: projectPath } }).then(($res) => {
|
|
21
|
+
if ($res.isErr) {
|
|
22
|
+
console.error($res.unwrapErr());
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
setRecordings($res.unwrap());
|
|
26
|
+
});
|
|
27
|
+
}, [projectPath]);
|
|
28
|
+
return (_jsxs(DockableDialog.Dialog, { children: [_jsx(DockableDialog.Header, { children: "Recording" }), _jsx(DockableDialog.Content, { children: _jsx("div", { id: "lv-recording-dialog", children: _jsxs(Tabs, { defaultValue: "configuration", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { className: "lv-recording-tabs", value: "configuration", children: "Configuration" }), _jsx(TabsTrigger, { className: "lv-recording-tabs", value: "saved", children: "Recordings" }), _jsx(TabsTrigger, { className: "lv-recording-tabs", value: "shortcuts", children: "Shortcuts" })] }), _jsx(TabsContent, { asChild: true, value: "configuration", children: _jsxs("section", { children: [_jsx("h3", { children: "Plugins" }), _jsx("div", { className: styles.togglePlugins, children: Object.values(plugins).map((plugin) => {
|
|
29
|
+
if (!("recorder" in plugin))
|
|
30
|
+
return null;
|
|
31
|
+
const studioPlugin = plugins[plugin.package];
|
|
32
|
+
if (!studioPlugin)
|
|
33
|
+
return null;
|
|
34
|
+
return (_jsx("button", { "aria-checked": enabledPlugins[plugin.package], className: styles.recordingToggle, onClick: () => togglePlugin(plugin.package), role: "switch", type: "button", children: plugin.icon({ height: 24, width: 24 }) }, plugin.package));
|
|
35
|
+
}) }), _jsx("table", { className: styles.configurationTable, children: _jsx("tbody", { children: Object.values(plugins).map((plugin) => {
|
|
36
|
+
if (!("recorder" in plugin))
|
|
37
|
+
return null;
|
|
38
|
+
if (!enabledPlugins[plugin.package])
|
|
39
|
+
return null;
|
|
40
|
+
const ConfigurationComponent = plugin.configurationComponent;
|
|
41
|
+
if (!ConfigurationComponent)
|
|
42
|
+
return null;
|
|
43
|
+
return (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: plugin.icon({ height: 36, width: 36 }) }), _jsx("td", { children: _jsx(ConfigurationComponent, {}) })] }, plugin.package));
|
|
44
|
+
}) }) })] }) }), _jsx(TabsContent, { asChild: true, value: "saved", children: _jsxs("section", { children: [_jsx("h3", { children: "Saved" }), _jsx("div", { className: styles.Recordings, children: recordings.map((r) => (_jsx(RecordingRow, { recording: r }, r.name))) })] }) }), _jsx(TabsContent, { asChild: true, value: "shortcuts", children: _jsxs("section", { children: [_jsx("h3", { children: "Shortcuts" }), _jsx(ShortcutsTable, { onShortcutChange: onShortcutChange, shortcuts: shortcuts })] }) })] }) }) })] }));
|
|
45
|
+
}
|
|
46
|
+
export function RecordingRow({ recording: r }) {
|
|
47
|
+
const { value: expanded, set: setExpanded } = useToggle();
|
|
48
|
+
const { plugins } = usePluginApi();
|
|
49
|
+
// console.log({ plugins });
|
|
50
|
+
return (_jsxs(Collapsible.Root, { className: styles.RecordingRow, onOpenChange: setExpanded, open: expanded, children: [_jsxs(Collapsible.Trigger, { className: styles.RecordingRowTrigger, children: [_jsx("span", { className: styles.recordingName, children: r.name }), _jsx("span", { className: styles.pluginIcons, children: r.plugins.map((p) => p in plugins ? (_jsx(Fragment, { children: plugins[p].icon() }, p)) : null) }), _jsx("time", { className: styles.recordingDuration, dateTime: formatTimeDuration(r.duration), children: formatTime(r.duration) })] }), _jsx(Collapsible.Panel, { className: styles.RecordingRowExpand, children: r.plugins.map((p) => {
|
|
51
|
+
const plugin = plugins[p];
|
|
52
|
+
if (!plugin)
|
|
53
|
+
return null;
|
|
54
|
+
const Component = plugin.recordingComponent;
|
|
55
|
+
if (!Component)
|
|
56
|
+
return null;
|
|
57
|
+
return _jsx(Component, { name: r.name }, plugin.package);
|
|
58
|
+
}) })] }));
|
|
59
|
+
}
|
|
60
|
+
const shortcutCommands = [
|
|
61
|
+
["Toggle panel", "toggle"],
|
|
62
|
+
["Start/Stop recording", "startStop"],
|
|
63
|
+
["Pause recording", "pause"],
|
|
64
|
+
["Discard recording", "discard"],
|
|
65
|
+
];
|
|
66
|
+
function ShortcutsTable({ shortcuts, onShortcutChange, }) {
|
|
67
|
+
return (_jsxs("table", { className: styles.shortcutsTable, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Command" }), _jsx("th", { children: "Shortcut" })] }) }), _jsx("tbody", { children: shortcutCommands.map(([label, key]) => (_jsx(ShortcutRow, { label: label, onChange: onShortcutChange
|
|
68
|
+
? (value) => onShortcutChange(key, value)
|
|
69
|
+
: undefined, shortcut: shortcuts?.[key] }, key))) })] }));
|
|
70
|
+
}
|
|
71
|
+
function ShortcutRow({ label, shortcut, onChange, }) {
|
|
72
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
73
|
+
const [localValue, setLocalValue] = useState(undefined);
|
|
74
|
+
const displayValue = localValue ?? shortcut;
|
|
75
|
+
const identifyKey = useCallback((e) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
const seq = Keymap.identify(e);
|
|
78
|
+
setLocalValue(seq);
|
|
79
|
+
onChange?.(seq);
|
|
80
|
+
setIsRecording(false);
|
|
81
|
+
}, [onChange]);
|
|
82
|
+
const handleFocus = useCallback(() => {
|
|
83
|
+
setIsRecording(true);
|
|
84
|
+
}, []);
|
|
85
|
+
const handleBlur = useCallback(() => {
|
|
86
|
+
setIsRecording(false);
|
|
87
|
+
}, []);
|
|
88
|
+
return (_jsxs("tr", { children: [_jsx("td", { children: label }), _jsx("td", { children: _jsx("input", { className: styles.shortcutInput, "data-recording": isRecording || undefined, onBlur: handleBlur, onFocus: handleFocus, onKeyDown: identifyKey, placeholder: isRecording ? "Press a key..." : "Click to record", readOnly: true, type: "text", value: displayValue ? fmtSeq(displayValue) : "" }) })] }));
|
|
89
|
+
}
|
|
90
|
+
/** Format key sequences with special characters on Mac */
|
|
91
|
+
function fmtSeq(str) {
|
|
92
|
+
if (!isMac())
|
|
93
|
+
return str;
|
|
94
|
+
if (str === undefined)
|
|
95
|
+
return str;
|
|
96
|
+
return str
|
|
97
|
+
.split("+")
|
|
98
|
+
.map((k) => {
|
|
99
|
+
if (k === "Ctrl")
|
|
100
|
+
return "^";
|
|
101
|
+
else if (k === "Alt")
|
|
102
|
+
return "\u2325";
|
|
103
|
+
if (k === "Shift")
|
|
104
|
+
return "\u21E7";
|
|
105
|
+
if (k === "Meta")
|
|
106
|
+
return "\u2318";
|
|
107
|
+
return k;
|
|
108
|
+
})
|
|
109
|
+
.join("");
|
|
110
|
+
}
|
|
111
|
+
function isMac() {
|
|
112
|
+
return (typeof globalThis.navigator !== "undefined" &&
|
|
113
|
+
navigator.platform === "MacIntel");
|
|
114
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
.Recordings {
|
|
2
|
+
--radius: 4px;
|
|
3
|
+
|
|
4
|
+
background-color: var(--gray-ui);
|
|
5
|
+
border: 1px solid var(--gray-sep);
|
|
6
|
+
border-radius: var(--radius);
|
|
7
|
+
|
|
8
|
+
margin: 4px 0;
|
|
9
|
+
|
|
10
|
+
* {
|
|
11
|
+
transition-property: background-color, border-color, color;
|
|
12
|
+
transition-duration: 150ms;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.RecordingRow {
|
|
17
|
+
&:first-child {
|
|
18
|
+
&,
|
|
19
|
+
& > .RecordingRowTrigger {
|
|
20
|
+
border-radius: var(--radius) var(--radius) 0 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
&:last-child {
|
|
25
|
+
&,
|
|
26
|
+
& > .RecordingRowTrigger {
|
|
27
|
+
border-radius: 0 0 var(--radius) var(--radius);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.RecordingRowTrigger {
|
|
33
|
+
align-items: center;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
display: flex;
|
|
36
|
+
padding: 4px 8px;
|
|
37
|
+
width: 100%;
|
|
38
|
+
|
|
39
|
+
&:hover {
|
|
40
|
+
background-color: var(--gray-hover);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&[data-panel-open] {
|
|
44
|
+
background-color: var(--gray-active);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.recordingName {
|
|
49
|
+
font-family: monospace;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.pluginIcons {
|
|
53
|
+
margin-left: auto;
|
|
54
|
+
|
|
55
|
+
svg {
|
|
56
|
+
height: 24px;
|
|
57
|
+
width: 24px;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.recordingDuration {
|
|
62
|
+
font-family: monospace;
|
|
63
|
+
font-size: 16px;
|
|
64
|
+
text-align: right;
|
|
65
|
+
width: 4em;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.RecordingRowExpand {
|
|
69
|
+
background-color: var(--gray-subtle);
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
|
|
72
|
+
&[data-open] {
|
|
73
|
+
animation: slideDown 150ms ease-out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&[data-closed] {
|
|
77
|
+
animation: slideUp 150ms ease-out;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@keyframes slideDown {
|
|
82
|
+
from {
|
|
83
|
+
height: 0;
|
|
84
|
+
}
|
|
85
|
+
to {
|
|
86
|
+
height: var(--collapsible-panel-height);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@keyframes slideUp {
|
|
91
|
+
from {
|
|
92
|
+
height: var(--collapsible-panel-height);
|
|
93
|
+
}
|
|
94
|
+
to {
|
|
95
|
+
height: 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.details {
|
|
100
|
+
padding: 4px 4px 4px 16px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.togglePlugins {
|
|
104
|
+
display: flex;
|
|
105
|
+
gap: 0.5em;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.recordingToggle {
|
|
109
|
+
background-color: var(--gray-ui);
|
|
110
|
+
border-radius: 4px;
|
|
111
|
+
cursor: pointer;
|
|
112
|
+
height: 36px;
|
|
113
|
+
width: 36px;
|
|
114
|
+
|
|
115
|
+
transition: unset;
|
|
116
|
+
|
|
117
|
+
&[aria-checked="true"] {
|
|
118
|
+
background-color: red;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
> svg {
|
|
122
|
+
height: 36px;
|
|
123
|
+
width: 36px;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.configurationTable {
|
|
128
|
+
border: 1px solid #000;
|
|
129
|
+
margin: 1em auto;
|
|
130
|
+
width: 100%;
|
|
131
|
+
|
|
132
|
+
> tbody > tr > th {
|
|
133
|
+
text-align: right;
|
|
134
|
+
padding: 4px;
|
|
135
|
+
width: calc(36px + 8px);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
> tbody > tr > td {
|
|
139
|
+
padding: 4px;
|
|
140
|
+
text-align: left;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.shortcutsTable {
|
|
145
|
+
border: 1px solid var(--gray-sep);
|
|
146
|
+
border-collapse: collapse;
|
|
147
|
+
margin: 0.5em 0;
|
|
148
|
+
width: 100%;
|
|
149
|
+
|
|
150
|
+
th,
|
|
151
|
+
td {
|
|
152
|
+
border: 1px solid var(--gray-sep);
|
|
153
|
+
padding: 6px 8px;
|
|
154
|
+
text-align: left;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
thead th {
|
|
158
|
+
background-color: var(--gray-ui);
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
text-transform: uppercase;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
tbody tr:hover {
|
|
165
|
+
background-color: var(--gray-hover);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.shortcutInput {
|
|
170
|
+
background-color: var(--gray-subtle);
|
|
171
|
+
border: 1px solid var(--gray-sep);
|
|
172
|
+
border-radius: 4px;
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
font-family: monospace;
|
|
175
|
+
font-size: 13px;
|
|
176
|
+
padding: 4px 8px;
|
|
177
|
+
text-align: center;
|
|
178
|
+
width: 100%;
|
|
179
|
+
|
|
180
|
+
&:focus {
|
|
181
|
+
border-color: var(--blue-focus, #0066cc);
|
|
182
|
+
outline: none;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
&[data-recording] {
|
|
186
|
+
background-color: var(--yellow-subtle, #fffbe6);
|
|
187
|
+
border-color: var(--yellow-border, #d9a600);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
&::placeholder {
|
|
191
|
+
color: var(--gray-text-muted, #888);
|
|
192
|
+
font-style: italic;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ProviderConfigGitHubPages } from "../providers/hosting/github-pages.mjs";
|
|
3
|
+
import { ProviderConfigLiqvidStudio } from "../providers/hosting/liqvid-studio.mjs";
|
|
4
|
+
import { ProviderConfigS3 } from "../providers/hosting/s3.mjs";
|
|
5
|
+
import { ProviderConfigSFTP } from "../providers/hosting/sftp.mjs";
|
|
6
|
+
import { ProviderConfigBlueSky } from "../providers/social/bluesky.mjs";
|
|
7
|
+
import { ProviderConfigFacebook } from "../providers/social/facebook.mjs";
|
|
8
|
+
import { ProviderConfigInstagram } from "../providers/social/instagram.mjs";
|
|
9
|
+
import { ProviderConfigTwitter } from "../providers/social/twitter.mjs";
|
|
10
|
+
import { ProviderConfigYouTube } from "../providers/social/youtube.mjs";
|
|
11
|
+
export const LiqvidConfig = z.object({
|
|
12
|
+
$schema: z.string(),
|
|
13
|
+
backend: z.object({
|
|
14
|
+
content: z.enum(["githubPages", "liqvidStudio", "s3", "sftp"]),
|
|
15
|
+
media: z.enum(["liqvidStudio", "s3", "sftp"]),
|
|
16
|
+
}),
|
|
17
|
+
providers: z.object({
|
|
18
|
+
// hosting
|
|
19
|
+
githubPages: ProviderConfigGitHubPages.optional(),
|
|
20
|
+
liqvidStudio: ProviderConfigLiqvidStudio.optional(),
|
|
21
|
+
s3: ProviderConfigS3.optional(),
|
|
22
|
+
sftp: ProviderConfigSFTP.optional(),
|
|
23
|
+
// social
|
|
24
|
+
...{
|
|
25
|
+
bluesky: ProviderConfigBlueSky.optional(),
|
|
26
|
+
facebook: ProviderConfigFacebook.optional(),
|
|
27
|
+
instagram: ProviderConfigInstagram.optional(),
|
|
28
|
+
twitter: ProviderConfigTwitter.optional(),
|
|
29
|
+
youtube: ProviderConfigYouTube.optional(),
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { DurationOptions } from "@liqvid/duration/zod";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const AspectRatio = z.object({
|
|
4
|
+
height: z.number(),
|
|
5
|
+
width: z.number(),
|
|
6
|
+
});
|
|
7
|
+
export const AspectRatioSpecifier = z.union([
|
|
8
|
+
z.templateLiteral([z.number(), ":", z.number()]),
|
|
9
|
+
z.tuple([z.number(), z.number()]),
|
|
10
|
+
AspectRatio,
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* project.json files
|
|
14
|
+
*/
|
|
15
|
+
export const ProjectJson = z.object({
|
|
16
|
+
aspectRatio: AspectRatioSpecifier.optional().default({
|
|
17
|
+
height: 9,
|
|
18
|
+
width: 16,
|
|
19
|
+
}),
|
|
20
|
+
name: z.string(),
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* auto-generated project-meta.json files
|
|
24
|
+
*/
|
|
25
|
+
export const AutoGenProjectMeta = z.object({
|
|
26
|
+
duration: DurationOptions,
|
|
27
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const RecordingMetaFile = z.object({
|
|
3
|
+
created: z.iso.datetime(),
|
|
4
|
+
duration: z.object({
|
|
5
|
+
milliseconds: z.number(),
|
|
6
|
+
}),
|
|
7
|
+
});
|
|
8
|
+
export const RecordingMeta = RecordingMetaFile.extend({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
plugins: z.array(z.string()),
|
|
11
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class DirectoryHelper {
|
|
2
|
+
dirname;
|
|
3
|
+
constructor(dirname = "") {
|
|
4
|
+
this.dirname = dirname;
|
|
5
|
+
}
|
|
6
|
+
dir(dirname) {
|
|
7
|
+
return new DirectoryHelper(`${this.dirname}/${dirname}`);
|
|
8
|
+
}
|
|
9
|
+
file(filename, version) {
|
|
10
|
+
return `${this.dirname}/${filename}`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Dialog } from "@base-ui/react/dialog";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import { Children, cloneElement, createContext, isValidElement, useCallback, useContext, useMemo, useState, } from "react";
|
|
5
|
+
import styles from "./Dialog.module.css";
|
|
6
|
+
const DialogApiContext = createContext({
|
|
7
|
+
close() { },
|
|
8
|
+
isDialog: false,
|
|
9
|
+
isOpen: false,
|
|
10
|
+
});
|
|
11
|
+
DialogApiContext.displayName = "DialogApi";
|
|
12
|
+
export function useDialogApi() {
|
|
13
|
+
return useContext(DialogApiContext);
|
|
14
|
+
}
|
|
15
|
+
export function DialogRoot({ children, defaultOpen = false, onOpenChange, open: controlledOpen, }) {
|
|
16
|
+
// open state
|
|
17
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
18
|
+
/** setOpen() wrapper which updates ancestor dropdown */
|
|
19
|
+
const wrappedOnOpenChange = useCallback((newOpen) => {
|
|
20
|
+
(onOpenChange ?? setOpen)(newOpen);
|
|
21
|
+
}, [onOpenChange]);
|
|
22
|
+
// api
|
|
23
|
+
const dialogApi = useMemo(() => ({
|
|
24
|
+
close() {
|
|
25
|
+
setOpen(false);
|
|
26
|
+
},
|
|
27
|
+
isDialog: true,
|
|
28
|
+
get isOpen() {
|
|
29
|
+
return controlledOpen ?? open;
|
|
30
|
+
},
|
|
31
|
+
}), [controlledOpen, open]);
|
|
32
|
+
return (_jsx(Dialog.Root, { modal: true, onOpenChange: wrappedOnOpenChange, open: controlledOpen ?? open, children: _jsx(DialogApiContext.Provider, { value: dialogApi, children: children }) }));
|
|
33
|
+
}
|
|
34
|
+
export function DialogClose({ asChild, children, className, ...props }) {
|
|
35
|
+
const combinedClassName = classNames(styles.Close, className);
|
|
36
|
+
if (asChild && isValidElement(children)) {
|
|
37
|
+
return (_jsx(Dialog.Close, { ...props, render: (renderProps) => {
|
|
38
|
+
const child = Children.only(children);
|
|
39
|
+
return cloneElement(child, {
|
|
40
|
+
...renderProps,
|
|
41
|
+
className: classNames(combinedClassName, child.props.className),
|
|
42
|
+
});
|
|
43
|
+
} }));
|
|
44
|
+
}
|
|
45
|
+
return (_jsx(Dialog.Close, { className: combinedClassName, ...props, children: children }));
|
|
46
|
+
}
|
|
47
|
+
export function DialogContent({ className, ...props }) {
|
|
48
|
+
return (_jsx(Dialog.Popup, { className: classNames(styles.Content, className), ...props }));
|
|
49
|
+
}
|
|
50
|
+
export function DialogPortal(props) {
|
|
51
|
+
return _jsx(Dialog.Portal, { ...props });
|
|
52
|
+
}
|
|
53
|
+
export function DialogOverlay({ className, ...props }) {
|
|
54
|
+
return (_jsx(Dialog.Backdrop, { className: classNames(styles.Overlay, className), ...props }));
|
|
55
|
+
}
|
|
56
|
+
export function DialogTitle({ className, ...props }) {
|
|
57
|
+
return (_jsx(Dialog.Title, { className: classNames(styles.Title, className), ...props }));
|
|
58
|
+
}
|
|
59
|
+
export function DialogTrigger({ asChild, children, className, ...props }) {
|
|
60
|
+
const combinedClassName = classNames(styles.Trigger, className);
|
|
61
|
+
if (asChild && isValidElement(children)) {
|
|
62
|
+
return (_jsx(Dialog.Trigger, { ...props, render: (renderProps) => {
|
|
63
|
+
const child = Children.only(children);
|
|
64
|
+
return cloneElement(child, {
|
|
65
|
+
...renderProps,
|
|
66
|
+
className: classNames(combinedClassName, child.props.className),
|
|
67
|
+
});
|
|
68
|
+
} }));
|
|
69
|
+
}
|
|
70
|
+
return (_jsx(Dialog.Trigger, { className: combinedClassName, ...props, children: children }));
|
|
71
|
+
}
|