mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-11-09 15:33:31 +00:00
docs(design): add HeroUI component catalogs and implementation notes
This commit is contained in:
1 parent
179aa4d82e
commit
832a2c8bae
22 files changed
+3122
No files matched your search
@@ -0,0 +1,7 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly app component catalog</title><style>html,body,#root{min-width:1440px!important;width:1440px!important;min-height:1000px!important}html,body{height:auto!important;overflow:auto!important}</style></head><body><div id="root"></div><script type="module">
|
||||
import {installDesktopBridge} from '/tests/ui/bridge.ts';
|
||||
await installDesktopBridge({addInitScript:async(fn,arg)=>fn(arg)},'filmstrip.mp4');
|
||||
// Source device rows render against fixtures without opening physical microphones/cameras.
|
||||
if (navigator.mediaDevices) Object.defineProperty(navigator.mediaDevices, 'getUserMedia', {configurable:true,value:async()=>{throw new DOMException('Design fixture: live device access disabled','NotAllowedError')}});
|
||||
await import('/src/design-app-catalog.tsx');
|
||||
</script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly design transfer</title><style>html,body,#root{min-width:1440px!important;width:1440px!important;height:1000px!important;min-height:1000px!important}</style></head><body><div id="root"></div><script type="module">
|
||||
import {installDesktopBridge} from '/tests/ui/bridge.ts';
|
||||
await installDesktopBridge({addInitScript:async(fn,arg)=>fn(arg)},'filmstrip.mp4');
|
||||
await import('/src/main.tsx');
|
||||
</script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly Export and Popup Catalog</title></head><body><div id="root"></div><script type="module" src="/src/design-extra-catalog.tsx"></script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly HUD branch catalog</title><style>html,body,#root{min-width:1440px!important;width:1440px!important;min-height:1000px!important}html,body{height:auto!important;overflow:auto!important}</style></head><body><div id="root"></div><script type="module">
|
||||
import {installDesktopBridge} from '/tests/ui/bridge.ts';
|
||||
await installDesktopBridge({addInitScript:async(fn,arg)=>fn(arg)},'filmstrip.mp4');
|
||||
await import('/src/design-hud-branches.tsx');
|
||||
</script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<!doctype html>
|
||||
<html><head><meta charset="UTF-8"><title>Recordly inspector catalog</title>
|
||||
<style>html,body,#root{min-width:1440px!important;width:1440px!important;height:auto!important;overflow:visible!important}.inspector-preview .overflow-y-auto{overflow:visible!important;max-height:none!important}.inspector-preview{min-height:500px}.inspector-preview>.card{overflow:visible!important}</style>
|
||||
</head><body><div id="root"></div><script type="module">
|
||||
import {installDesktopBridge} from '/tests/ui/bridge.ts';
|
||||
await installDesktopBridge({addInitScript:async(fn,arg)=>fn(arg)},'filmstrip.mp4');
|
||||
await import('/src/design-inspector-catalog.tsx');
|
||||
</script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly Component Library</title></head><body><div id="root"></div><script type="module" src="/src/design-library.tsx"></script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly Preview Menus</title></head><body><div id="root"></div><script type="module" src="/src/design-preview-menus.tsx"></script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly Timeline Component Library</title></head><body><div id="root"></div><script type="module" src="/src/design-timeline-catalog.tsx"></script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly timeline detail components</title><style>html,body,#root{width:1200px!important;min-width:1200px!important;height:auto!important;overflow:visible!important}</style></head><body><div id="root"></div><script type="module">
|
||||
import {installDesktopBridge} from '/tests/ui/bridge.ts';
|
||||
await installDesktopBridge({addInitScript:async(fn,arg)=>fn(arg)},'filmstrip.mp4');
|
||||
await import('/src/design-timeline-details.tsx');
|
||||
</script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html><head><meta charset="UTF-8"><title>Recordly window states</title></head>
|
||||
<body><div id="root"></div><script type="module">
|
||||
import {installDesktopBridge} from '/tests/ui/bridge.ts';
|
||||
await installDesktopBridge({addInitScript:async(fn,arg)=>fn(arg)},'filmstrip.mp4');
|
||||
const query = new URLSearchParams(location.search);
|
||||
const windowType = query.get('windowType');
|
||||
const state = query.get('state') || 'available';
|
||||
const style = document.createElement('style');
|
||||
const [width, height] = windowType === 'update-toast' ? [420, 172] : windowType === 'countdown' ? [420, 320] : [1440, 1000];
|
||||
style.textContent = `html,body,#root{width:${width}px!important;min-width:${width}px!important;height:${height}px!important;min-height:${height}px!important}`;
|
||||
document.head.append(style);
|
||||
if (windowType === 'update-toast') {
|
||||
const phase = state.endsWith('error') ? 'error' : ['downloading','ready'].includes(state) ? state : 'available';
|
||||
window.electronAPI.getCurrentUpdateToastPayload = async () => ({
|
||||
version:'1.5.0',
|
||||
detail: phase === 'error' ? 'Check your connection and try again.' : phase === 'ready' ? 'Your update is ready to install.' : 'A new version of Recordly is available.',
|
||||
phase, delayMs: 3600000,
|
||||
isPreview: state === 'preview',
|
||||
isExperimental: state === 'experimental',
|
||||
progressPercent: 42, transferredBytes: 44040192, totalBytes: 104857600, bytesPerSecond: 3145728,
|
||||
primaryAction: state === 'check-error' ? 'retry-check' : 'install-and-restart',
|
||||
});
|
||||
}
|
||||
if (windowType === 'countdown') window.electronAPI.getActiveCountdown = async () => ({success:true, seconds:Number(query.get('seconds') || 3)});
|
||||
await import('/src/main.tsx');
|
||||
</script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html><html><head><meta charset="UTF-8"><title>Recordly window catalog</title><style>html,body,#root{width:1440px!important;min-width:1440px!important;height:auto!important;overflow:visible!important}[data-figma-name="HudWindow / Idle"] [style*="100vh"]{height:100%!important;align-items:center!important}[data-figma-name="HudWindow / Idle"] [style*="translate3d"]{transform:none!important}</style></head><body><div id="root"></div><script type="module">
|
||||
import {installDesktopBridge} from '/tests/ui/bridge.ts';
|
||||
await installDesktopBridge({addInitScript:async(fn,arg)=>fn(arg)},'filmstrip.mp4');
|
||||
await import('/src/design-window-catalog.tsx');
|
||||
</script><script src="https://mcp.figma.com/mcp/html-to-design/capture.js" async></script></body></html>
|
||||
@@ -0,0 +1,590 @@
|
||||
# Recordly HeroUI — Figma component coverage
|
||||
|
||||
[Open the editable Figma file](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm)
|
||||
|
||||
Audited 20 September 2026 against the `recordly-heroui` renderer. The file contains **254 native components**, **6,986 editable component layers**, **126 variables**, an assembled editor and two structural wireframes. Variable-alias validation found no broken references.
|
||||
|
||||
## Start here
|
||||
|
||||
- [Assembled desktop editor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-3)
|
||||
- [Editor wireframe](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=33-2)
|
||||
- [Recorder wireframe](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=33-13)
|
||||
- [Foundations](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=9-4)
|
||||
- [Getting started](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=9-3)
|
||||
|
||||
Components are organized by controls, inspectors, timeline, recording, windows and popup families. Edit a main component to update its Figma instances. The full component/node index below links directly to each definition. This is an editable design handoff; Figma changes do not automatically rewrite the React project.
|
||||
|
||||
## Scope and fidelity
|
||||
|
||||
- Every one of the 91 original renderer TSX modules is accounted for below: 85 visual modules and 6 providers/bootstrap modules without standalone UI.
|
||||
- Includes basic and advanced inspectors, four background tabs, selected/unselected timeline blocks, annotations, waveform and keyframe details, project dialogs, export stages and errors, presets, announcement layouts, all recorder menus, recording/finalizing states, floating webcam preview, countdown, update notifications, and tooltips.
|
||||
- Controls, text, icons and waveform shapes are editable Figma layers. Filmstrip/preview footage and image assets retain bitmap media fills. These are fixture media, not editable video sequences.
|
||||
- The source uses SF Pro; the matching Figma faces are used. Menlo was unavailable in Figma and its small monospaced samples use JetBrains Mono Medium.
|
||||
- Context providers, event hooks, native operating-system file/device permission dialogs and runtime behavior have no independent Figma visual. Project-owned dialogs and device-selection menus are included.
|
||||
- Frame and Crop inspector identifiers currently resolve to Scene in the source. Their captured compatibility states reflect that behavior. Extensions are retained as a labelled legacy component, despite removal from active navigation.
|
||||
- Capture-only containers make otherwise hidden dialogs visible and expand inspector scroll areas. Preview menus and recording-only HUD branches preserve source JSX and original CSS with inert handlers. No capture controls are added to the shipping editor.
|
||||
|
||||
## Source module coverage
|
||||
|
||||
| Source | Figma representation |
|
||||
| --- | --- |
|
||||
| [src/App.tsx](../src/App.tsx) | [Editor screen](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-3), [App / Startup](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4977) |
|
||||
| [src/components/announcements/AnnouncementDialog.tsx](../src/components/announcements/AnnouncementDialog.tsx) | [Popup/announcement-popup](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4683), [Announcement cover](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=51-37) |
|
||||
| [src/components/announcements/EditorAnnouncementBanner.tsx](../src/components/announcements/EditorAnnouncementBanner.tsx) | [Popup/announcement-banner](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4701) |
|
||||
| [src/components/announcements/LiveAnnouncementNotifications.tsx](../src/components/announcements/LiveAnnouncementNotifications.tsx) | [Popup/announcement-notification](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4713) |
|
||||
| [src/components/countdown/CountdownOverlay.tsx](../src/components/countdown/CountdownOverlay.tsx) | [CountdownOverlay / 3 seconds](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4916) |
|
||||
| [src/components/launch/HudWindow.tsx](../src/components/launch/HudWindow.tsx) | [HudWindow / Idle](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4968), [HUD — recording states](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=46-2) |
|
||||
| [src/components/launch/LaunchWindow.tsx](../src/components/launch/LaunchWindow.tsx) | [HudWindow / Idle](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4968), [HUD — recording states](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=46-2), [Popups — Recording](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-2) |
|
||||
| [src/components/launch/MarqueeText.tsx](../src/components/launch/MarqueeText.tsx) | [MarqueeText](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1306) |
|
||||
| [src/components/launch/RecordingControls.tsx](../src/components/launch/RecordingControls.tsx) | [RecordingControls / Recording](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1191), [RecordingControls / Paused](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1222) |
|
||||
| [src/components/launch/SourceSelector.tsx](../src/components/launch/SourceSelector.tsx) | [SourceSelectorContent / Sources](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1255), [Popup/launch-sources](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-255) |
|
||||
| [src/components/launch/UpdateToastWindow.tsx](../src/components/launch/UpdateToastWindow.tsx) | [Windows & tooltips](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4759) |
|
||||
| [src/components/launch/contexts/HudInteractionContext.tsx](../src/components/launch/contexts/HudInteractionContext.tsx) | Provider/bootstrap only; represented through its rendered children. |
|
||||
| [src/components/launch/popovers/CountdownPopover.tsx](../src/components/launch/popovers/CountdownPopover.tsx) | [Popup/launch-countdown](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-97) |
|
||||
| [src/components/launch/popovers/LaunchPopoverCoordinator.tsx](../src/components/launch/popovers/LaunchPopoverCoordinator.tsx) | Provider/bootstrap only; represented through its rendered children. |
|
||||
| [src/components/launch/popovers/MicPopover.tsx](../src/components/launch/popovers/MicPopover.tsx) | [Popup/launch-mic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-113), [Popup/launch-mic-devices](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-37) |
|
||||
| [src/components/launch/popovers/MorePopover.tsx](../src/components/launch/popovers/MorePopover.tsx) | [Popup/launch-more](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-236) |
|
||||
| [src/components/launch/popovers/PopoverScaffold.tsx](../src/components/launch/popovers/PopoverScaffold.tsx) | [DropdownItem / Selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1280), [MicDeviceRow / Selected and unselected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1301), [Popups — Recording](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-2) |
|
||||
| [src/components/launch/popovers/ProjectPopover.tsx](../src/components/launch/popovers/ProjectPopover.tsx) | [Popup/launch-projects](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-279) |
|
||||
| [src/components/launch/popovers/SourcePopover.tsx](../src/components/launch/popovers/SourcePopover.tsx) | [Popup/launch-sources](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-255) |
|
||||
| [src/components/launch/popovers/WebcamPopover.tsx](../src/components/launch/popovers/WebcamPopover.tsx) | [Popup/launch-webcam](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-122), [Popup/launch-webcam-devices](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-67) |
|
||||
| [src/components/ui/accordion.tsx](../src/components/ui/accordion.tsx) | [Accordion](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-15) |
|
||||
| [src/components/ui/audio-level-meter.tsx](../src/components/ui/audio-level-meter.tsx) | [App — Shared utilities](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-348) |
|
||||
| [src/components/ui/button.tsx](../src/components/ui/button.tsx) | [Button](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=18-2) |
|
||||
| [src/components/ui/card.tsx](../src/components/ui/card.tsx) | [Card](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-2) |
|
||||
| [src/components/ui/choice-group.tsx](../src/components/ui/choice-group.tsx) | [ChoiceGroup](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-36) |
|
||||
| [src/components/ui/color-picker.tsx](../src/components/ui/color-picker.tsx) | [ColorPalette](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-368), [ColorControl](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-379), [Popup/color](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-886) |
|
||||
| [src/components/ui/content-clamp.tsx](../src/components/ui/content-clamp.tsx) | [ContentClamp](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-383), [Tooltip / Content clamp](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4981) |
|
||||
| [src/components/ui/dialog.tsx](../src/components/ui/dialog.tsx) | [Popup/dialog](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-573) |
|
||||
| [src/components/ui/dropdown-menu.tsx](../src/components/ui/dropdown-menu.tsx) | [Popup/dropdown](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-783) |
|
||||
| [src/components/ui/input.tsx](../src/components/ui/input.tsx) | [Input](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=22-2), [TextArea](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=22-27) |
|
||||
| [src/components/ui/item-content.tsx](../src/components/ui/item-content.tsx) | [ItemContent](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-390) |
|
||||
| [src/components/ui/label.tsx](../src/components/ui/label.tsx) | [Label](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-47) |
|
||||
| [src/components/ui/popover.tsx](../src/components/ui/popover.tsx) | [Popup/popover](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-769) |
|
||||
| [src/components/ui/select.tsx](../src/components/ui/select.tsx) | [Select](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-58), [Popup/select](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-759) |
|
||||
| [src/components/ui/separator.tsx](../src/components/ui/separator.tsx) | [Separator](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-51) |
|
||||
| [src/components/ui/skeleton.tsx](../src/components/ui/skeleton.tsx) | [Skeleton](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-54) |
|
||||
| [src/components/ui/slider.tsx](../src/components/ui/slider.tsx) | [Slider](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=24-2) |
|
||||
| [src/components/ui/switch.tsx](../src/components/ui/switch.tsx) | [Switch](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=23-2) |
|
||||
| [src/components/ui/tabs.tsx](../src/components/ui/tabs.tsx) | [Tabs](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-45) |
|
||||
| [src/components/ui/toast.tsx](../src/components/ui/toast.tsx) | [Popup/toast](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-794) |
|
||||
| [src/components/ui/toggle-group.tsx](../src/components/ui/toggle-group.tsx) | [ToggleGroup](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-27) |
|
||||
| [src/components/ui/toggle.tsx](../src/components/ui/toggle.tsx) | [Toggle](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-2) |
|
||||
| [src/components/video-editor/AddCustomFontDialog.tsx](../src/components/video-editor/AddCustomFontDialog.tsx) | [Popup/font](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-605) |
|
||||
| [src/components/video-editor/AnnotationOverlay.tsx](../src/components/video-editor/AnnotationOverlay.tsx) | [Timeline — AnnotationOverlay](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4199) |
|
||||
| [src/components/video-editor/AnnotationSettingsPanel.tsx](../src/components/video-editor/AnnotationSettingsPanel.tsx) | [App — Annotation controls](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-750) |
|
||||
| [src/components/video-editor/ArrowSvgs.tsx](../src/components/video-editor/ArrowSvgs.tsx) | [AnnotationSettingsPanel / figure](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-986) — all eight editable direction vectors |
|
||||
| [src/components/video-editor/CaptionListPanel.tsx](../src/components/video-editor/CaptionListPanel.tsx) | [CaptionListPanel / Editing](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-748) |
|
||||
| [src/components/video-editor/CropControl.tsx](../src/components/video-editor/CropControl.tsx) | [CropControl](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-690) |
|
||||
| [src/components/video-editor/EditorWindow.tsx](../src/components/video-editor/EditorWindow.tsx) | [Assembled editor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-3) |
|
||||
| [src/components/video-editor/ExportSettingsMenu.tsx](../src/components/video-editor/ExportSettingsMenu.tsx) | [ExportSettingsMenu / MP4](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-586), [ExportSettingsMenu / GIF](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-642) |
|
||||
| [src/components/video-editor/ExtensionManager.tsx](../src/components/video-editor/ExtensionManager.tsx) | [ExtensionManager / Legacy](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-454) |
|
||||
| [src/components/video-editor/FormatSelector.tsx](../src/components/video-editor/FormatSelector.tsx) | [FormatSelector / MP4](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-472), [FormatSelector / GIF](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-489) |
|
||||
| [src/components/video-editor/GifOptionsPanel.tsx](../src/components/video-editor/GifOptionsPanel.tsx) | [GifOptionsPanel](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-519) |
|
||||
| [src/components/video-editor/KeyboardShortcutsHelp.tsx](../src/components/video-editor/KeyboardShortcutsHelp.tsx) | [Popup/keyboard-help](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-853) |
|
||||
| [src/components/video-editor/PlaybackControls.tsx](../src/components/video-editor/PlaybackControls.tsx) | [Timeline — PlaybackControls](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4126) |
|
||||
| [src/components/video-editor/ProjectBrowserDialog.tsx](../src/components/video-editor/ProjectBrowserDialog.tsx) | [App — Projects](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-407) |
|
||||
| [src/components/video-editor/SettingsPanel.tsx](../src/components/video-editor/SettingsPanel.tsx) | [Inspectors — all sections](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2) |
|
||||
| [src/components/video-editor/ShortcutsConfigDialog.tsx](../src/components/video-editor/ShortcutsConfigDialog.tsx) | [Popup/shortcuts](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-701) |
|
||||
| [src/components/video-editor/SliderControl.tsx](../src/components/video-editor/SliderControl.tsx) | [SliderControl](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-655) |
|
||||
| [src/components/video-editor/TutorialHelp.tsx](../src/components/video-editor/TutorialHelp.tsx) | [Popup/tutorial](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-556), [Popup/feedback](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-415), [Popup/keyboard-dialog](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-493), [Editor/Header](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-391) |
|
||||
| [src/components/video-editor/VideoEditor.tsx](../src/components/video-editor/VideoEditor.tsx) | [Assembled editor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-3) |
|
||||
| [src/components/video-editor/VideoPlayback.tsx](../src/components/video-editor/VideoPlayback.tsx) | [Editor/Preview](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-887), [Timeline — AnnotationOverlay](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4199) |
|
||||
| [src/components/video-editor/WallpaperGrid.tsx](../src/components/video-editor/WallpaperGrid.tsx) | [WallpaperGrid](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-713) |
|
||||
| [src/components/video-editor/WebcamCropControl.tsx](../src/components/video-editor/WebcamCropControl.tsx) | [WebcamCropControl](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-674) |
|
||||
| [src/components/video-editor/layout/CropEditorDialog.tsx](../src/components/video-editor/layout/CropEditorDialog.tsx) | [Popup/crop-dialog](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-731) |
|
||||
| [src/components/video-editor/layout/EditorDialogs.tsx](../src/components/video-editor/layout/EditorDialogs.tsx) | [Popups — Dialogs](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-281) |
|
||||
| [src/components/video-editor/layout/EditorExportMenu.tsx](../src/components/video-editor/layout/EditorExportMenu.tsx) | [Popups — Export](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4276) |
|
||||
| [src/components/video-editor/layout/EditorHeader.tsx](../src/components/video-editor/layout/EditorHeader.tsx) | [Editor/Header](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-391) |
|
||||
| [src/components/video-editor/layout/EditorLoadingSkeleton.tsx](../src/components/video-editor/layout/EditorLoadingSkeleton.tsx) | [Popup/loading](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-957) |
|
||||
| [src/components/video-editor/layout/EditorPresetMenu.tsx](../src/components/video-editor/layout/EditorPresetMenu.tsx) | [Popups — Presets & announcements](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4592) |
|
||||
| [src/components/video-editor/layout/EditorPreviewPanel.tsx](../src/components/video-editor/layout/EditorPreviewPanel.tsx) | [Editor/Preview](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-887), [Popups — Preview](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4988) |
|
||||
| [src/components/video-editor/layout/EditorShell.tsx](../src/components/video-editor/layout/EditorShell.tsx) | [Assembled editor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-3) |
|
||||
| [src/components/video-editor/layout/EditorSidebar.tsx](../src/components/video-editor/layout/EditorSidebar.tsx) | [Editor/Sidebar / Scene](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-630), [Inspectors — all sections](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2), [Tooltip / Sidebar placement](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4985) |
|
||||
| [src/components/video-editor/layout/EditorTimelinePanel.tsx](../src/components/video-editor/layout/EditorTimelinePanel.tsx) | [Editor/Timeline](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-986), [Timeline — TimelineEditor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3613) |
|
||||
| [src/components/video-editor/layout/EditorVideoPreview.tsx](../src/components/video-editor/layout/EditorVideoPreview.tsx) | [Editor/Preview](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-887) |
|
||||
| [src/components/video-editor/timeline/Item.tsx](../src/components/video-editor/timeline/Item.tsx) | [Timeline — TimelineEditor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3613) — clip, zoom, annotation, caption and audio blocks |
|
||||
| [src/components/video-editor/timeline/Row.tsx](../src/components/video-editor/timeline/Row.tsx) | [Timeline — TimelineEditor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3613) — nested row layers |
|
||||
| [src/components/video-editor/timeline/TimelineEditor.tsx](../src/components/video-editor/timeline/TimelineEditor.tsx) | [Timeline — TimelineEditor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3613) |
|
||||
| [src/components/video-editor/timeline/components/filmstrip/ClipFilmstrip.tsx](../src/components/video-editor/timeline/components/filmstrip/ClipFilmstrip.tsx) | [Timeline — TimelineEditor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3613) — nested filmstrip media frames |
|
||||
| [src/components/video-editor/timeline/components/markers/KeyframeMarkers.tsx](../src/components/video-editor/timeline/components/markers/KeyframeMarkers.tsx) | [Default marker](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=52-7), [Selected marker](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=52-9) |
|
||||
| [src/components/video-editor/timeline/components/playhead/PlaybackCursor.tsx](../src/components/video-editor/timeline/components/playhead/PlaybackCursor.tsx) | [Timeline — TimelineEditor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3613) — nested playhead layers |
|
||||
| [src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx](../src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx) | [Timeline — TimelineEditor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3613) — nested track viewport layers |
|
||||
| [src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx](../src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx) | [AudioWaveform / Normal](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=55-6), [AudioWaveform / Normalized](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=55-9) |
|
||||
| [src/components/video-editor/timeline/components/wrapper/TimelineWrapper.tsx](../src/components/video-editor/timeline/components/wrapper/TimelineWrapper.tsx) | [Timeline — TimelineEditor](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3613) — composition/context wrapper |
|
||||
| [src/contexts/I18nContext.tsx](../src/contexts/I18nContext.tsx) | Provider/bootstrap only; represented through its rendered children. |
|
||||
| [src/contexts/ShortcutsContext.tsx](../src/contexts/ShortcutsContext.tsx) | Provider/bootstrap only; represented through its rendered children. |
|
||||
| [src/contexts/ThemeContext.tsx](../src/contexts/ThemeContext.tsx) | Provider/bootstrap only; represented through its rendered children. |
|
||||
| [src/main.tsx](../src/main.tsx) | Provider/bootstrap only; represented through its rendered children. |
|
||||
|
||||
## Native component index
|
||||
|
||||
Each row is a Figma component definition. Button names include their containing style family to distinguish repeated size/state names.
|
||||
|
||||
### Icons
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Icon / Plus](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-5) | `16:5` |
|
||||
| [Icon / Trash](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-8) | `16:8` |
|
||||
| [Icon / Play](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-11) | `16:11` |
|
||||
| [Icon / Pause](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-14) | `16:14` |
|
||||
| [Icon / Scissors](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-17) | `16:17` |
|
||||
| [Icon / Volume2](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-20) | `16:20` |
|
||||
| [Icon / Undo2](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-23) | `16:23` |
|
||||
| [Icon / Redo2](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-26) | `16:26` |
|
||||
| [Icon / ChevronDown](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-29) | `16:29` |
|
||||
| [Icon / Settings2](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-32) | `16:32` |
|
||||
| [Icon / MousePointer](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-35) | `16:35` |
|
||||
| [Icon / Type](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-38) | `16:38` |
|
||||
| [Icon / Camera](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-41) | `16:41` |
|
||||
| [Icon / Image](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-44) | `16:44` |
|
||||
| [Icon / Download](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=16-47) | `16:47` |
|
||||
|
||||
### Button
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [default / Size=sm, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-20) | `19:20` |
|
||||
| [default / Size=sm, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-23) | `19:23` |
|
||||
| [default / Size=default, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-26) | `19:26` |
|
||||
| [default / Size=default, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-29) | `19:29` |
|
||||
| [default / Size=lg, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-32) | `19:32` |
|
||||
| [default / Size=lg, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-35) | `19:35` |
|
||||
| [default / Size=icon, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-40) | `19:40` |
|
||||
| [default / Size=icon, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-45) | `19:45` |
|
||||
| [secondary / Size=sm, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-48) | `19:48` |
|
||||
| [secondary / Size=sm, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-51) | `19:51` |
|
||||
| [secondary / Size=default, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-54) | `19:54` |
|
||||
| [secondary / Size=default, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-57) | `19:57` |
|
||||
| [secondary / Size=lg, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-60) | `19:60` |
|
||||
| [secondary / Size=lg, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-63) | `19:63` |
|
||||
| [secondary / Size=icon, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-68) | `19:68` |
|
||||
| [secondary / Size=icon, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-73) | `19:73` |
|
||||
| [outline / Size=sm, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-76) | `19:76` |
|
||||
| [outline / Size=sm, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-79) | `19:79` |
|
||||
| [outline / Size=default, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-82) | `19:82` |
|
||||
| [outline / Size=default, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-85) | `19:85` |
|
||||
| [outline / Size=lg, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-88) | `19:88` |
|
||||
| [outline / Size=lg, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-91) | `19:91` |
|
||||
| [outline / Size=icon, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-96) | `19:96` |
|
||||
| [outline / Size=icon, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-101) | `19:101` |
|
||||
| [ghost / Size=sm, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-104) | `19:104` |
|
||||
| [ghost / Size=sm, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-107) | `19:107` |
|
||||
| [ghost / Size=default, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-110) | `19:110` |
|
||||
| [ghost / Size=default, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-113) | `19:113` |
|
||||
| [ghost / Size=lg, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-116) | `19:116` |
|
||||
| [ghost / Size=lg, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-119) | `19:119` |
|
||||
| [ghost / Size=icon, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-124) | `19:124` |
|
||||
| [ghost / Size=icon, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-129) | `19:129` |
|
||||
| [destructive / Size=sm, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-132) | `19:132` |
|
||||
| [destructive / Size=sm, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-135) | `19:135` |
|
||||
| [destructive / Size=default, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-138) | `19:138` |
|
||||
| [destructive / Size=default, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-141) | `19:141` |
|
||||
| [destructive / Size=lg, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-144) | `19:144` |
|
||||
| [destructive / Size=lg, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-147) | `19:147` |
|
||||
| [destructive / Size=icon, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-152) | `19:152` |
|
||||
| [destructive / Size=icon, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-157) | `19:157` |
|
||||
| [destructive-soft / Size=sm, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-160) | `19:160` |
|
||||
| [destructive-soft / Size=sm, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-163) | `19:163` |
|
||||
| [destructive-soft / Size=default, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-166) | `19:166` |
|
||||
| [destructive-soft / Size=default, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-169) | `19:169` |
|
||||
| [destructive-soft / Size=lg, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-172) | `19:172` |
|
||||
| [destructive-soft / Size=lg, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-175) | `19:175` |
|
||||
| [destructive-soft / Size=icon, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-180) | `19:180` |
|
||||
| [destructive-soft / Size=icon, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-185) | `19:185` |
|
||||
| [link / Size=sm, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-188) | `19:188` |
|
||||
| [link / Size=sm, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-191) | `19:191` |
|
||||
| [link / Size=default, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-194) | `19:194` |
|
||||
| [link / Size=default, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-197) | `19:197` |
|
||||
| [link / Size=lg, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-200) | `19:200` |
|
||||
| [link / Size=lg, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-203) | `19:203` |
|
||||
| [link / Size=icon, State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-208) | `19:208` |
|
||||
| [link / Size=icon, State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=19-213) | `19:213` |
|
||||
|
||||
### Input
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=22-5) | `22:5` |
|
||||
| [State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=22-8) | `22:8` |
|
||||
| [State=Filled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=22-11) | `22:11` |
|
||||
|
||||
### TextArea
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [TextArea](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=22-30) | `22:30` |
|
||||
|
||||
### Switch
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [State=Off](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=23-8) | `23:8` |
|
||||
| [State=On](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=23-14) | `23:14` |
|
||||
| [State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=23-20) | `23:20` |
|
||||
|
||||
### Slider
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [State=Single](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=24-8) | `24:8` |
|
||||
| [State=Range](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=24-16) | `24:16` |
|
||||
| [State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=24-22) | `24:22` |
|
||||
|
||||
### Toggle
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-5) | `28:5` |
|
||||
| [State=Selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-8) | `28:8` |
|
||||
| [State=Disabled](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-11) | `28:11` |
|
||||
|
||||
### ToggleGroup
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [ToggleGroup](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-35) | `28:35` |
|
||||
|
||||
### ChoiceGroup
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [ChoiceGroup](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-44) | `28:44` |
|
||||
|
||||
### Tabs
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Tabs](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-57) | `28:57` |
|
||||
|
||||
### Select
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Select](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=28-64) | `28:64` |
|
||||
|
||||
### Card
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Card](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-14) | `30:14` |
|
||||
|
||||
### Accordion
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [State=Expanded](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-25) | `30:25` |
|
||||
| [State=Collapsed](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-31) | `30:31` |
|
||||
|
||||
### Label
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Label](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-50) | `30:50` |
|
||||
|
||||
### Separator
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Separator](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-53) | `30:53` |
|
||||
|
||||
### Skeleton
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Skeleton](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=30-57) | `30:57` |
|
||||
|
||||
### App — Shared utilities
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [ColorPalette](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-368) | `35:368` |
|
||||
| [ColorControl](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-379) | `35:379` |
|
||||
| [ContentClamp](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-383) | `35:383` |
|
||||
| [ItemContent](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-390) | `35:390` |
|
||||
| [AudioLevelMeter / 12](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-395) | `35:395` |
|
||||
| [AudioLevelMeter / 72](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-400) | `35:400` |
|
||||
| [AudioLevelMeter / 94](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-405) | `35:405` |
|
||||
|
||||
### App — Projects
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [ProjectBrowserDialog / Populated](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-432) | `35:432` |
|
||||
| [ProjectBrowserDialog / Empty](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-443) | `35:443` |
|
||||
| [ExtensionManager / Legacy](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-454) | `35:454` |
|
||||
|
||||
### App — Export controls
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [FormatSelector / MP4](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-472) | `35:472` |
|
||||
| [FormatSelector / GIF](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-489) | `35:489` |
|
||||
| [GifOptionsPanel](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-519) | `35:519` |
|
||||
| [ExportSettingsMenu / MP4](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-586) | `35:586` |
|
||||
| [ExportSettingsMenu / GIF](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-642) | `35:642` |
|
||||
|
||||
### App — Preview controls
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [SliderControl](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-655) | `35:655` |
|
||||
| [WebcamCropControl](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-674) | `35:674` |
|
||||
| [CropControl](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-690) | `35:690` |
|
||||
| [WallpaperGrid](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-713) | `35:713` |
|
||||
| [CaptionListPanel / Editing](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-748) | `35:748` |
|
||||
|
||||
### App — Annotation controls
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [AnnotationSettingsPanel / text](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-879) | `35:879` |
|
||||
| [AnnotationSettingsPanel / figure](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-986) | `35:986` |
|
||||
| [AnnotationSettingsPanel / blur](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1098) | `35:1098` |
|
||||
| [AnnotationSettingsPanel / image](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1159) | `35:1159` |
|
||||
|
||||
### App — Recording & sources
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [RecordingControls / Recording](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1191) | `35:1191` |
|
||||
| [RecordingControls / Paused](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1222) | `35:1222` |
|
||||
| [SourceSelectorContent / Sources](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1255) | `35:1255` |
|
||||
| [SourceSelectorContent / Empty](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1260) | `35:1260` |
|
||||
| [SourceSelectorContent / Loading](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1265) | `35:1265` |
|
||||
| [DropdownItem / Selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1280) | `35:1280` |
|
||||
| [MicDeviceRow / Selected and unselected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1301) | `35:1301` |
|
||||
| [MarqueeText](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=35-1306) | `35:1306` |
|
||||
|
||||
### Popups — Recording
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Popup/launch-mic-devices](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-37) | `38:37` |
|
||||
| [Popup/launch-webcam-devices](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-67) | `38:67` |
|
||||
| [Popup/launch-countdown](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-97) | `38:97` |
|
||||
| [Popup/launch-mic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-113) | `38:113` |
|
||||
| [Popup/launch-webcam](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-122) | `38:122` |
|
||||
| [Popup/launch-more](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-236) | `38:236` |
|
||||
| [Popup/launch-sources](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-255) | `38:255` |
|
||||
| [Popup/launch-projects](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-279) | `38:279` |
|
||||
|
||||
### Popups — Dialogs
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Popup/project-save](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-307) | `38:307` |
|
||||
| [Popup/project-saving](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-334) | `38:334` |
|
||||
| [Popup/unsaved-changes](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-353) | `38:353` |
|
||||
| [Popup/native-capture-unavailable](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-368) | `38:368` |
|
||||
| [Popup/feedback](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-415) | `38:415` |
|
||||
| [Popup/keyboard-dialog](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-493) | `38:493` |
|
||||
| [Popup/tutorial](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-556) | `38:556` |
|
||||
| [Popup/dialog](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-573) | `38:573` |
|
||||
| [Popup/font](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-605) | `38:605` |
|
||||
| [Popup/shortcuts](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-701) | `38:701` |
|
||||
| [Popup/crop-dialog](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-731) | `38:731` |
|
||||
|
||||
### Popups — Controls & feedback
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Popup/select](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-759) | `38:759` |
|
||||
| [Popup/popover](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-769) | `38:769` |
|
||||
| [Popup/dropdown](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-783) | `38:783` |
|
||||
| [Popup/toast](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-794) | `38:794` |
|
||||
| [Popup/keyboard-help](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-853) | `38:853` |
|
||||
| [Popup/color](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-886) | `38:886` |
|
||||
| [Popup/loading](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=38-957) | `38:957` |
|
||||
|
||||
### Inspectors — all sections
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Inspector/scene / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-116) | `41:116` |
|
||||
| [Inspector/scene / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-283) | `41:283` |
|
||||
| [Inspector/frame / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-398) | `41:398` |
|
||||
| [Inspector/frame / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-565) | `41:565` |
|
||||
| [Inspector/crop / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-680) | `41:680` |
|
||||
| [Inspector/crop / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-847) | `41:847` |
|
||||
| [Inspector/cursor / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-932) | `41:932` |
|
||||
| [Inspector/cursor / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1115) | `41:1115` |
|
||||
| [Inspector/captions / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1177) | `41:1177` |
|
||||
| [Inspector/captions / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1300) | `41:1300` |
|
||||
| [Inspector/caption / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1337) | `41:1337` |
|
||||
| [Inspector/caption / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1374) | `41:1374` |
|
||||
| [Inspector/webcam / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1451) | `41:1451` |
|
||||
| [Inspector/webcam / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1600) | `41:1600` |
|
||||
| [Inspector/settings / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1668) | `41:1668` |
|
||||
| [Inspector/settings / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1839) | `41:1839` |
|
||||
| [Inspector/zoom / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1878) | `41:1878` |
|
||||
| [Inspector/zoom / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1930) | `41:1930` |
|
||||
| [Inspector/clip / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1957) | `41:1957` |
|
||||
| [Inspector/clip / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-1984) | `41:1984` |
|
||||
| [Inspector/audio / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2016) | `41:2016` |
|
||||
| [Inspector/audio / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2048) | `41:2048` |
|
||||
| [Inspector/extensions / Basic](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2163) | `41:2163` |
|
||||
| [Inspector/extensions / Advanced](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2330) | `41:2330` |
|
||||
| [Inspector/Background / Image](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2444) | `41:2444` |
|
||||
| [Inspector/Background / Video](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2497) | `41:2497` |
|
||||
| [Inspector/Background / Color](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2575) | `41:2575` |
|
||||
| [Inspector/Background / Gradient](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2661) | `41:2661` |
|
||||
| [Inspector/Zoom / Manual](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2713) | `41:2713` |
|
||||
| [Inspector/Clip / Muted and speed override](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2742) | `41:2742` |
|
||||
| [Inspector/Captions / Model unavailable](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2804) | `41:2804` |
|
||||
| [Inspector/Captions / Downloading model](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2868) | `41:2868` |
|
||||
| [Inspector/Captions / Model ready](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2930) | `41:2930` |
|
||||
| [Inspector/Captions / Generating](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-2998) | `41:2998` |
|
||||
| [Inspector/Captions / Download error](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3060) | `41:3060` |
|
||||
| [Inspector/Webcam / Empty](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3131) | `41:3131` |
|
||||
| [Inspector/Cursor / Hidden](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3216) | `41:3216` |
|
||||
| [Inspector/Zoom / No selection](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3220) | `41:3220` |
|
||||
| [Inspector/Clip / No selection](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3240) | `41:3240` |
|
||||
| [Inspector/Audio / No selection](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3265) | `41:3265` |
|
||||
|
||||
### Timeline — TimelineEditor
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [TimelineEditor / clip selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3712) | `41:3712` |
|
||||
| [TimelineEditor / zoom selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3812) | `41:3812` |
|
||||
| [TimelineEditor / annotation selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-3912) | `41:3912` |
|
||||
| [TimelineEditor / audio selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4012) | `41:4012` |
|
||||
| [TimelineEditor / caption selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4112) | `41:4112` |
|
||||
| [TimelineEditor / Empty](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4124) | `41:4124` |
|
||||
|
||||
### Timeline — PlaybackControls
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [PlaybackControls / Paused](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4149) | `41:4149` |
|
||||
| [PlaybackControls / Playing](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4173) | `41:4173` |
|
||||
| [PlaybackControls / Muted](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4197) | `41:4197` |
|
||||
|
||||
### Timeline — AnnotationOverlay
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [AnnotationOverlay / text / Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4205) | `41:4205` |
|
||||
| [AnnotationOverlay / text / Selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4219) | `41:4219` |
|
||||
| [AnnotationOverlay / figure / Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4226) | `41:4226` |
|
||||
| [AnnotationOverlay / figure / Selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4240) | `41:4240` |
|
||||
| [AnnotationOverlay / image / Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4245) | `41:4245` |
|
||||
| [AnnotationOverlay / image / Selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4257) | `41:4257` |
|
||||
| [AnnotationOverlay / blur / Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4262) | `41:4262` |
|
||||
| [AnnotationOverlay / blur / Selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4274) | `41:4274` |
|
||||
|
||||
### Popups — Export
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Popup/export-settings-mp4](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4357) | `41:4357` |
|
||||
| [Popup/export-settings-gif](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4413) | `41:4413` |
|
||||
| [Popup/export-preparing](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4431) | `41:4431` |
|
||||
| [Popup/export-rendering](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4452) | `41:4452` |
|
||||
| [Popup/export-audio](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4473) | `41:4473` |
|
||||
| [Popup/export-finalizing](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4491) | `41:4491` |
|
||||
| [Popup/export-muxing](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4509) | `41:4509` |
|
||||
| [Popup/export-saving](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4527) | `41:4527` |
|
||||
| [Popup/export-success](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4541) | `41:4541` |
|
||||
| [Popup/export-error](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4553) | `41:4553` |
|
||||
| [Popup/export-pending-save](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4567) | `41:4567` |
|
||||
| [Popup/export-legacy](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4590) | `41:4590` |
|
||||
|
||||
### Popups — Presets & announcements
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Popup/presets-empty](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4611) | `41:4611` |
|
||||
| [Popup/presets-saved](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4649) | `41:4649` |
|
||||
| [Popup/announcement-popup](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4683) | `41:4683` |
|
||||
| [Popup/announcement-banner](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4701) | `41:4701` |
|
||||
| [Popup/announcement-notification](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4713) | `41:4713` |
|
||||
| [Popup/toolbar-aspect-ratio](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4757) | `41:4757` |
|
||||
| [Popup/announcement-cover](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=51-37) | `51:37` |
|
||||
|
||||
### Windows & tooltips
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [UpdateToastWindow / Available](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4778) | `41:4778` |
|
||||
| [UpdateToastWindow / Downloading](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4803) | `41:4803` |
|
||||
| [UpdateToastWindow / Ready](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4823) | `41:4823` |
|
||||
| [UpdateToastWindow / Check error](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4843) | `41:4843` |
|
||||
| [UpdateToastWindow / Download error](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4863) | `41:4863` |
|
||||
| [UpdateToastWindow / Experimental](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4886) | `41:4886` |
|
||||
| [UpdateToastWindow / Preview](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4909) | `41:4909` |
|
||||
| [CountdownOverlay / 3 seconds](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4916) | `41:4916` |
|
||||
| [HudWindow / Idle](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4968) | `41:4968` |
|
||||
| [App / Startup](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4977) | `41:4977` |
|
||||
| [Tooltip / Content clamp](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4981) | `41:4981` |
|
||||
| [Tooltip / Sidebar placement](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-4985) | `41:4985` |
|
||||
|
||||
### Popups — Preview
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Popup/aspect-ratio](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-5017) | `41:5017` |
|
||||
| [Popup/add-layer](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-5025) | `41:5025` |
|
||||
| [Popup/preview-volume](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=41-5034) | `41:5034` |
|
||||
|
||||
### HUD — recording states
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [HUD / Finalizing recording](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=46-18) | `46:18` |
|
||||
| [HUD / Floating webcam preview](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=46-23) | `46:23` |
|
||||
| [HUD / Recording](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=46-61) | `46:61` |
|
||||
|
||||
### Timeline — KeyframeMarkers
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [State=Default](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=52-7) | `52:7` |
|
||||
| [State=Selected](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=52-9) | `52:9` |
|
||||
|
||||
### Timeline — AudioWaveform
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [AudioWaveform / Normal](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=55-6) | `55:6` |
|
||||
| [AudioWaveform / Normalized](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=55-9) | `55:9` |
|
||||
|
||||
### Editor — shell & screen
|
||||
|
||||
| Component | Node |
|
||||
| --- | --- |
|
||||
| [Editor/Header](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-391) | `32:391` |
|
||||
| [Editor/Sidebar / Scene](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-630) | `32:630` |
|
||||
| [Editor/Preview](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-887) | `32:887` |
|
||||
| [Editor/Timeline](https://www.figma.com/design/bE2Ew2nF1Zd0ixci9SwJDm?node-id=32-986) | `32:986` |
|
||||
|
||||
## Reproducible capture fixtures
|
||||
|
||||
| Entry | Coverage |
|
||||
| --- | --- |
|
||||
| `design-library.html` | Primitive controls and variants |
|
||||
| `design-capture.html?windowType=editor` | Actual assembled renderer |
|
||||
| `design-app-catalog.html` | Shared app controls, source lists and annotation inspectors |
|
||||
| `design-app-catalog.html?view=all-states` | 26 popup and dialog states |
|
||||
| `design-inspector-catalog.html` | 40 inspector/background states |
|
||||
| `design-timeline-catalog.html` | Timeline, playback and annotation overlays |
|
||||
| `design-timeline-details.html` | Explicit waveform peaks and keyframe markers |
|
||||
| `design-extra-catalog.html` | Export, preset and announcement states; legacy toolbar |
|
||||
| `design-extra-catalog.html?view=announcement-cover` | Cover-media announcement |
|
||||
| `design-preview-menus.html` | Aspect ratio, Add Layer and volume menus |
|
||||
| `design-window-catalog.html` | Seven update states, countdown, idle HUD, startup and tooltips |
|
||||
| `design-hud-branches.html` | Finalizing recorder and floating webcam/recording HUD |
|
||||
| `design-window-capture.html` | Individual window state verification through the live bridge path |
|
||||
|
||||
The capture entry points load the existing test bridge and fixture media. The renderer-only development server must be running to use them. They are separate from the production `index.html` entry.
|
||||
|
||||
## Validation
|
||||
|
||||
- TypeScript and targeted lint pass for source changes and all capture fixtures.
|
||||
- Timeline/deletion changes: 135 focused unit tests and 13 browser regressions passed.
|
||||
- Controlled and live update-window paths were rendered successfully; the window/detail galleries reported no JavaScript errors.
|
||||
- Figma audit checks native component/layer counts, variable aliases and fonts. Visual QA corrected capture artifacts in slider borders, tooltip sizing and popup envelopes. Raw and failed intermediate capture frames were removed after conversion.
|
||||
|
||||
Compatibility toolbar, unmounted ruler and marker-overlay source files were removed on 20 September 2026. Their earlier Figma captures are historical, not current app surfaces.
|
||||
@@ -0,0 +1,65 @@
|
||||
# HeroUI editor redundancy audit
|
||||
|
||||
Reviewed 19 September 2026 in the `recordly-heroui` redesign. Findings use the current renderer and source, including the existing uncommitted redesign work.
|
||||
|
||||
## Cleaned up
|
||||
|
||||
| Area | Problem | Change |
|
||||
| --- | --- | --- |
|
||||
| Zoom inspector | Two Delete Zoom buttons for the same selected block: one in the scrolling content, another in the fixed footer. | Kept the fixed footer action. |
|
||||
| Extensions navigation | A permanent tool tab leads only to “Extensions are no longer available.” | Removed the navigation entry; retained the existing compatibility component. |
|
||||
| Account navigation | A permanent button only shows “Account coming soon.” | Removed the placeholder button. |
|
||||
| Clip and Caption inspectors | The panel title is repeated immediately as a section heading without introducing a subsection. | Kept the panel title and removed the repeated heading. |
|
||||
| Basic Zoom inspector | Animation heading, Reset button and directions to Settings appear without an animation control. The actual control is Advanced-only. | Grouped these with Classic Animation under Advanced. |
|
||||
|
||||
## Further consolidation opportunities
|
||||
|
||||
These are recommendations, not additional changes in this patch.
|
||||
|
||||
1. **Crop has two separate editing surfaces.** The toolbar opens the visual Crop Video dialog; Scene → Advanced offers four inset sliders for the same crop state. Put precise inset inputs and reset inside the crop dialog so there is one place to edit cropping. Preserve numeric precision when consolidating.
|
||||
2. **Zoom controls are scattered across two inspectors.** The block inspector contains Mode, Amount and Classic Animation; Settings contains Connect neighboring zooms, automatic suggestions and Motion Presets. Group motion settings in a clearly labelled project-wide subsection alongside Zoom controls, instead of a message directing users elsewhere. Some motion presets also affect the cursor, so their scope must stay explicit.
|
||||
3. **Adding content is split between Add Layer and adjacent buttons.** Add Layer offers annotation/audio, while Add Zoom is a separate icon. A single Add menu could enumerate all supported insertions; keep Split separate as an editing operation. Hover-to-add is useful direct manipulation and need not be removed.
|
||||
4. **Support commands occupy permanent editing-toolbar space.** Discord and Feedback sit beside project opening and Undo/Redo. A single Help menu could contain both plus shortcut help. This is lower-priority toolbar clutter, not two identical functions.
|
||||
5. **Audio repeats its inspector title.** An “Audio” heading inside the Audio inspector mainly provides a home for Reset. Move Reset to the inspector header when standardizing section actions.
|
||||
6. **Saved Presets and Motion Presets need clearer scope.** The header stores whole-editor presets; Settings applies motion presets. These are different functions, but the repeated naming obscures that distinction. Label the former “Style presets” and the latter “Motion” or explain their scope.
|
||||
|
||||
Removed the unused compatibility timeline toolbar and its design-catalog fixture, plus the unmounted TimelineAxis and ClipMarkerOverlay components. Removed ignored SliderControl props and their call-site values.
|
||||
|
||||
## Interaction defects fixed
|
||||
|
||||
- Connected `handleClipDelete` to `TimelineEditor.onClipDelete`. Previously Backspace reached a no-op because the callback was absent.
|
||||
- Routed Delete, Backspace and the configured delete shortcut consistently for every selected block type before the timeline-focus check. Inspecting a block no longer makes it undeletable. Form controls, editable text, composition events and overlay controls retain their own keyboard behavior.
|
||||
- Removed the clip-only Backspace special case, which bypassed shared selection priority.
|
||||
- Preserved a selected caption fragment when the playhead leaves its time span. Deletion still maps the fragment to its source caption cue.
|
||||
- Removed unnecessary requirements for optional selection callbacks from clip, annotation and audio deletion.
|
||||
- Cleared stale keyframe selection when selecting a timeline block.
|
||||
- Cleared block selection when navigating to a global sidebar tool. A selected annotation previously overrode every tool's inspector, making Cursor, Scene, Webcam and Settings appear to show the same content.
|
||||
|
||||
## Validation
|
||||
|
||||
Passed: TypeScript checking, 72 focused unit tests, 14 browser tests, targeted lint, and diff whitespace checks.
|
||||
|
||||
Regression coverage includes keyboard deletion for each block type, inspector deletion, clip undo, caption deletion after seeking, text-entry protection, and switching away from an annotation inspector. Unit coverage includes both delete keys, configured shortcuts, selection priority, editable/overlay guards and composition/modifier handling. Browser tests use the real renderer with a mocked Electron bridge and fixture media; packaged Electron is not exercised here.
|
||||
|
||||
## Videos library and HUD follow-up
|
||||
|
||||
- HUD source/device rows and source/region triggers use ghost buttons. Project cards fit the 300px HUD popup, scroll vertically with thin scrollbars, and no longer truncate the library at 24 entries.
|
||||
- Caption generation skips session re-registration when the source is unchanged, preserving active companion-audio approvals. Local media URLs resolve the current server port; preview retries a failed local load once through the existing approved-media API.
|
||||
- Videos uses a secondary header button at the far left and replaces the existing left inspector with lightweight file cards (no embedded video players). It lists files in the configured recordings directory. Removal sends the selected recordings and their capture companions to system Trash. Session undo restores from a temporary recovery copy without overwriting newer files. This replaces the earlier hide-only behavior. Automatic recording age/count pruning is removed.
|
||||
- Timeline drops insert at a clip boundary. Since preview/export share a single source, imports prepare an immutable combined source in `.recordly-media`. New footage fits the current source canvas/frame rate; subsequent imports copy existing prepared video, with lossless system/mic companions and shifted cursor telemetry. Preparation takes time for large recordings. Clip source bounds prevent trimming into the next recording. Timeline history can undo/redo insertions because existing source offsets are retained.
|
||||
- The existing FFmpeg metadata parser is now shared by import and export, rather than duplicating it or depending on the incompatible FFprobe binary found in this development install.
|
||||
|
||||
Validation: real FFmpeg integration covers differing dimensions, audio routing, repeated inserts, and original preservation. Browser tests cover library removal/undo, drag insertion/undo/redo, caption generation without session reset, preview recovery, and 30 projects in the HUD popup. Caption visibility at 1x/2x/4x and gap snapping remain covered.
|
||||
|
||||
|
||||
## Clips, overlays, and cloud integration
|
||||
|
||||
- The Videos inspector uses shared search, checkbox, menu, and button controls. File rows support selection, double-click/+ insertion, and drag-to-timeline; bulk Trash is in the actions menu. No library video elements are loaded.
|
||||
- Webcam imports normalize and concatenate a separate webcam track, account for capture start offsets, and preserve source-time visibility ranges. Preview and both renderer paths hide the bubble in screen-only spans. Saved projects preserve these ranges; style presets omit them and preserve the current source's ranges when applied.
|
||||
- Imported click, mouse-up, right-click, and cursor-shape samples retain event types, shifted timestamps, and letterbox-adjusted coordinates.
|
||||
- Annotation drag bounds now use the complete canvas. Position percentages may extend outside the recording rectangle and survive save/reload; export uses the same coordinates. Webcam and caption overlays already use their own canvas layout.
|
||||
- Zoom blocks show the icon with the full label when space allows; narrow blocks hide the icon and mode while retaining the numeric multiplier.
|
||||
- Cloud integration was ported from the cloud worktree, including its current service/viewer edits, desktop authentication, multipart uploads, and preparation of the current edit for sharing. Account lives at the bottom of the tool rail; Create share link lives in Export. Nothing was deployed or uploaded. Live sign-in requires the public Supabase environment settings described in authentication.md.
|
||||
|
||||
|
||||
Thumbnail and sign-in refinement: Videos uses lazily requested 160×90 JPEG stills, cached by source path, modification time, and size. It displays unmodified filenames, including extensions, with ellipsis and the full hover title. Sign-in uses HeroUI Modal, Form, TextField, Input, Button, and Alert components with shared styling.
|
||||
@@ -0,0 +1,799 @@
|
||||
import { FolderOpen, Microphone, Timer } from "@phosphor-icons/react";
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { UNSAFE_PortalProvider } from "react-aria";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import "./components/launch/launchTheme.css";
|
||||
import { HudInteractionContext } from "./components/launch/contexts/HudInteractionContext";
|
||||
import { MarqueeText } from "./components/launch/MarqueeText";
|
||||
import { CountdownPopover } from "./components/launch/popovers/CountdownPopover";
|
||||
import {
|
||||
LaunchPopoverCoordinatorProvider,
|
||||
useLaunchPopoverCoordinator,
|
||||
} from "./components/launch/popovers/LaunchPopoverCoordinator";
|
||||
import { MicPopover } from "./components/launch/popovers/MicPopover";
|
||||
import { MorePopover } from "./components/launch/popovers/MorePopover";
|
||||
import { DropdownItem, MicDeviceRow } from "./components/launch/popovers/PopoverScaffold";
|
||||
import { ProjectPopover } from "./components/launch/popovers/ProjectPopover";
|
||||
import { SourcePopover } from "./components/launch/popovers/SourcePopover";
|
||||
import { WebcamPopover } from "./components/launch/popovers/WebcamPopover";
|
||||
import { RecordingControls } from "./components/launch/RecordingControls";
|
||||
import { SourceSelectorContent } from "./components/launch/SourceSelector";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
} from "./components/ui/select";
|
||||
import { AudioLevelMeter } from "./components/ui/audio-level-meter";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { ColorControl, ColorPalette } from "./components/ui/color-picker";
|
||||
import { ContentClamp } from "./components/ui/content-clamp";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "./components/ui/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "./components/ui/dropdown-menu";
|
||||
import ItemContent from "./components/ui/item-content";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./components/ui/popover";
|
||||
import { Toaster, toast } from "./components/ui/toast";
|
||||
import { AddCustomFontDialog } from "./components/video-editor/AddCustomFontDialog";
|
||||
import { AnnotationSettingsPanel } from "./components/video-editor/AnnotationSettingsPanel";
|
||||
import CaptionListPanel from "./components/video-editor/CaptionListPanel";
|
||||
import { CropControl } from "./components/video-editor/CropControl";
|
||||
import { ExportSettingsMenu } from "./components/video-editor/ExportSettingsMenu";
|
||||
import ExtensionManager from "./components/video-editor/ExtensionManager";
|
||||
import { FormatSelector } from "./components/video-editor/FormatSelector";
|
||||
import { GifOptionsPanel } from "./components/video-editor/GifOptionsPanel";
|
||||
import { KeyboardShortcutsHelp } from "./components/video-editor/KeyboardShortcutsHelp";
|
||||
import { CropEditorDialog } from "./components/video-editor/layout/CropEditorDialog";
|
||||
import { EditorDialogs } from "./components/video-editor/layout/EditorDialogs";
|
||||
import {
|
||||
FeedbackDialog,
|
||||
KeyboardShortcutsDialog,
|
||||
TutorialHelp,
|
||||
} from "./components/video-editor/TutorialHelp";
|
||||
import { EditorLoadingSkeleton } from "./components/video-editor/layout/EditorLoadingSkeleton";
|
||||
import ProjectBrowserDialog from "./components/video-editor/ProjectBrowserDialog";
|
||||
import { ShortcutsConfigDialog } from "./components/video-editor/ShortcutsConfigDialog";
|
||||
import { SliderControl } from "./components/video-editor/SliderControl";
|
||||
import {
|
||||
type AnnotationType,
|
||||
DEFAULT_ANNOTATION_POSITION,
|
||||
DEFAULT_ANNOTATION_SIZE,
|
||||
DEFAULT_ANNOTATION_STYLE,
|
||||
DEFAULT_FIGURE_DATA,
|
||||
} from "./components/video-editor/types";
|
||||
import { WallpaperGrid } from "./components/video-editor/WallpaperGrid";
|
||||
import { WebcamCropControl } from "./components/video-editor/WebcamCropControl";
|
||||
import { I18nProvider, useI18n } from "./contexts/I18nContext";
|
||||
import { ShortcutsProvider, useShortcuts } from "./contexts/ShortcutsContext";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
|
||||
const noop = () => undefined;
|
||||
const view = new URLSearchParams(location.search).get("view") ?? "gallery";
|
||||
const palette = ["#2563eb", "#a855f7", "#f43f5e", "#f59e0b", "#10b981", "#ffffff", "#171717"];
|
||||
const entries = [
|
||||
{
|
||||
path: "/design-fixtures/product-demo.recordly",
|
||||
name: "Product demo",
|
||||
updatedAt: 1789800000000,
|
||||
thumbnailPath: null,
|
||||
isCurrent: true,
|
||||
isInProjectsDirectory: true,
|
||||
},
|
||||
{
|
||||
path: "/design-fixtures/onboarding.recordly",
|
||||
name: "Onboarding walkthrough",
|
||||
updatedAt: 1789710000000,
|
||||
thumbnailPath: null,
|
||||
isCurrent: false,
|
||||
isInProjectsDirectory: true,
|
||||
},
|
||||
];
|
||||
const screenSources = [
|
||||
{
|
||||
id: "screen:1:0",
|
||||
name: "Built-in Display",
|
||||
thumbnail: null,
|
||||
display_id: "1",
|
||||
appIcon: null,
|
||||
sourceType: "screen" as const,
|
||||
},
|
||||
];
|
||||
const windowSources = [
|
||||
{
|
||||
id: "window:1:0",
|
||||
name: "Product walkthrough",
|
||||
thumbnail: null,
|
||||
display_id: "1",
|
||||
appIcon: null,
|
||||
sourceType: "window" as const,
|
||||
appName: "Browser",
|
||||
},
|
||||
];
|
||||
|
||||
function Sample({
|
||||
name,
|
||||
children,
|
||||
width = 396,
|
||||
}: {
|
||||
name: string;
|
||||
children: ReactNode;
|
||||
width?: number;
|
||||
}) {
|
||||
return (
|
||||
<article
|
||||
data-figma-name={`App / ${name}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
padding: 24,
|
||||
width,
|
||||
background: "var(--surface)",
|
||||
borderRadius: 12,
|
||||
alignSelf: "flex-start",
|
||||
}}
|
||||
>
|
||||
<h3 style={{ fontSize: 13, color: "var(--muted)", fontWeight: 600 }}>{name}</h3>
|
||||
<div data-figma-name={`component/${name}`}>{children}</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
function Section({ name, children }: { name: string; children: ReactNode }) {
|
||||
return (
|
||||
<section
|
||||
data-figma-name={name}
|
||||
style={{ display: "flex", flexDirection: "column", gap: 24 }}
|
||||
>
|
||||
<h2 style={{ fontSize: 24, fontWeight: 600 }}>{name}</h2>
|
||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 24, alignItems: "flex-start" }}>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
function ExportSample({ format }: { format: "mp4" | "gif" }) {
|
||||
return (
|
||||
<ExportSettingsMenu
|
||||
exportFormat={format}
|
||||
exportQuality="good"
|
||||
exportEncodingMode="balanced"
|
||||
mp4FrameRate={30}
|
||||
gifFrameRate={20}
|
||||
gifLoop
|
||||
gifSizePreset="medium"
|
||||
gifOutputDimensions={{ width: 960, height: 540 }}
|
||||
showCaptionSidecarOption
|
||||
includeCaptionSidecar
|
||||
onExport={noop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function AnnotationSample({ type }: { type: AnnotationType }) {
|
||||
return (
|
||||
<AnnotationSettingsPanel
|
||||
annotation={{
|
||||
id: `catalog-${type}`,
|
||||
type,
|
||||
startMs: 0,
|
||||
endMs: 3000,
|
||||
content: type === "text" ? "Your next great idea" : "",
|
||||
position: DEFAULT_ANNOTATION_POSITION,
|
||||
size: DEFAULT_ANNOTATION_SIZE,
|
||||
style: DEFAULT_ANNOTATION_STYLE,
|
||||
zIndex: 1,
|
||||
figureData: DEFAULT_FIGURE_DATA,
|
||||
blurIntensity: 20,
|
||||
blurColor: "#000000",
|
||||
}}
|
||||
onContentChange={noop}
|
||||
onTypeChange={noop}
|
||||
onStyleChange={noop}
|
||||
onFigureDataChange={noop}
|
||||
onBlurIntensityChange={noop}
|
||||
onBlurColorChange={noop}
|
||||
onDelete={noop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function Gallery() {
|
||||
return (
|
||||
<main
|
||||
style={{ width: 1440, padding: 64, display: "flex", flexDirection: "column", gap: 64 }}
|
||||
>
|
||||
<h1 style={{ fontSize: 32, fontWeight: 600 }}>Recordly — App component catalog</h1>
|
||||
<Section name="Shared utility components">
|
||||
<Sample name="ColorPalette">
|
||||
<ColorPalette color="#2563eb" colors={palette} onChange={noop} />
|
||||
</Sample>
|
||||
<Sample name="ColorControl">
|
||||
<div style={{ display: "flex", gap: 12 }}>
|
||||
<ColorControl
|
||||
value="#2563eb"
|
||||
label="Background"
|
||||
colors={palette}
|
||||
onChange={noop}
|
||||
/>
|
||||
<ColorControl value="transparent" label="No fill" compact onChange={noop} />
|
||||
</div>
|
||||
</Sample>
|
||||
<Sample name="ContentClamp">
|
||||
<ContentClamp truncateLength={32}>
|
||||
A deliberately long recording title that truncates while preserving its full
|
||||
text in the tooltip
|
||||
</ContentClamp>
|
||||
</Sample>
|
||||
<Sample name="ItemContent">
|
||||
<ItemContent classes="bg-default">
|
||||
<FolderOpen size={20} />
|
||||
<span>Product demo recording</span>
|
||||
</ItemContent>
|
||||
</Sample>
|
||||
{[12, 72, 94].map((level) => (
|
||||
<Sample key={level} name={`AudioLevelMeter / ${level}`}>
|
||||
<AudioLevelMeter level={level} />
|
||||
</Sample>
|
||||
))}
|
||||
</Section>
|
||||
<Section name="Recording and source selection">
|
||||
{[false, true].map((paused) => (
|
||||
<Sample
|
||||
key={String(paused)}
|
||||
name={`RecordingControls / ${paused ? "Paused" : "Recording"}`}
|
||||
width={612}
|
||||
>
|
||||
<div className="launch-theme flex items-center gap-2">
|
||||
<RecordingControls
|
||||
paused={paused}
|
||||
microphoneEnabled={!paused}
|
||||
elapsed={83}
|
||||
onToggleMicrophone={noop}
|
||||
onPauseResume={noop}
|
||||
onStopRecording={noop}
|
||||
onHideHud={noop}
|
||||
onCancelRecording={noop}
|
||||
formatTime={() => "01:23"}
|
||||
/>
|
||||
</div>
|
||||
</Sample>
|
||||
))}
|
||||
<Sample name="SourceSelectorContent / Sources">
|
||||
<SourceSelectorContent
|
||||
screenSources={screenSources}
|
||||
windowSources={windowSources}
|
||||
selectedSource="Built-in Display"
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="SourceSelectorContent / Empty">
|
||||
<SourceSelectorContent />
|
||||
</Sample>
|
||||
<Sample name="SourceSelectorContent / Loading">
|
||||
<SourceSelectorContent loading />
|
||||
</Sample>
|
||||
<Sample name="DropdownItem / Selected">
|
||||
<DropdownItem selected icon={<Timer size={16} />} onClick={noop}>
|
||||
3s countdown
|
||||
</DropdownItem>
|
||||
<DropdownItem icon={<Microphone size={16} />} onClick={noop}>
|
||||
Microphone
|
||||
</DropdownItem>
|
||||
</Sample>
|
||||
<Sample name="MicDeviceRow / Selected and unselected">
|
||||
<MicDeviceRow
|
||||
device={{ deviceId: "fixture-mic", label: "Built-in microphone" }}
|
||||
selected
|
||||
onSelect={noop}
|
||||
/>
|
||||
<MicDeviceRow
|
||||
device={{ deviceId: "fixture-usb", label: "USB microphone" }}
|
||||
selected={false}
|
||||
onSelect={noop}
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="MarqueeText">
|
||||
<MarqueeText text="A very long display name for the current selected application window" />
|
||||
</Sample>
|
||||
</Section>
|
||||
<Section name="Project library">
|
||||
<Sample name="ProjectBrowserDialog / Populated">
|
||||
<ProjectBrowserDialog
|
||||
open
|
||||
onOpenChange={noop}
|
||||
entries={entries}
|
||||
onOpenProject={noop}
|
||||
onImportFile={noop}
|
||||
renderMode="inline"
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="ProjectBrowserDialog / Empty">
|
||||
<ProjectBrowserDialog
|
||||
open
|
||||
onOpenChange={noop}
|
||||
entries={[]}
|
||||
onOpenProject={noop}
|
||||
onImportFile={noop}
|
||||
renderMode="inline"
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="ExtensionManager / Legacy">
|
||||
<ExtensionManager />
|
||||
</Sample>
|
||||
</Section>
|
||||
<Section name="Export controls">
|
||||
<Sample name="FormatSelector / MP4">
|
||||
<FormatSelector selectedFormat="mp4" onFormatChange={noop} />
|
||||
</Sample>
|
||||
<Sample name="FormatSelector / GIF">
|
||||
<FormatSelector selectedFormat="gif" onFormatChange={noop} />
|
||||
</Sample>
|
||||
<Sample name="GifOptionsPanel">
|
||||
<GifOptionsPanel
|
||||
frameRate={20}
|
||||
onFrameRateChange={noop}
|
||||
loop
|
||||
onLoopChange={noop}
|
||||
sizePreset="medium"
|
||||
onSizePresetChange={noop}
|
||||
outputDimensions={{ width: 960, height: 540 }}
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="ExportSettingsMenu / MP4" width={612}>
|
||||
<ExportSample format="mp4" />
|
||||
</Sample>
|
||||
<Sample name="ExportSettingsMenu / GIF" width={612}>
|
||||
<ExportSample format="gif" />
|
||||
</Sample>
|
||||
</Section>
|
||||
<Section name="Editor controls">
|
||||
<Sample name="SliderControl">
|
||||
<SliderControl
|
||||
label="Padding"
|
||||
value={24}
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
onChange={noop}
|
||||
formatValue={(v) => `${v}px`}
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="WebcamCropControl">
|
||||
<WebcamCropControl
|
||||
cropRegion={{ x: 0.1, y: 0.1, width: 0.8, height: 0.8 }}
|
||||
onCropChange={noop}
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="CropControl">
|
||||
<CropControl
|
||||
videoElement={null}
|
||||
cropRegion={{ x: 0.1, y: 0.1, width: 0.8, height: 0.8 }}
|
||||
onCropChange={noop}
|
||||
aspectRatio="16:9"
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="WallpaperGrid">
|
||||
<WallpaperGrid
|
||||
items={[1, 2, 3, 4].map((i) => ({
|
||||
key: String(i),
|
||||
value: String(i),
|
||||
previewUrl: `/wallpapers/wallpaper${i}.jpg`,
|
||||
label: `Wallpaper ${i}`,
|
||||
removable: i === 4,
|
||||
}))}
|
||||
addLabel="Add wallpaper"
|
||||
onAdd={noop}
|
||||
onSelect={noop}
|
||||
onRemove={noop}
|
||||
isSelected={(value) => value === "1"}
|
||||
/>
|
||||
</Sample>
|
||||
<Sample name="CaptionListPanel / Editing">
|
||||
<CaptionListPanel
|
||||
cues={[
|
||||
{
|
||||
id: "caption-1",
|
||||
startMs: 0,
|
||||
endMs: 2000,
|
||||
text: "Create something worth sharing.",
|
||||
},
|
||||
{
|
||||
id: "caption-2",
|
||||
startMs: 2000,
|
||||
endMs: 5000,
|
||||
text: "Record, refine, and export.",
|
||||
},
|
||||
]}
|
||||
selectedCaptionId="caption-1"
|
||||
currentTimeMs={1000}
|
||||
onBeginCaptionEdit={noop}
|
||||
onCaptionTextEdit={noop}
|
||||
onCaptionRetime={noop}
|
||||
onCaptionSplit={noop}
|
||||
onCaptionMerge={noop}
|
||||
onCaptionDelete={noop}
|
||||
/>
|
||||
</Sample>
|
||||
</Section>
|
||||
<Section name="Annotation inspectors">
|
||||
{(["text", "figure", "blur", "image"] as const).map((type) => (
|
||||
<Sample key={type} name={`AnnotationSettingsPanel / ${type}`}>
|
||||
<AnnotationSample type={type} />
|
||||
</Sample>
|
||||
))}
|
||||
</Section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
function OpenLaunchPopover({ mode }: { mode: string }) {
|
||||
const { requestOpen } = useLaunchPopoverCoordinator();
|
||||
useEffect(() => {
|
||||
requestOpen(mode.replace("launch-", "").replace("-devices", ""));
|
||||
}, [requestOpen, mode]);
|
||||
const trigger = <Button variant="secondary">{mode.replace("launch-", "")} options</Button>;
|
||||
return (
|
||||
<div
|
||||
className="launch-theme"
|
||||
style={{ paddingTop: 650, display: "flex", justifyContent: "center", width: 1440 }}
|
||||
>
|
||||
{mode === "launch-countdown" && (
|
||||
<CountdownPopover trigger={trigger} countdownDelay={3} onSelectDelay={noop} />
|
||||
)}
|
||||
{(mode === "launch-mic" || mode === "launch-mic-devices") && (
|
||||
<MicPopover
|
||||
trigger={trigger}
|
||||
systemAudioEnabled
|
||||
onToggleSystemAudio={noop}
|
||||
microphoneEnabled={mode === "launch-mic-devices"}
|
||||
onDisableMicrophone={noop}
|
||||
devices={
|
||||
mode === "launch-mic-devices"
|
||||
? [
|
||||
{ deviceId: "fixture-mic", label: "Built-in microphone" },
|
||||
{ deviceId: "fixture-usb", label: "USB microphone" },
|
||||
]
|
||||
: []
|
||||
}
|
||||
microphoneDeviceId="fixture-mic"
|
||||
onSelectDevice={noop}
|
||||
/>
|
||||
)}
|
||||
{(mode === "launch-webcam" || mode === "launch-webcam-devices") && (
|
||||
<WebcamPopover
|
||||
trigger={trigger}
|
||||
webcamEnabled={mode === "launch-webcam-devices"}
|
||||
onDisableWebcam={noop}
|
||||
canToggleFloatingPreview
|
||||
showFloatingWebcamPreview
|
||||
onToggleFloatingPreview={noop}
|
||||
showWebcamControls={false}
|
||||
setWebcamPreviewNode={noop}
|
||||
videoDevices={
|
||||
mode === "launch-webcam-devices"
|
||||
? [
|
||||
{ deviceId: "fixture-camera", label: "Built-in camera" },
|
||||
{ deviceId: "fixture-webcam", label: "USB webcam" },
|
||||
]
|
||||
: []
|
||||
}
|
||||
webcamDeviceId="fixture-camera"
|
||||
onSelectVideoDevice={noop}
|
||||
/>
|
||||
)}
|
||||
{mode === "launch-more" && (
|
||||
<MorePopover
|
||||
trigger={trigger}
|
||||
supportsHudCaptureProtection
|
||||
hideHudFromCapture
|
||||
onToggleHudCaptureProtection={noop}
|
||||
onChooseRecordingsDirectory={noop}
|
||||
onOpenVideoFile={noop}
|
||||
onOpenProjectBrowser={noop}
|
||||
showDevUpdatePreview={false}
|
||||
onPreviewUpdateUi={noop}
|
||||
appVersion="1.4.0"
|
||||
/>
|
||||
)}
|
||||
{mode === "launch-sources" && (
|
||||
<SourcePopover
|
||||
trigger={trigger}
|
||||
selectedSource="Built-in Display"
|
||||
onSourceSelect={noop}
|
||||
/>
|
||||
)}
|
||||
{mode === "launch-projects" && (
|
||||
<ProjectPopover trigger={trigger} entries={entries} onOpenProject={noop} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function ProjectDialogState({ mode }: { mode: string }) {
|
||||
const { t } = useI18n();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const anchorRef = useRef<HTMLButtonElement>(null);
|
||||
return (
|
||||
<EditorDialogs
|
||||
t={t}
|
||||
projectSaveDialogOpen={mode === "project-save" || mode === "project-saving"}
|
||||
setProjectSaveDialogOpen={noop}
|
||||
projectSaveDialogDraft="Product walkthrough"
|
||||
setProjectSaveDialogDraft={noop}
|
||||
projectSaveDialogInputRef={inputRef}
|
||||
isSavingProjectDialog={mode === "project-saving"}
|
||||
resolveProjectSaveDialog={noop}
|
||||
handleProjectSaveDialogSubmit={async () => undefined}
|
||||
unsavedChangesDialogOpen={mode === "unsaved-changes"}
|
||||
setUnsavedChangesDialogOpen={noop}
|
||||
unsavedChangesDialogActionLabel="open another project"
|
||||
resolveUnsavedChangesDialog={noop}
|
||||
projectBrowserOpen={false}
|
||||
setProjectBrowserOpen={noop}
|
||||
projectLibraryEntries={entries}
|
||||
projectBrowserAnchorRef={anchorRef}
|
||||
handleImportMediaOrProject={async () => undefined}
|
||||
handleOpenProjectFromLibrary={async () => undefined}
|
||||
nativeCaptureUnavailableModalOpen={mode === "native-capture-unavailable"}
|
||||
setNativeCaptureUnavailableModalOpen={noop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function OverlayViews({ mode }: { mode: string }) {
|
||||
const triggerRoot = useRef<HTMLElement>(null);
|
||||
const { t } = useI18n();
|
||||
const { openConfig } = useShortcuts();
|
||||
useEffect(() => {
|
||||
if (mode === "shortcuts") openConfig();
|
||||
if (mode === "toast") {
|
||||
toast.success("Export complete", {
|
||||
id: "catalog-export",
|
||||
description: "Your recording is ready to share.",
|
||||
duration: Infinity,
|
||||
action: { label: "Show file", onClick: noop },
|
||||
});
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
if (
|
||||
[
|
||||
"font",
|
||||
"keyboard-help",
|
||||
"color",
|
||||
"feedback",
|
||||
"keyboard-dialog",
|
||||
"tutorial",
|
||||
].includes(mode)
|
||||
)
|
||||
triggerRoot.current
|
||||
?.querySelector<HTMLButtonElement>("[data-catalog-trigger] button")
|
||||
?.click();
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [openConfig, mode]);
|
||||
if (mode.startsWith("launch-")) return <OpenLaunchPopover mode={mode} />;
|
||||
if (mode === "loading") return <EditorLoadingSkeleton />;
|
||||
return (
|
||||
<main
|
||||
ref={triggerRoot}
|
||||
data-figma-name={`App state / ${mode}`}
|
||||
style={{ width: 1440, height: 1000, padding: 120, background: "var(--background)" }}
|
||||
>
|
||||
<h1 style={{ fontSize: 24, marginBottom: 40 }}>Recordly · {mode}</h1>
|
||||
{mode === "dialog" && (
|
||||
<Dialog open>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save your project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Keep your recording and edits together so you can continue later.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button>Save project</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{mode === "popover" && (
|
||||
<Popover open>
|
||||
<PopoverTrigger>
|
||||
<Button>Preview options</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<h3>Preview options</h3>
|
||||
<p>Adjust the recording preview.</p>
|
||||
<AudioLevelMeter level={48} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
{mode === "dropdown" && (
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger>
|
||||
<Button>Project actions</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem id="save">
|
||||
Save project<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem id="duplicate">Duplicate project</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem id="delete" variant="danger">
|
||||
Delete project
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{mode === "select" && (
|
||||
<Select isOpen value="balanced">
|
||||
<SelectTrigger className="w-64">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="fast">Fast</SelectItem>
|
||||
<SelectItem value="balanced">Balanced</SelectItem>
|
||||
<SelectItem value="quality">Best quality</SelectItem>
|
||||
<SelectItem value="unavailable" disabled>
|
||||
Unavailable
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
{mode === "toast" && <Toaster />}
|
||||
{mode === "font" && (
|
||||
<div data-catalog-trigger>
|
||||
<AddCustomFontDialog />
|
||||
</div>
|
||||
)}
|
||||
{mode === "keyboard-help" && (
|
||||
<div data-catalog-trigger>
|
||||
<KeyboardShortcutsHelp />
|
||||
</div>
|
||||
)}
|
||||
{mode === "color" && (
|
||||
<div data-catalog-trigger>
|
||||
<ColorControl
|
||||
value="#2563eb"
|
||||
onChange={noop}
|
||||
label="Background color"
|
||||
colors={palette}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mode === "feedback" && (
|
||||
<div data-catalog-trigger>
|
||||
<FeedbackDialog />
|
||||
</div>
|
||||
)}
|
||||
{mode === "keyboard-dialog" && (
|
||||
<div data-catalog-trigger>
|
||||
<KeyboardShortcutsDialog />
|
||||
</div>
|
||||
)}
|
||||
{mode === "tutorial" && (
|
||||
<div data-catalog-trigger>
|
||||
<TutorialHelp />
|
||||
</div>
|
||||
)}
|
||||
{[
|
||||
"project-save",
|
||||
"project-saving",
|
||||
"unsaved-changes",
|
||||
"native-capture-unavailable",
|
||||
].includes(mode) && <ProjectDialogState mode={mode} />}
|
||||
{mode === "shortcuts" && <ShortcutsConfigDialog />}
|
||||
{mode === "crop-dialog" && (
|
||||
<CropEditorDialog
|
||||
open
|
||||
t={t}
|
||||
videoElement={null}
|
||||
cropRegion={{ x: 0.1, y: 0.1, width: 0.8, height: 0.8 }}
|
||||
setCropRegion={noop}
|
||||
aspectRatio="16:9"
|
||||
onCancel={noop}
|
||||
onDone={noop}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
const overlayModes = [
|
||||
"launch-mic-devices",
|
||||
"launch-webcam-devices",
|
||||
"select",
|
||||
"project-save",
|
||||
"project-saving",
|
||||
"unsaved-changes",
|
||||
"native-capture-unavailable",
|
||||
"feedback",
|
||||
"keyboard-dialog",
|
||||
"tutorial",
|
||||
"dialog",
|
||||
"popover",
|
||||
"dropdown",
|
||||
"toast",
|
||||
"font",
|
||||
"keyboard-help",
|
||||
"color",
|
||||
"shortcuts",
|
||||
"crop-dialog",
|
||||
"loading",
|
||||
"launch-countdown",
|
||||
"launch-mic",
|
||||
"launch-webcam",
|
||||
"launch-more",
|
||||
"launch-sources",
|
||||
"launch-projects",
|
||||
];
|
||||
|
||||
/** Keep real portals inside their own preview canvas without duplicating React roots. */
|
||||
function ScopedState({ mode }: { mode: string }) {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<section data-figma-name={`Popup state / ${mode}`} style={{ width: 1440 }}>
|
||||
<h2 style={{ fontSize: 24, fontWeight: 600, padding: "24px 32px" }}>{mode}</h2>
|
||||
<div
|
||||
ref={setContainer}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 1440,
|
||||
height: 1000,
|
||||
overflow: "hidden",
|
||||
transform: "translateZ(0)",
|
||||
isolation: "isolate",
|
||||
}}
|
||||
>
|
||||
{container && (
|
||||
<UNSAFE_PortalProvider getContainer={() => container}>
|
||||
<ShortcutsProvider>
|
||||
<LaunchPopoverCoordinatorProvider>
|
||||
<OverlayViews mode={mode} />
|
||||
</LaunchPopoverCoordinatorProvider>
|
||||
</ShortcutsProvider>
|
||||
</UNSAFE_PortalProvider>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
function AllStates() {
|
||||
return (
|
||||
<main
|
||||
data-figma-name="Recordly / All popup states"
|
||||
style={{ width: 1440, display: "flex", flexDirection: "column", gap: 48 }}
|
||||
>
|
||||
{overlayModes.map((mode) => (
|
||||
<ScopedState key={mode} mode={mode} />
|
||||
))}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
<ShortcutsProvider>
|
||||
<HudInteractionContext.Provider value={{ onMouseEnter: noop, onMouseLeave: noop }}>
|
||||
<LaunchPopoverCoordinatorProvider>
|
||||
{view === "gallery" ? (
|
||||
<Gallery />
|
||||
) : view === "all-states" ? (
|
||||
<AllStates />
|
||||
) : (
|
||||
<OverlayViews mode={view} />
|
||||
)}
|
||||
</LaunchPopoverCoordinatorProvider>
|
||||
</HudInteractionContext.Provider>
|
||||
</ShortcutsProvider>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
@@ -0,0 +1,322 @@
|
||||
import { type ComponentProps, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { UNSAFE_PortalProvider } from "react-aria";
|
||||
import "./index.css";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import { I18nProvider, useI18n } from "./contexts/I18nContext";
|
||||
import { AnnouncementDialog } from "./components/announcements/AnnouncementDialog";
|
||||
import { EditorAnnouncementBanner } from "./components/announcements/EditorAnnouncementBanner";
|
||||
import { LiveAnnouncementNotifications } from "./components/announcements/LiveAnnouncementNotifications";
|
||||
import { Toaster } from "./components/ui/toast";
|
||||
import { EditorExportMenu } from "./components/video-editor/layout/EditorExportMenu";
|
||||
import { EditorPresetMenu } from "./components/video-editor/layout/EditorPresetMenu";
|
||||
import { useExportSettings } from "./components/video-editor/export/useExportSettings";
|
||||
import { useExportSession } from "./components/video-editor/export/useExportSession";
|
||||
import { useExportStatusViewModel } from "./components/video-editor/export/useExportStatusViewModel";
|
||||
import {
|
||||
DEFAULT_EDITOR_PREFERENCES,
|
||||
type EditorPresetSnapshot,
|
||||
} from "./components/video-editor/editorPreferences";
|
||||
import {
|
||||
DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
DEFAULT_CROP_REGION,
|
||||
} from "./components/video-editor/types";
|
||||
import type { ExportProgress } from "./lib/exporter";
|
||||
import type { AnnouncementFeed } from "./lib/announcements";
|
||||
|
||||
const noop = () => undefined;
|
||||
const query = new URLSearchParams(location.search);
|
||||
const view = query.get("view");
|
||||
const memory = new Map<string, unknown>();
|
||||
const feed: AnnouncementFeed = {
|
||||
settings: { aspectRatio: "4:3" },
|
||||
announcements: [
|
||||
{
|
||||
id: "catalog-popup",
|
||||
title: "A clearer way to tell your story",
|
||||
body: "Explore the redesigned editor, with precise timeline cuts and a simpler workspace.",
|
||||
presentation: "popup",
|
||||
audience: "editor",
|
||||
priority: 20,
|
||||
mediaMode: view === "announcement-cover" ? "cover" : "banner",
|
||||
media: { type: "image", url: "/wallpapers/bluerays.jpeg", alt: "Blue rays" },
|
||||
action: { label: "Explore the editor", section: "scene" },
|
||||
},
|
||||
{
|
||||
id: "catalog-popup-next",
|
||||
title: "Create with confidence",
|
||||
body: "Keep your favorite settings together in a reusable preset.",
|
||||
presentation: "popup",
|
||||
audience: "editor",
|
||||
priority: 10,
|
||||
action: { label: "Open settings", section: "settings" },
|
||||
},
|
||||
{
|
||||
id: "catalog-banner",
|
||||
title: "A new Recordly update is available",
|
||||
body: "See what has changed in the latest release.",
|
||||
presentation: "banner",
|
||||
audience: "editor",
|
||||
priority: 20,
|
||||
action: { label: "Learn more", url: "https://recordly.dev" },
|
||||
},
|
||||
{
|
||||
id: "catalog-notification",
|
||||
title: "Your workspace is ready",
|
||||
body: "Browse the refreshed controls and save your favorite setup as a preset.",
|
||||
presentation: "notification",
|
||||
audience: "editor",
|
||||
priority: 20,
|
||||
displayDurationSeconds: 3600,
|
||||
action: { label: "View settings", section: "settings" },
|
||||
},
|
||||
],
|
||||
};
|
||||
Object.assign(window, {
|
||||
electronAPI: {
|
||||
getAppSetting: (key: string) =>
|
||||
key === "recordly.theme"
|
||||
? query.get("theme") === "dark"
|
||||
? "dark"
|
||||
: "light"
|
||||
: (memory.get(key) ?? null),
|
||||
setAppSetting: (key: string, value: unknown) => {
|
||||
memory.set(key, value);
|
||||
return true;
|
||||
},
|
||||
getPlatform: async () => "darwin",
|
||||
getAppVersion: async () => "1.4.0",
|
||||
getAnnouncements: async () => feed,
|
||||
openExternal: async () => ({ success: true }),
|
||||
},
|
||||
});
|
||||
const modes = [
|
||||
"export-settings-mp4",
|
||||
"export-settings-gif",
|
||||
"export-preparing",
|
||||
"export-rendering",
|
||||
"export-audio",
|
||||
"export-finalizing",
|
||||
"export-muxing",
|
||||
"export-saving",
|
||||
"export-success",
|
||||
"export-error",
|
||||
"export-pending-save",
|
||||
"export-legacy",
|
||||
"presets-empty",
|
||||
"presets-saved",
|
||||
"announcement-popup",
|
||||
"announcement-banner",
|
||||
"announcement-notification",
|
||||
];
|
||||
|
||||
function ExportState({ mode }: { mode: string }) {
|
||||
const { t } = useI18n();
|
||||
const baseSettings = useExportSettings(DEFAULT_EDITOR_PREFERENCES, [], []);
|
||||
const baseSession = useExportSession();
|
||||
const exportSettings = {
|
||||
...baseSettings,
|
||||
exportFormat: mode === "export-settings-gif" ? ("gif" as const) : ("mp4" as const),
|
||||
exportPipelineModel: mode === "export-legacy" ? ("legacy" as const) : ("modern" as const),
|
||||
};
|
||||
const exporting = [
|
||||
"export-preparing",
|
||||
"export-rendering",
|
||||
"export-audio",
|
||||
"export-finalizing",
|
||||
"export-muxing",
|
||||
"export-saving",
|
||||
"export-legacy",
|
||||
].includes(mode);
|
||||
const progress: ExportProgress = {
|
||||
currentFrame: 84,
|
||||
totalFrames: 180,
|
||||
percentage: 47,
|
||||
estimatedTimeRemaining: 3,
|
||||
renderFps: 96.2,
|
||||
renderBackend: "webgpu",
|
||||
encodeBackend: "webcodecs",
|
||||
...(mode === "export-preparing" ? { phase: "preparing" as const } : {}),
|
||||
...(mode === "export-audio" ? { phase: "finalizing" as const, audioProgress: 0.62 } : {}),
|
||||
...(mode === "export-finalizing"
|
||||
? { phase: "finalizing" as const, renderProgress: 76 }
|
||||
: {}),
|
||||
...(mode === "export-muxing" ? { phase: "finalizing" as const, renderProgress: 100 } : {}),
|
||||
...(mode === "export-saving" ? { phase: "saving" as const } : {}),
|
||||
};
|
||||
const exportSession = {
|
||||
...baseSession,
|
||||
showExportDropdown: true,
|
||||
isExporting: exporting,
|
||||
exportProgress: exporting ? progress : null,
|
||||
exportError:
|
||||
mode === "export-error"
|
||||
? "The video encoder could not finish this export. Try a different export mode."
|
||||
: mode === "export-pending-save"
|
||||
? "Your video was rendered, but the destination could not be saved."
|
||||
: null,
|
||||
hasPendingExportSave: mode === "export-pending-save",
|
||||
exportedFilePath: mode === "export-success" ? "/Movies/Product walkthrough.mp4" : undefined,
|
||||
};
|
||||
const exportStatus = useExportStatusViewModel({
|
||||
t,
|
||||
settings: exportSettings,
|
||||
session: exportSession,
|
||||
});
|
||||
const exportDimensions = {
|
||||
gifOutputDimensions: { width: 960, height: 540 },
|
||||
mp4OutputDimensions: {
|
||||
medium: { width: 1280, height: 720 },
|
||||
good: { width: 1920, height: 1080 },
|
||||
high: { width: 2560, height: 1440 },
|
||||
source: { width: 1920, height: 1080 },
|
||||
},
|
||||
supportedMp4SourceDimensions: {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
capped: false,
|
||||
encoderPath: null,
|
||||
},
|
||||
ensureSupportedMp4SourceDimensions: async () => ({
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
capped: false,
|
||||
encoderPath: null,
|
||||
}),
|
||||
} satisfies ComponentProps<typeof EditorExportMenu>["exportDimensions"];
|
||||
return (
|
||||
<EditorExportMenu
|
||||
projectTitle="Demo recording"
|
||||
prepareExportForShare={async () => undefined}
|
||||
onRequestShareSignIn={() => {}}
|
||||
shareRequestNonce={0}
|
||||
t={t}
|
||||
exportSettings={exportSettings}
|
||||
exportSession={exportSession}
|
||||
exportDimensions={exportDimensions}
|
||||
exportStatus={exportStatus}
|
||||
hasCaptionsForSidecar
|
||||
nvidiaCudaExportAvailable
|
||||
experimentalNvidiaCudaExport={false}
|
||||
setExperimentalNvidiaCudaExport={noop}
|
||||
handleOpenExportDropdown={noop}
|
||||
handleExportDropdownClose={noop}
|
||||
handleCancelExport={noop}
|
||||
handleRetrySaveExport={noop}
|
||||
handleStartExportFromDropdown={noop}
|
||||
revealExportedFile={noop}
|
||||
exportMessage={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function PresetState({ empty }: { empty: boolean }) {
|
||||
const { t } = useI18n();
|
||||
const snapshot: EditorPresetSnapshot = {
|
||||
...DEFAULT_EDITOR_PREFERENCES,
|
||||
cropRegion: DEFAULT_CROP_REGION,
|
||||
autoCaptionSettings: DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
};
|
||||
const presets = [
|
||||
{
|
||||
id: "clean",
|
||||
name: "Clean product demo",
|
||||
createdAt: "2026-09-19T00:00:00Z",
|
||||
updatedAt: "2026-09-19T00:00:00Z",
|
||||
snapshot,
|
||||
},
|
||||
{
|
||||
id: "social",
|
||||
name: "Social portrait",
|
||||
createdAt: "2026-09-19T00:00:00Z",
|
||||
updatedAt: "2026-09-19T00:00:00Z",
|
||||
snapshot,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<EditorPresetMenu
|
||||
t={t}
|
||||
presets={{
|
||||
editorPresets: empty ? [] : presets,
|
||||
activeEditorPresetId: empty ? null : "clean",
|
||||
presetPopoverOpen: true,
|
||||
setPresetPopoverOpen: noop,
|
||||
presetNameDraft: empty ? "" : "My new preset",
|
||||
setPresetNameDraft: noop,
|
||||
currentEditorPreset: empty ? null : presets[0],
|
||||
handleApplyEditorPreset: noop,
|
||||
handleDeleteEditorPreset: noop,
|
||||
handleSavePresetSubmit: noop,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function State({ mode }: { mode: string }) {
|
||||
if (mode.startsWith("export-")) return <ExportState mode={mode} />;
|
||||
if (mode.startsWith("presets-")) return <PresetState empty={mode === "presets-empty"} />;
|
||||
if (mode === "announcement-popup" || mode === "announcement-cover")
|
||||
return <AnnouncementDialog audience="editor" />;
|
||||
if (mode === "announcement-banner")
|
||||
return (
|
||||
<div style={{ width: 1280 }}>
|
||||
<EditorAnnouncementBanner />
|
||||
</div>
|
||||
);
|
||||
if (mode === "announcement-notification")
|
||||
return (
|
||||
<>
|
||||
<LiveAnnouncementNotifications audience="editor" />
|
||||
<Toaster className="absolute bottom-8 right-8" />
|
||||
</>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
function ScopedState({ mode }: { mode: string }) {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<section data-figma-name={`Extra popup state / ${mode}`} style={{ width: 1440 }}>
|
||||
<h2 style={{ fontSize: 24, fontWeight: 600, padding: "24px 64px" }}>{mode}</h2>
|
||||
<div
|
||||
ref={setContainer}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 1440,
|
||||
height: mode.startsWith("export-settings") ? 850 : 700,
|
||||
overflow: "hidden",
|
||||
transform: "translateZ(0)",
|
||||
isolation: "isolate",
|
||||
background: "var(--surface)",
|
||||
}}
|
||||
>
|
||||
{container && (
|
||||
<UNSAFE_PortalProvider getContainer={() => container}>
|
||||
<div
|
||||
data-figma-name={`component/${mode}`}
|
||||
style={{ display: "flex", justifyContent: "center", paddingTop: 64 }}
|
||||
>
|
||||
<State mode={mode} />
|
||||
</div>
|
||||
</UNSAFE_PortalProvider>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
function Catalog() {
|
||||
return (
|
||||
<main
|
||||
data-figma-name="Recordly / Export presets and announcement popups"
|
||||
style={{ width: 1440, display: "flex", flexDirection: "column", gap: 48 }}
|
||||
>
|
||||
{(view ? [view] : modes).map((mode) => (
|
||||
<ScopedState key={mode} mode={mode} />
|
||||
))}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
<Catalog />
|
||||
</I18nProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Capture-only fixtures for otherwise hook-local LaunchWindow branches.
|
||||
* Source JSX copied verbatim from LaunchWindow.tsx: RecordingControls binding,
|
||||
* finalizingControls and complete returned HUD shell, including floating webcam.
|
||||
* Imports the original LaunchWindow.module.css and original RecordingControls.
|
||||
* Only recorder state, handlers and camera stream are replaced with inert fixtures.
|
||||
*/
|
||||
import { useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { ArrowClockwiseIcon } from "@phosphor-icons/react";
|
||||
import { RxDragHandleDots2 } from "react-icons/rx";
|
||||
import { I18nProvider, useScopedT } from "./contexts/I18nContext";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import { HudInteractionContext } from "./components/launch/contexts/HudInteractionContext";
|
||||
import { RecordingControls } from "./components/launch/RecordingControls";
|
||||
import styles from "./components/launch/LaunchWindow.module.css";
|
||||
import "./index.css";
|
||||
import "./components/launch/launchTheme.css";
|
||||
|
||||
const noop = () => undefined;
|
||||
function attachFixtureVideo(node: HTMLVideoElement | null) {
|
||||
if (!node) return;
|
||||
node.addEventListener("loadeddata", () => { node.currentTime = 2; }, { once: true });
|
||||
node.src = "/tests/ui/fixtures/filmstrip.mp4";
|
||||
}
|
||||
function HudBranch({ finalizing }: { finalizing: boolean }) {
|
||||
const t = useScopedT("launch");
|
||||
const recording = !finalizing;
|
||||
const paused = false;
|
||||
const microphoneEnabled = true;
|
||||
const elapsed = 83;
|
||||
const formatTime = () => "01:23";
|
||||
const setMicrophoneEnabled = (_enabled: boolean) => undefined;
|
||||
const resumeRecording = noop, pauseRecording = noop, toggleRecording = noop, cancelRecording = noop;
|
||||
const hudContentRef = useRef<HTMLDivElement>(null);
|
||||
const hudBarTransformRef = useRef<HTMLDivElement>(null);
|
||||
const hudBarRef = useRef<HTMLDivElement>(null);
|
||||
const recordingWebcamPreviewContainerRef = useRef<HTMLDivElement>(null);
|
||||
const setRecordingWebcamPreviewNode = attachFixtureVideo;
|
||||
const recordingHudOffset = {x:0,y:0}, webcamPreviewOffset = {x:0,y:0};
|
||||
const showRecordingWebcamPreview = !finalizing;
|
||||
const useNativeHudBarDrag = false, shouldAnimateHudLayout = false;
|
||||
const handleHudMouseEnter = noop, handleHudMouseLeave = noop;
|
||||
const handleHudBarPointerDown = noop, handleHudBarPointerMove = noop, handleHudBarPointerUp = noop;
|
||||
const handleWebcamPreviewPointerDown = noop, handleWebcamPreviewPointerMove = noop, handleWebcamPreviewPointerUp = noop;
|
||||
const hudStateTransition = {duration:0.24,ease:[0.22,1,0.36,1] as const};
|
||||
const hudMode = finalizing ? "finalizing" : "recording";
|
||||
const idleControls = null;
|
||||
const recordingControls = (
|
||||
<RecordingControls
|
||||
paused={paused}
|
||||
microphoneEnabled={microphoneEnabled}
|
||||
elapsed={elapsed}
|
||||
onToggleMicrophone={() => setMicrophoneEnabled(!microphoneEnabled)}
|
||||
onPauseResume={paused ? resumeRecording : pauseRecording}
|
||||
onStopRecording={toggleRecording}
|
||||
onHideHud={() => window.electronAPI?.hudOverlayHide?.()}
|
||||
onCancelRecording={cancelRecording}
|
||||
formatTime={formatTime}
|
||||
/>
|
||||
);
|
||||
const finalizingControls = (
|
||||
<div className={styles.finalizingState}>
|
||||
<ArrowClockwiseIcon size={15} className={styles.finalizingSpin} />
|
||||
<div className={styles.finalizingCopy}>
|
||||
<span>{t("recording.preparing", "Preparing recording")}</span>
|
||||
<small>{t("recording.preparingSubtitle", "Opening the editor in a moment")}</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<HudInteractionContext.Provider
|
||||
value={{ onMouseEnter: handleHudMouseEnter, onMouseLeave: handleHudMouseLeave }}
|
||||
>
|
||||
<div
|
||||
className="w-full flex justify-center bg-transparent overflow-visible items-end pb-5 pointer-events-none"
|
||||
style={{ height: "100vh" }}
|
||||
>
|
||||
<div
|
||||
ref={hudContentRef}
|
||||
className="flex items-center overflow-visible flex-col-reverse pointer-events-none"
|
||||
>
|
||||
<div className="flex flex-col items-center pointer-events-none p-2">
|
||||
<div
|
||||
ref={hudBarTransformRef}
|
||||
style={{
|
||||
transform: `translate3d(${recordingHudOffset.x}px, ${recordingHudOffset.y}px, 0)`,
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
ref={hudBarRef}
|
||||
layout={shouldAnimateHudLayout}
|
||||
transition={hudStateTransition}
|
||||
className={`${styles.bar} launch-theme mb-2 pointer-events-auto`}
|
||||
onMouseEnter={handleHudMouseEnter}
|
||||
onMouseLeave={handleHudMouseLeave}
|
||||
>
|
||||
<div
|
||||
// Linux compositors and non-passthrough Windows fallback windows
|
||||
// need native window dragging; the JS drag path only translates
|
||||
// content inside the HUD window.
|
||||
className={`flex items-center px-0.5 cursor-grab active:cursor-grabbing ${
|
||||
useNativeHudBarDrag ? styles.electronDrag : ""
|
||||
}`}
|
||||
onPointerDown={handleHudBarPointerDown}
|
||||
onPointerMove={handleHudBarPointerMove}
|
||||
onPointerUp={handleHudBarPointerUp}
|
||||
onPointerCancel={handleHudBarPointerUp}
|
||||
>
|
||||
<RxDragHandleDots2 size={14} className="text-[#6b6b78]" />
|
||||
</div>
|
||||
|
||||
<div className={styles.barStateViewport}>
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<motion.div
|
||||
key={hudMode}
|
||||
layout={shouldAnimateHudLayout}
|
||||
className={styles.barState}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
scale: 0.985,
|
||||
filter: "blur(8px)",
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
filter: "blur(0px)",
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -10,
|
||||
scale: 0.985,
|
||||
filter: "blur(6px)",
|
||||
}}
|
||||
transition={hudStateTransition}
|
||||
>
|
||||
{finalizing
|
||||
? finalizingControls
|
||||
: recording
|
||||
? recordingControls
|
||||
: idleControls}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
{showRecordingWebcamPreview && (
|
||||
<div
|
||||
ref={recordingWebcamPreviewContainerRef}
|
||||
className={`${styles.recordingWebcamPreview} ${styles.electronNoDrag} pointer-events-auto`}
|
||||
data-hud-interactive
|
||||
title={t("recording.webcam")}
|
||||
style={{
|
||||
transform: `translate(${webcamPreviewOffset.x}px, ${webcamPreviewOffset.y}px)`,
|
||||
}}
|
||||
onMouseEnter={handleHudMouseEnter}
|
||||
onMouseLeave={handleHudMouseLeave}
|
||||
onPointerDown={handleWebcamPreviewPointerDown}
|
||||
onPointerMove={handleWebcamPreviewPointerMove}
|
||||
onPointerUp={handleWebcamPreviewPointerUp}
|
||||
onPointerCancel={handleWebcamPreviewPointerUp}
|
||||
>
|
||||
<video
|
||||
ref={setRecordingWebcamPreviewNode}
|
||||
className={styles.recordingWebcamPreviewVideo}
|
||||
muted
|
||||
playsInline
|
||||
style={{ transform: "scaleX(-1)" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HudInteractionContext.Provider>
|
||||
);
|
||||
}
|
||||
function HudBranchGallery() {
|
||||
return <main data-figma-name="Recordly / Remaining HUD branches" style={{width:1440}}>
|
||||
{[{name:"HUD / Finalizing recording",finalizing:true},{name:"HUD / Floating webcam preview",finalizing:false}].map(state => <section key={state.name} data-figma-name={state.name} data-source="src/components/launch/LaunchWindow.tsx" style={{width:1440}}>
|
||||
<h1 style={{fontSize:24,fontWeight:600,padding:32}}>{state.name}</h1>
|
||||
<div data-figma-name={`component/${state.name}`} style={{position:"relative",width:1440,height:"100vh",overflow:"hidden",transform:"translateZ(0)",isolation:"isolate"}}><HudBranch finalizing={state.finalizing}/></div>
|
||||
</section>)}
|
||||
</main>;
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(<ThemeProvider><I18nProvider><HudBranchGallery/></I18nProvider></ThemeProvider>);
|
||||
@@ -0,0 +1,290 @@
|
||||
import { type ComponentProps } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { SettingsPanel } from "./components/video-editor/SettingsPanel";
|
||||
import {
|
||||
DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
DEFAULT_WEBCAM_OVERLAY,
|
||||
type EditorEffectSection,
|
||||
} from "./components/video-editor/types";
|
||||
import { I18nProvider } from "./contexts/I18nContext";
|
||||
import { ShortcutsProvider } from "./contexts/ShortcutsContext";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
|
||||
type PanelProps = ComponentProps<typeof SettingsPanel>;
|
||||
const noop = () => undefined;
|
||||
const base: PanelProps = {
|
||||
selected: "#2563eb",
|
||||
onWallpaperChange: noop,
|
||||
aspectRatio: "16:9",
|
||||
showCursor: true,
|
||||
selectedZoomDepth: 3,
|
||||
selectedZoomMode: "auto",
|
||||
selectedClipSpeed: 1,
|
||||
selectedClipMuted: false,
|
||||
selectedAudioVolume: 0.8,
|
||||
selectedAudioNormalize: true,
|
||||
webcam: {
|
||||
...DEFAULT_WEBCAM_OVERLAY,
|
||||
enabled: true,
|
||||
sourcePath: "/tests/ui/fixtures/filmstrip.mp4",
|
||||
},
|
||||
webcamPreviewSrc: "/tests/ui/fixtures/filmstrip.mp4",
|
||||
cropRegion: { x: 0, y: 0, width: 1, height: 1 },
|
||||
autoCaptionSettings: { ...DEFAULT_AUTO_CAPTION_SETTINGS, enabled: true },
|
||||
autoCaptions: [
|
||||
{ id: "caption-1", startMs: 0, endMs: 2800, text: "Create something worth sharing." },
|
||||
{ id: "caption-2", startMs: 2800, endMs: 6000, text: "Make every moment clear." },
|
||||
],
|
||||
captionCurrentTimeMs: 1000,
|
||||
};
|
||||
|
||||
const sections: EditorEffectSection[] = [
|
||||
"scene",
|
||||
"frame",
|
||||
"crop",
|
||||
"cursor",
|
||||
"captions",
|
||||
"caption",
|
||||
"webcam",
|
||||
"settings",
|
||||
"zoom",
|
||||
"clip",
|
||||
"audio",
|
||||
"extensions",
|
||||
];
|
||||
const variants: { name: string; props: Partial<PanelProps> }[] = sections.flatMap((section) =>
|
||||
[false, true].map((advanced) => ({
|
||||
name: `${section} / ${advanced ? "Advanced" : "Basic"}`,
|
||||
props: {
|
||||
activeEffectSection: section,
|
||||
advanced,
|
||||
selectedZoomId: section === "zoom" ? "zoom-1" : null,
|
||||
selectedClipId: section === "clip" ? "clip-1" : null,
|
||||
selectedAudioId: section === "audio" ? "audio-1" : null,
|
||||
selectedCaptionId: section === "caption" ? "caption-1" : null,
|
||||
},
|
||||
})),
|
||||
);
|
||||
variants.push(
|
||||
{
|
||||
name: "Background / Image",
|
||||
props: { panelMode: "background", selected: "/wallpapers/wallpaper1.jpg" },
|
||||
},
|
||||
{
|
||||
name: "Background / Video",
|
||||
props: { panelMode: "background", selected: "/tests/ui/fixtures/filmstrip.mp4" },
|
||||
},
|
||||
{ name: "Background / Color", props: { panelMode: "background", selected: "#2563eb" } },
|
||||
{
|
||||
name: "Background / Gradient",
|
||||
props: { panelMode: "background", selected: "linear-gradient(315deg, #EC0101, #5044A9)" },
|
||||
},
|
||||
{
|
||||
name: "Zoom / Manual",
|
||||
props: {
|
||||
activeEffectSection: "zoom",
|
||||
advanced: true,
|
||||
selectedZoomId: "zoom-1",
|
||||
selectedZoomMode: "manual",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Clip / Muted and speed override",
|
||||
props: {
|
||||
activeEffectSection: "clip",
|
||||
advanced: true,
|
||||
selectedClipId: "clip-1",
|
||||
selectedClipSpeed: 2,
|
||||
selectedClipMuted: true,
|
||||
hasClipAudioOverrides: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Captions / Model unavailable",
|
||||
props: {
|
||||
activeEffectSection: "captions",
|
||||
autoCaptions: [],
|
||||
autoCaptionSettings: DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Captions / Downloading model",
|
||||
props: {
|
||||
activeEffectSection: "captions",
|
||||
whisperModelDownloadStatus: "downloading",
|
||||
whisperModelDownloadProgress: 42,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Captions / Model ready",
|
||||
props: {
|
||||
activeEffectSection: "captions",
|
||||
whisperModelPath: "/models/ggml-small.bin",
|
||||
whisperModelDownloadStatus: "downloaded",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Captions / Generating",
|
||||
props: {
|
||||
activeEffectSection: "captions",
|
||||
whisperModelPath: "/models/ggml-small.bin",
|
||||
isGeneratingCaptions: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Captions / Download error",
|
||||
props: { activeEffectSection: "captions", whisperModelDownloadStatus: "error" },
|
||||
},
|
||||
{
|
||||
name: "Webcam / Empty",
|
||||
props: {
|
||||
activeEffectSection: "webcam",
|
||||
webcam: DEFAULT_WEBCAM_OVERLAY,
|
||||
webcamPreviewSrc: null,
|
||||
},
|
||||
},
|
||||
{ name: "Cursor / Hidden", props: { activeEffectSection: "cursor", showCursor: false } },
|
||||
{ name: "Zoom / No selection", props: { activeEffectSection: "zoom", selectedZoomId: null } },
|
||||
{ name: "Clip / No selection", props: { activeEffectSection: "clip", selectedClipId: null } },
|
||||
{
|
||||
name: "Audio / No selection",
|
||||
props: { activeEffectSection: "audio", selectedAudioId: null },
|
||||
},
|
||||
);
|
||||
|
||||
function Catalog() {
|
||||
return (
|
||||
<main
|
||||
data-figma-name="Recordly / Inspector catalog"
|
||||
style={{
|
||||
width: 1440,
|
||||
padding: 40,
|
||||
background: "var(--background)",
|
||||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 12 }}>
|
||||
Inspectors and background panels
|
||||
</h1>
|
||||
<p style={{ marginBottom: 32 }}>
|
||||
Actual SettingsPanel components, including basic, advanced, empty and loading
|
||||
states.
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
alignItems: "start",
|
||||
gap: 28,
|
||||
}}
|
||||
>
|
||||
{variants.map(({ name, props }) => (
|
||||
<section
|
||||
key={name}
|
||||
data-figma-name={`Inspector / ${name}`}
|
||||
style={{
|
||||
background: "var(--surface)",
|
||||
border: "1px solid var(--separator)",
|
||||
borderRadius: 16,
|
||||
paddingTop: 20,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 600, padding: "0 20px 20px" }}>
|
||||
{name}
|
||||
</h2>
|
||||
<div className="inspector-preview">
|
||||
<SettingsPanel {...base} {...callbacks} {...props} />
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Keep every optional source control visible without mutating an editor project.
|
||||
const callbacks = {
|
||||
onWallpaperChange: noop,
|
||||
onZoomDepthChange: noop,
|
||||
onZoomModeChange: noop,
|
||||
onZoomDelete: noop,
|
||||
onResetClipAudio: noop,
|
||||
onClipSpeedChange: noop,
|
||||
onClipMutedChange: noop,
|
||||
onClipDelete: noop,
|
||||
onAudioVolumeChange: noop,
|
||||
onAudioNormalizeChange: noop,
|
||||
onAudioDelete: noop,
|
||||
onShadowChange: noop,
|
||||
onBackgroundBlurChange: noop,
|
||||
onConnectZoomsChange: noop,
|
||||
onAutoApplyFreshRecordingAutoZoomsChange: noop,
|
||||
onZoomInDurationMsChange: noop,
|
||||
onZoomInOverlapMsChange: noop,
|
||||
onZoomOutDurationMsChange: noop,
|
||||
onConnectedZoomGapMsChange: noop,
|
||||
onConnectedZoomDurationMsChange: noop,
|
||||
onZoomInEasingChange: noop,
|
||||
onZoomOutEasingChange: noop,
|
||||
onConnectedZoomEasingChange: noop,
|
||||
onShowCursorChange: noop,
|
||||
onLoopCursorChange: noop,
|
||||
onCursorStyleChange: noop,
|
||||
onCursorSizeChange: noop,
|
||||
onCursorSmoothingChange: noop,
|
||||
onCursorSpringStiffnessMultiplierChange: noop,
|
||||
onCursorSpringDampingMultiplierChange: noop,
|
||||
onCursorSpringMassMultiplierChange: noop,
|
||||
onCameraSpringStiffnessMultiplierChange: noop,
|
||||
onCameraSpringDampingMultiplierChange: noop,
|
||||
onCameraSpringMassMultiplierChange: noop,
|
||||
onZoomClassicModeChange: noop,
|
||||
onCursorClickEffectChange: noop,
|
||||
onCursorClickEffectColorChange: noop,
|
||||
onCursorClickEffectScaleChange: noop,
|
||||
onCursorClickEffectOpacityChange: noop,
|
||||
onCursorClickEffectDurationMsChange: noop,
|
||||
onCursorClickBounceChange: noop,
|
||||
onCursorClickBounceDurationChange: noop,
|
||||
onCursorSwayChange: noop,
|
||||
onBorderRadiusChange: noop,
|
||||
onWebcamChange: noop,
|
||||
onUploadWebcam: noop,
|
||||
onClearWebcam: noop,
|
||||
onPaddingChange: noop,
|
||||
onCropChange: noop,
|
||||
onAspectRatioChange: noop,
|
||||
onAnnotationContentChange: noop,
|
||||
onAnnotationTypeChange: noop,
|
||||
onAnnotationStyleChange: noop,
|
||||
onAnnotationFigureDataChange: noop,
|
||||
onAnnotationBlurIntensityChange: noop,
|
||||
onAnnotationBlurColorChange: noop,
|
||||
onAnnotationDelete: noop,
|
||||
onAutoCaptionSettingsChange: noop,
|
||||
onPickWhisperExecutable: noop,
|
||||
onPickWhisperModel: noop,
|
||||
onGenerateAutoCaptions: noop,
|
||||
onClearAutoCaptions: noop,
|
||||
onDownloadWhisperSmallModel: noop,
|
||||
onDeleteWhisperSmallModel: noop,
|
||||
onBeginCaptionEdit: noop,
|
||||
onCaptionTextEdit: noop,
|
||||
onCaptionRetime: noop,
|
||||
onCaptionSplit: noop,
|
||||
onCaptionMerge: noop,
|
||||
onCaptionDelete: noop,
|
||||
onOpenNativeCaptureUnavailableModal: noop,
|
||||
};
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
<ShortcutsProvider>
|
||||
<Catalog />
|
||||
</ShortcutsProvider>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import './index.css';
|
||||
import {Button} from './components/ui/button';
|
||||
import {Input,TextArea} from './components/ui/input';
|
||||
import {Switch} from './components/ui/switch';
|
||||
import {Slider} from './components/ui/slider';
|
||||
import {Toggle} from './components/ui/toggle';
|
||||
import {ToggleGroup,ToggleGroupItem} from './components/ui/toggle-group';
|
||||
import {ChoiceGroup,ChoiceItem} from './components/ui/choice-group';
|
||||
import {Tabs,TabsList,TabsTrigger,TabsContent} from './components/ui/tabs';
|
||||
import {Select,SelectTrigger,SelectValue,SelectContent,SelectItem} from './components/ui/select';
|
||||
import {Card,CardHeader,CardTitle,CardDescription,CardContent,CardFooter} from './components/ui/card';
|
||||
import {Accordion,AccordionItem,AccordionTrigger,AccordionContent} from './components/ui/accordion';
|
||||
import {Label} from './components/ui/label';
|
||||
import {Separator} from './components/ui/separator';
|
||||
import {Skeleton} from './components/ui/skeleton';
|
||||
import {Plus,Trash,Play,Pause,Scissors,SpeakerHigh as Volume2,ArrowCounterClockwise as Undo2,ArrowClockwise as Redo2,CaretDown as ChevronDown,SlidersHorizontal as Settings2,Cursor as MousePointer,TextT as Type,Camera,Image,Download} from '@phosphor-icons/react';
|
||||
const Sample=({name,children}:{name:string,children:React.ReactNode})=><div data-figma-name={name} style={{display:'flex',flexDirection:'column',gap:12,padding:24,background:'var(--surface)',borderRadius:12,width:340}}><h3 style={{fontSize:12,color:'var(--muted)'}}>{name}</h3><div data-figma-name={'component/'+name}>{children}</div></div>;
|
||||
const Section=({name,children}:{name:string,children:React.ReactNode})=><section data-figma-name={name} style={{display:'flex',flexDirection:'column',gap:24}}><h2 style={{fontSize:24,fontWeight:600}}>{name}</h2><div style={{display:'flex',flexWrap:'wrap',gap:24}}>{children}</div></section>;
|
||||
function Library(){return <main style={{padding:64,width:1440,display:'flex',flexDirection:'column',gap:64}}><h1 style={{fontSize:32,fontWeight:600}}>Recordly — Source components</h1>
|
||||
<Section name="Button">{(['default','secondary','outline','ghost','destructive','destructive-soft','link'] as const).map(variant=><Sample key={variant} name={'Button / '+variant}><div style={{display:'flex',gap:8,flexWrap:'wrap'}}>{(['sm','default','lg'] as const).map(size=><Button key={size} variant={variant} size={size}>{size==='default'?'Action':size}</Button>)}<Button variant={variant} disabled>Disabled</Button><Button variant={variant} size="icon"><Plus/></Button></div></Sample>)}</Section>
|
||||
<Section name="Input">{[false,true].map(disabled=><Sample name={'Input / '+(disabled?'Disabled':'Default')}><Input placeholder="Project name" disabled={disabled}/></Sample>)}<Sample name="Input / Filled"><Input defaultValue="My recording"/></Sample><Sample name="TextArea"><TextArea defaultValue="Add an annotation to your video"/></Sample></Section>
|
||||
<Section name="Switch">{[false,true].map(checked=><Sample name={'Switch / '+checked}><Switch checked={checked}>Enable captions</Switch></Sample>)}<Sample name="Switch / Disabled"><Switch disabled checked>Enable captions</Switch></Sample></Section>
|
||||
<Section name="Slider"><Sample name="Slider / Single"><Slider aria-label="Padding" value={[40]} min={0} max={100}/></Sample><Sample name="Slider / Range"><Slider aria-label="Range" value={[20,80]} min={0} max={100}/></Sample><Sample name="Slider / Disabled"><Slider aria-label="Disabled" value={[40]} disabled/></Sample></Section>
|
||||
<Section name="Toggle"><Sample name="Toggle / Default"><Toggle pressed={false}>Bold</Toggle></Sample><Sample name="Toggle / Selected"><Toggle pressed>Bold</Toggle></Sample><Sample name="Toggle / Disabled"><Toggle disabled>Bold</Toggle></Sample><Sample name="ToggleGroup"><ToggleGroup type="single" value="center"><ToggleGroupItem value="left">Left</ToggleGroupItem><ToggleGroupItem value="center">Center</ToggleGroupItem><ToggleGroupItem value="right">Right</ToggleGroupItem></ToggleGroup></Sample></Section>
|
||||
<Section name="ChoiceGroup"><Sample name="ChoiceGroup"><ChoiceGroup value="fill"><ChoiceItem value="fill">Fill</ChoiceItem><ChoiceItem value="fit">Fit</ChoiceItem><ChoiceItem value="crop">Crop</ChoiceItem></ChoiceGroup></Sample></Section>
|
||||
<Section name="Tabs"><Sample name="Tabs"><Tabs value="image"><TabsList><TabsTrigger value="image">Image</TabsTrigger><TabsTrigger value="video">Video</TabsTrigger><TabsTrigger value="color">Color</TabsTrigger></TabsList><TabsContent value="image">Background options</TabsContent></Tabs></Sample></Section>
|
||||
<Section name="Select"><Sample name="Select"><Select value="auto"><SelectTrigger><SelectValue/></SelectTrigger><SelectContent><SelectItem value="auto">Auto</SelectItem><SelectItem value="1080">1080p</SelectItem></SelectContent></Select></Sample></Section>
|
||||
<Section name="Card"><Sample name="Card"><Card><CardHeader><CardTitle>Recording</CardTitle><CardDescription>Project settings</CardDescription></CardHeader><CardContent>Adjust your recording.</CardContent><CardFooter><Button size="sm">Open project</Button></CardFooter></Card></Sample></Section>
|
||||
<Section name="Accordion"><Sample name="Accordion / Expanded"><Accordion defaultExpandedKeys={['advanced']}><AccordionItem id="advanced"><AccordionTrigger>Advanced</AccordionTrigger><AccordionContent>Additional animation settings</AccordionContent></AccordionItem></Accordion></Sample><Sample name="Accordion / Collapsed"><Accordion><AccordionItem id="advanced"><AccordionTrigger>Advanced</AccordionTrigger><AccordionContent>Additional animation settings</AccordionContent></AccordionItem></Accordion></Sample></Section>
|
||||
<Section name="Label and Separator"><Sample name="Label"><Label>Project name</Label></Sample><Sample name="Separator"><Separator/></Sample><Sample name="Skeleton"><Skeleton style={{width:260,height:60,borderRadius:8}}/></Sample></Section>
|
||||
<Section name="Icons">{Object.entries({Plus,Trash,Play,Pause,Scissors,Volume2,Undo2,Redo2,ChevronDown,Settings2,MousePointer,Type,Camera,Image,Download}).map(([name,Icon])=><Sample name={'Icon / '+name}><Icon size={24}/></Sample>)}</Section>
|
||||
</main>};createRoot(document.getElementById('root')!).render(<Library/>);
|
||||
@@ -0,0 +1,212 @@
|
||||
// The three SourceMenu JSX branches below are extracted from
|
||||
// components/video-editor/layout/EditorPreviewPanel.tsx for design capture.
|
||||
// Only the menu roots are forced open; contents, styles and handlers stay source exact.
|
||||
// This isolates its menus without mounting the video compositor.
|
||||
import { useRef, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { UNSAFE_PortalProvider } from "react-aria";
|
||||
import { CaretDown, Check, Plus, SpeakerHigh, SpeakerLow, SpeakerX } from "@phosphor-icons/react";
|
||||
import "./index.css";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import { I18nProvider, useI18n } from "./contexts/I18nContext";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { Slider } from "./components/ui/slider";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./components/ui/popover";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "./components/ui/dropdown-menu";
|
||||
import { ASPECT_RATIOS, type AspectRatio, getAspectRatioLabel } from "./utils/aspectRatioUtils";
|
||||
import { useTimelineState } from "./components/video-editor/state/useTimelineState";
|
||||
import type { TimelineEditorHandle } from "./components/video-editor/timeline/TimelineEditor";
|
||||
const noop = () => undefined;
|
||||
const query = new URLSearchParams(location.search);
|
||||
Object.assign(window, {
|
||||
electronAPI: {
|
||||
getAppSetting: (key: string) =>
|
||||
key === "recordly.theme" ? (query.get("theme") === "dark" ? "dark" : "light") : null,
|
||||
setAppSetting: () => true,
|
||||
getPlatform: async () => "darwin",
|
||||
},
|
||||
});
|
||||
const modes = ["aspect-ratio", "add-layer", "preview-volume"] as const;
|
||||
type Mode = (typeof modes)[number];
|
||||
function SourceMenu({ mode }: { mode: Mode }) {
|
||||
const { t } = useI18n();
|
||||
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("16:9");
|
||||
const [previewVolume, setPreviewVolume] = useState(0.72);
|
||||
const timeline = useTimelineState();
|
||||
const timelineRef = useRef<TimelineEditorHandle>({
|
||||
addZoom: noop,
|
||||
suggestZooms: noop,
|
||||
splitClip: noop,
|
||||
addAnnotation: noop,
|
||||
addAudio: async () => undefined,
|
||||
keyframes: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-figma-name={`component/EditorPreviewPanel/${mode}`}>
|
||||
{mode === "aspect-ratio" ? (
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-7 gap-1 px-2 text-xs">
|
||||
<span className="font-medium">{getAspectRatioLabel(aspectRatio)}</span>
|
||||
<CaretDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center">
|
||||
{ASPECT_RATIOS.map((ratio) => (
|
||||
<DropdownMenuItem
|
||||
key={ratio}
|
||||
onClick={() => setAspectRatio(ratio)}
|
||||
className="flex cursor-pointer items-center justify-between gap-3 text-muted-foreground hover:bg-foreground/10 hover:text-foreground"
|
||||
>
|
||||
<span>{getAspectRatioLabel(ratio)}</span>
|
||||
{aspectRatio === ratio ? (
|
||||
<Check className="h-3 w-3 text-[#2563EB]" />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : mode === "add-layer" ? (
|
||||
<DropdownMenu open>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-9 gap-2 px-3">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">{t("editor.toolbar.addLayer")}</span>
|
||||
<CaretDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const nextTrack =
|
||||
timeline.annotationRegions.length > 0
|
||||
? Math.max(
|
||||
...timeline.annotationRegions.map(
|
||||
(region) => region.trackIndex ?? 0,
|
||||
),
|
||||
) + 1
|
||||
: 0;
|
||||
timelineRef.current?.addAnnotation(nextTrack);
|
||||
}}
|
||||
className="cursor-pointer text-muted-foreground hover:bg-foreground/10 hover:text-foreground"
|
||||
>
|
||||
{t("timeline.annotation.label")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const nextTrack =
|
||||
timeline.audioRegions.length > 0
|
||||
? Math.max(
|
||||
...timeline.audioRegions.map(
|
||||
(region) => region.trackIndex ?? 0,
|
||||
),
|
||||
) + 1
|
||||
: 0;
|
||||
timelineRef.current?.addAudio(nextTrack);
|
||||
}}
|
||||
className="cursor-pointer text-muted-foreground hover:bg-foreground/10 hover:text-foreground"
|
||||
>
|
||||
{t("timeline.audio.label")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Popover open>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t("editor.playback.volume", "Preview volume")}
|
||||
title={t("editor.playback.volume", "Preview volume")}
|
||||
>
|
||||
{previewVolume <= 0.001 ? (
|
||||
<SpeakerX className="size-3.5" />
|
||||
) : previewVolume < 0.5 ? (
|
||||
<SpeakerLow className="size-3.5" />
|
||||
) : (
|
||||
<SpeakerHigh className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="top"
|
||||
sideOffset={10}
|
||||
aria-label="Preview volume"
|
||||
className="flex w-14 flex-col items-center gap-3 p-3"
|
||||
>
|
||||
<span className="text-[10px] tabular-nums text-muted-foreground">
|
||||
{Math.round(previewVolume * 100)}%
|
||||
</span>
|
||||
<Slider
|
||||
aria-label={t("editor.playback.volume", "Preview volume")}
|
||||
orientation="vertical"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={[previewVolume]}
|
||||
onValueChange={([value]) => setPreviewVolume(value)}
|
||||
className="h-28"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function ScopedState({ mode }: { mode: Mode }) {
|
||||
const [host, setHost] = useState<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<section data-figma-name={`Preview menu / ${mode}`} style={{ width: 1440 }}>
|
||||
<h2 style={{ fontSize: 24, fontWeight: 600, padding: "24px 64px" }}>{mode}</h2>
|
||||
<div
|
||||
ref={setHost}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 1440,
|
||||
height: 650,
|
||||
overflow: "hidden",
|
||||
transform: "translateZ(0)",
|
||||
isolation: "isolate",
|
||||
background: "var(--surface)",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
paddingTop: mode === "preview-volume" ? 340 : 64,
|
||||
}}
|
||||
>
|
||||
{host && (
|
||||
<UNSAFE_PortalProvider getContainer={() => host}>
|
||||
<SourceMenu mode={mode} />
|
||||
</UNSAFE_PortalProvider>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
function Catalog() {
|
||||
const view = query.get("view");
|
||||
return (
|
||||
<main
|
||||
data-figma-name="Recordly / All preview menus"
|
||||
style={{ width: 1440, display: "flex", flexDirection: "column", gap: 48 }}
|
||||
>
|
||||
{modes
|
||||
.filter((mode) => !view || view === mode)
|
||||
.map((mode) => (
|
||||
<ScopedState key={mode} mode={mode} />
|
||||
))}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
<Catalog />
|
||||
</I18nProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
@@ -0,0 +1,272 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { I18nProvider } from "./contexts/I18nContext";
|
||||
import { ShortcutsProvider } from "./contexts/ShortcutsContext";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
import TimelineEditor from "./components/video-editor/timeline/TimelineEditor";
|
||||
import PlaybackControls from "./components/video-editor/PlaybackControls";
|
||||
import { AnnotationOverlay } from "./components/video-editor/AnnotationOverlay";
|
||||
import {
|
||||
DEFAULT_ANNOTATION_STYLE,
|
||||
type AnnotationRegion,
|
||||
type ClipRegion,
|
||||
type ZoomRegion,
|
||||
} from "./components/video-editor/types";
|
||||
|
||||
const noop = () => undefined;
|
||||
const media = `${location.origin}/tests/ui/fixtures/filmstrip.mp4`;
|
||||
const theme = new URLSearchParams(location.search).get("theme") === "dark" ? "dark" : "light";
|
||||
Object.assign(window, {
|
||||
electronAPI: {
|
||||
getAppSetting: (key: string) => (key === "recordly.theme" ? theme : null),
|
||||
setAppSetting: () => true,
|
||||
getPlatform: async () => "darwin",
|
||||
getShortcuts: async () => null,
|
||||
getVideoAudioFallbackPaths: async () => ({ success: true, paths: [] }),
|
||||
getLocalMediaUrl: async (path: string) => ({ success: true, url: path }),
|
||||
},
|
||||
});
|
||||
const clips: ClipRegion[] = [
|
||||
{ id: "clip-a", startMs: 0, endMs: 2800, sourceStartMs: 0, speed: 1 },
|
||||
{ id: "clip-b", startMs: 2800, endMs: 6000, sourceStartMs: 2800, speed: 1, muted: true },
|
||||
];
|
||||
const zooms: ZoomRegion[] = [
|
||||
{
|
||||
id: "zoom-a",
|
||||
startMs: 400,
|
||||
endMs: 1900,
|
||||
depth: 2,
|
||||
focus: { cx: 0.5, cy: 0.5 },
|
||||
mode: "auto",
|
||||
},
|
||||
{
|
||||
id: "zoom-b",
|
||||
startMs: 3400,
|
||||
endMs: 5200,
|
||||
depth: 4,
|
||||
focus: { cx: 0.7, cy: 0.3 },
|
||||
mode: "manual",
|
||||
},
|
||||
];
|
||||
const annotation: AnnotationRegion = {
|
||||
id: "annotation-a",
|
||||
startMs: 800,
|
||||
endMs: 2300,
|
||||
type: "text",
|
||||
content: "Make your point",
|
||||
position: { x: 16, y: 28 },
|
||||
size: { width: 68, height: 40 },
|
||||
style: {
|
||||
...DEFAULT_ANNOTATION_STYLE,
|
||||
fontSize: 64,
|
||||
backgroundColor: "#2563eb",
|
||||
borderRadius: 8,
|
||||
},
|
||||
zIndex: 1,
|
||||
};
|
||||
function Sample({
|
||||
name,
|
||||
children,
|
||||
height,
|
||||
}: {
|
||||
name: string;
|
||||
children: React.ReactNode;
|
||||
height?: number;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
data-figma-name={`Timeline library / ${name}`}
|
||||
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
||||
>
|
||||
<h2 style={{ fontSize: 18, fontWeight: 600 }}>{name}</h2>
|
||||
<div
|
||||
data-figma-name={`component/${name}`}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height,
|
||||
border: "1px solid var(--separator)",
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
background: "var(--surface)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
function TimelineSample({
|
||||
selection,
|
||||
}: {
|
||||
selection: "clip" | "zoom" | "annotation" | "audio" | "caption" | "empty";
|
||||
}) {
|
||||
return (
|
||||
<TimelineEditor
|
||||
videoDuration={selection === "empty" ? 0 : 6}
|
||||
currentTime={2.8}
|
||||
playheadTime={2.8}
|
||||
onSeek={noop}
|
||||
videoPath={media}
|
||||
videoSourcePath={media}
|
||||
disableSuggestedZooms
|
||||
clipRegions={clips}
|
||||
zoomRegions={zooms}
|
||||
onZoomAdded={noop}
|
||||
onZoomSpanChange={noop}
|
||||
onZoomDelete={noop}
|
||||
selectedZoomId={selection === "zoom" ? "zoom-a" : null}
|
||||
onSelectZoom={noop}
|
||||
onClipSplit={noop}
|
||||
onClipSpanChange={noop}
|
||||
onClipDelete={noop}
|
||||
selectedClipId={selection === "clip" ? "clip-a" : null}
|
||||
onSelectClip={noop}
|
||||
annotationRegions={[
|
||||
annotation,
|
||||
{
|
||||
...annotation,
|
||||
id: "annotation-b",
|
||||
startMs: 3500,
|
||||
endMs: 5100,
|
||||
content: "A second callout",
|
||||
trackIndex: 1,
|
||||
},
|
||||
]}
|
||||
onAnnotationAdded={noop}
|
||||
onAnnotationSpanChange={noop}
|
||||
onAnnotationDelete={noop}
|
||||
onSelectAnnotation={noop}
|
||||
selectedAnnotationId={selection === "annotation" ? "annotation-a" : null}
|
||||
audioRegions={[
|
||||
{
|
||||
id: "audio-a",
|
||||
startMs: 0,
|
||||
endMs: 6000,
|
||||
audioPath: `${location.origin}/tests/ui/fixtures/preview.mp4`,
|
||||
volume: 0.8,
|
||||
},
|
||||
]}
|
||||
onAudioAdded={noop}
|
||||
onAudioSpanChange={noop}
|
||||
onAudioDelete={noop}
|
||||
onSelectAudio={noop}
|
||||
selectedAudioId={selection === "audio" ? "audio-a" : null}
|
||||
captionRegions={[
|
||||
{ id: "caption-a", startMs: 100, endMs: 2000, text: "Welcome to Recordly" },
|
||||
{ id: "caption-b", startMs: 2900, endMs: 5200, text: "Create something clear." },
|
||||
]}
|
||||
captionsEnabled
|
||||
captionQuickAddEnabled
|
||||
onCaptionAdded={noop}
|
||||
onCaptionSpanChange={noop}
|
||||
onCaptionDelete={noop}
|
||||
onSelectCaption={noop}
|
||||
selectedCaptionId={selection === "caption" ? "caption-a" : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function Library() {
|
||||
const [image, setImage] = useState("");
|
||||
useEffect(() => {
|
||||
void fetch("/rec-button.png")
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => setImage(String(reader.result));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<main
|
||||
style={{ width: 1440, padding: 64, display: "flex", flexDirection: "column", gap: 48 }}
|
||||
>
|
||||
<h1 style={{ fontSize: 32, fontWeight: 600 }}>
|
||||
Recordly — Timeline and preview components
|
||||
</h1>
|
||||
{(["clip", "zoom", "annotation", "audio", "caption", "empty"] as const).map(
|
||||
(selection) => (
|
||||
<Sample
|
||||
key={selection}
|
||||
name={`TimelineEditor / ${selection === "empty" ? "Empty" : `${selection} selected`}`}
|
||||
height={selection === "empty" ? 180 : 420}
|
||||
>
|
||||
<TimelineSample selection={selection} />
|
||||
</Sample>
|
||||
),
|
||||
)}
|
||||
{[
|
||||
{ name: "Paused", playing: false, volume: 1 },
|
||||
{ name: "Playing", playing: true, volume: 0.6 },
|
||||
{ name: "Muted", playing: false, volume: 0 },
|
||||
].map((state) => (
|
||||
<Sample key={state.name} name={`PlaybackControls / ${state.name}`}>
|
||||
<PlaybackControls
|
||||
isPlaying={state.playing}
|
||||
currentTime={2.8}
|
||||
duration={6}
|
||||
onTogglePlayPause={noop}
|
||||
onSeek={noop}
|
||||
volume={state.volume}
|
||||
onVolumeChange={noop}
|
||||
/>
|
||||
</Sample>
|
||||
))}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 32 }}>
|
||||
{(["text", "figure", "image", "blur"] as const).flatMap((type) =>
|
||||
[false, true].map((selected) => {
|
||||
const name = `AnnotationOverlay / ${type} / ${selected ? "Selected" : "Default"}`;
|
||||
return (
|
||||
<Sample key={name} name={name}>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: 560,
|
||||
height: 240,
|
||||
overflow: "hidden",
|
||||
background: "#182332",
|
||||
}}
|
||||
>
|
||||
<AnnotationOverlay
|
||||
annotation={{
|
||||
...annotation,
|
||||
type,
|
||||
content: type === "image" ? image : annotation.content,
|
||||
figureData: {
|
||||
arrowDirection: "right",
|
||||
color: "#2563eb",
|
||||
strokeWidth: 4,
|
||||
},
|
||||
blurIntensity: 40,
|
||||
blurColor: "rgba(255,255,255,.15)",
|
||||
}}
|
||||
isSelected={selected}
|
||||
containerWidth={560}
|
||||
containerHeight={240}
|
||||
recordingRect={{ x: 0, y: 0, width: 560, height: 240 }}
|
||||
sceneTransform={{ scale: 1, x: 0, y: 0 }}
|
||||
onPositionChange={noop}
|
||||
onSizeChange={noop}
|
||||
onClick={noop}
|
||||
zIndex={1}
|
||||
isSelectedBoost={selected}
|
||||
/>
|
||||
</div>
|
||||
</Sample>
|
||||
);
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
<ShortcutsProvider>
|
||||
<Library />
|
||||
</ShortcutsProvider>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
@@ -0,0 +1,87 @@
|
||||
import { TimelineContext, useTimelineContext } from "dnd-timeline";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import KeyframeMarkers from "./components/video-editor/timeline/components/markers/KeyframeMarkers";
|
||||
import AudioWaveform from "./components/video-editor/timeline/components/waveform/AudioWaveform";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
|
||||
const noop = () => undefined;
|
||||
const peaks = {
|
||||
durationMs: 6000,
|
||||
peaks: Float32Array.from(
|
||||
{ length: 100 },
|
||||
(_, index) => Math.abs(Math.sin(index * 0.47) * Math.cos(index * 0.13)) * 0.85 + 0.03,
|
||||
),
|
||||
};
|
||||
const samples = [
|
||||
{ name: "KeyframeMarkers / Default", markers: true, selected: false, normalized: false },
|
||||
{ name: "KeyframeMarkers / Selected", markers: true, selected: true, normalized: false },
|
||||
{ name: "AudioWaveform / Normal", markers: false, selected: false, normalized: false },
|
||||
{ name: "AudioWaveform / Normalized", markers: false, selected: false, normalized: true },
|
||||
];
|
||||
function Detail({ sample }: { sample: (typeof samples)[number] }) {
|
||||
const { setTimelineRef, style } = useTimelineContext();
|
||||
const timelineRef = useRef<HTMLDivElement | null>(null);
|
||||
const setRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
timelineRef.current = node;
|
||||
setTimelineRef(node);
|
||||
},
|
||||
[setTimelineRef],
|
||||
);
|
||||
return (
|
||||
<div
|
||||
ref={setRef}
|
||||
data-figma-name={`component/${sample.name}`}
|
||||
style={{
|
||||
...style,
|
||||
position: "relative",
|
||||
height: 96,
|
||||
width: 500,
|
||||
background: sample.markers ? "var(--surface)" : "#dbe9df",
|
||||
border: "1px solid var(--separator)",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
{sample.markers ? (
|
||||
<KeyframeMarkers
|
||||
keyframes={[{ id: "keyframe-1", time: 3000 }]}
|
||||
selectedKeyframeId={sample.selected ? "keyframe-1" : null}
|
||||
setSelectedKeyframeId={noop}
|
||||
onKeyframeMove={noop}
|
||||
videoDurationMs={6000}
|
||||
timelineRef={timelineRef}
|
||||
/>
|
||||
) : (
|
||||
<AudioWaveform peaks={peaks} normalize={sample.normalized} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ThemeProvider>
|
||||
<main
|
||||
style={{
|
||||
width: 1200,
|
||||
padding: 40,
|
||||
background: "var(--background)",
|
||||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: 28, fontWeight: 700, marginBottom: 32 }}>
|
||||
Timeline detail components
|
||||
</h1>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 32 }}>
|
||||
{samples.map((sample) => (
|
||||
<section key={sample.name} data-figma-name={sample.name}>
|
||||
<h2 style={{ fontSize: 18, marginBottom: 20 }}>{sample.name}</h2>
|
||||
<TimelineContext range={{ start: 0, end: 6000 }} onRangeChanged={noop} onResizeEnd={noop}>
|
||||
<Detail sample={sample} />
|
||||
</TimelineContext>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Tooltip } from "@heroui/react";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { UNSAFE_PortalProvider } from "react-aria";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import "./components/launch/launchTheme.css";
|
||||
import App from "./App";
|
||||
import { CountdownOverlay } from "./components/countdown/CountdownOverlay";
|
||||
import HudWindow from "./components/launch/HudWindow";
|
||||
import { type UpdateToastPayload, UpdateToastWindow } from "./components/launch/UpdateToastWindow";
|
||||
import { Button } from "./components/ui/button";
|
||||
import { I18nProvider } from "./contexts/I18nContext";
|
||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||
|
||||
const states: { name: string; payload: UpdateToastPayload }[] = [
|
||||
{
|
||||
name: "Available",
|
||||
payload: {
|
||||
version: "1.5.0",
|
||||
detail: "A new version of Recordly is available.",
|
||||
phase: "available",
|
||||
delayMs: 3600000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Downloading",
|
||||
payload: {
|
||||
version: "1.5.0",
|
||||
detail: "Your update is downloading.",
|
||||
phase: "downloading",
|
||||
delayMs: 3600000,
|
||||
progressPercent: 42,
|
||||
transferredBytes: 44040192,
|
||||
totalBytes: 104857600,
|
||||
bytesPerSecond: 3145728,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ready",
|
||||
payload: {
|
||||
version: "1.5.0",
|
||||
detail: "Your update is ready to install.",
|
||||
phase: "ready",
|
||||
delayMs: 3600000,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Check error",
|
||||
payload: {
|
||||
version: "1.5.0",
|
||||
detail: "Check your connection and try again.",
|
||||
phase: "error",
|
||||
delayMs: 3600000,
|
||||
primaryAction: "retry-check",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Download error",
|
||||
payload: {
|
||||
version: "1.5.0",
|
||||
detail: "The download was interrupted. Please try again.",
|
||||
phase: "error",
|
||||
delayMs: 3600000,
|
||||
primaryAction: "install-and-restart",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Experimental",
|
||||
payload: {
|
||||
version: "1.5.0",
|
||||
detail: "An experimental update is available.",
|
||||
phase: "available",
|
||||
delayMs: 3600000,
|
||||
isExperimental: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Preview",
|
||||
payload: {
|
||||
version: "1.5.0",
|
||||
detail: "Preview of the update notification.",
|
||||
phase: "available",
|
||||
delayMs: 3600000,
|
||||
isPreview: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function Sample({
|
||||
name,
|
||||
height = 300,
|
||||
wide = false,
|
||||
children,
|
||||
}: {
|
||||
name: string;
|
||||
height?: number;
|
||||
wide?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<section
|
||||
data-figma-name={name}
|
||||
style={{
|
||||
gridColumn: wide ? "1 / -1" : undefined,
|
||||
border: "1px solid var(--separator)",
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
background: "var(--surface)",
|
||||
}}
|
||||
>
|
||||
<h2 style={{ padding: "20px 24px", fontSize: 18, fontWeight: 600 }}>{name}</h2>
|
||||
<div
|
||||
ref={setContainer}
|
||||
style={{
|
||||
height,
|
||||
position: "relative",
|
||||
transform: "translateZ(0)",
|
||||
isolation: "isolate",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{container && (
|
||||
<UNSAFE_PortalProvider getContainer={() => container}>
|
||||
{children}
|
||||
</UNSAFE_PortalProvider>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Catalog() {
|
||||
return (
|
||||
<main
|
||||
style={{
|
||||
width: 1440,
|
||||
padding: 40,
|
||||
background: "var(--background)",
|
||||
color: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: 32, fontWeight: 700, marginBottom: 32 }}>
|
||||
Recorder windows and notifications
|
||||
</h1>
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 28 }}>
|
||||
{states.map(({ name, payload }) => (
|
||||
<Sample key={name} name={`UpdateToastWindow / ${name}`} height={250}>
|
||||
<div style={{ width: 420, height: 172, margin: "25px auto" }}>
|
||||
<UpdateToastWindow payload={payload} />
|
||||
</div>
|
||||
</Sample>
|
||||
))}
|
||||
<Sample name="CountdownOverlay / 3 seconds">
|
||||
<CountdownOverlay />
|
||||
</Sample>
|
||||
<Sample name="HudWindow / Idle" wide height={360}>
|
||||
<HudWindow />
|
||||
</Sample>
|
||||
<Sample name="App / Startup" wide height={280}>
|
||||
<App />
|
||||
</Sample>
|
||||
<Sample name="Tooltip / Content clamp" height={180}>
|
||||
<div style={{ padding: "80px 70px" }}>
|
||||
<Tooltip isOpen>
|
||||
<Tooltip.Trigger>
|
||||
<Button variant="ghost">Project title…</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
Project title with the full untruncated text
|
||||
</Tooltip.Content>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Sample>
|
||||
<Sample name="Tooltip / Sidebar placement" height={180}>
|
||||
<div style={{ padding: "60px 70px" }}>
|
||||
<Tooltip isOpen>
|
||||
<Tooltip.Trigger>
|
||||
<Button variant="ghost">Scene</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content placement="right">Scene</Tooltip.Content>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Sample>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ThemeProvider>
|
||||
<I18nProvider>
|
||||
<Catalog />
|
||||
</I18nProvider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
Reference in new issue
Block a user