Organize settings with HeroUI section tags and default new zooms to 1.8x

This commit is contained in:
webadderall
2026-09-23 12:09:30 +10:00
parent 026e8d8493
commit e75c6ce2b5
10 changed files with 504 additions and 402 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 43 KiB

+297 -268
View File
@@ -1,3 +1,4 @@
import { SettingsSections, SettingsCategory } from "./SettingsSections";
import { Card, RadioGroup, Radio, Label, Description } from "@heroui/react";
import { ProgressBar } from "@heroui/react";
import { ColorControl, ColorPalette } from "@/components/ui/color-picker";
@@ -2236,301 +2237,329 @@ export function SettingsPanel({
const effectSectionContent = (() => {
const settingsSectionContent = (
<div className="space-y-5">
<section className="flex flex-col gap-4">
<SectionLabel>{t("editor.theme.appearance", "Appearance")}</SectionLabel>
<ChoiceGroup
type="single"
aria-label={t("editor.theme.appearance", "Appearance")}
value={themePreference}
onValueChange={(value) => {
if (value === "light" || value === "dark" || value === "system")
setThemePreference(value);
}}
fullWidth
size="sm"
>
<ChoiceItem value="light" className="flex-1">
{t("editor.theme.light", "Light")}
</ChoiceItem>
<ChoiceItem value="dark" className="flex-1">
{t("editor.theme.dark", "Dark")}
</ChoiceItem>
<ChoiceItem value="system" className="flex-1">
{t("editor.theme.system", "System")}
</ChoiceItem>
</ChoiceGroup>
</section>
<section className="flex flex-col gap-4">
<SectionLabel>{t("common.app.language", "Language")}</SectionLabel>
<Select value={locale} onValueChange={(value) => setLocale(value as AppLocale)}>
<SelectTrigger className="h-9 w-full text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SUPPORTED_LOCALES.map((candidateLocale) => (
<SelectItem key={candidateLocale} value={candidateLocale}>
{APP_LANGUAGE_LABELS[candidateLocale]}
</SelectItem>
))}
</SelectContent>
</Select>
</section>
{advanced && (
<SettingsSections
categories={advanced ? ["general", "motion", "advanced"] : ["general", "motion"]}
>
<SettingsCategory category="general">
<section className="flex flex-col gap-4">
<SectionLabel>{tSettings("updates.title", "Updates")}</SectionLabel>
<SectionLabel>{t("editor.theme.appearance", "Appearance")}</SectionLabel>
<ChoiceGroup
type="single"
aria-label={t("editor.theme.appearance", "Appearance")}
value={themePreference}
onValueChange={(value) => {
if (value === "light" || value === "dark" || value === "system")
setThemePreference(value);
}}
fullWidth
size="sm"
>
<ChoiceItem value="light" className="flex-1">
{t("editor.theme.light", "Light")}
</ChoiceItem>
<ChoiceItem value="dark" className="flex-1">
{t("editor.theme.dark", "Dark")}
</ChoiceItem>
<ChoiceItem value="system" className="flex-1">
{t("editor.theme.system", "System")}
</ChoiceItem>
</ChoiceGroup>
</section>
<section className="flex flex-col gap-4">
<SectionLabel>{t("common.app.language", "Language")}</SectionLabel>
<Select
value={locale}
onValueChange={(value) => setLocale(value as AppLocale)}
>
<SelectTrigger className="h-9 w-full text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{SUPPORTED_LOCALES.map((candidateLocale) => (
<SelectItem key={candidateLocale} value={candidateLocale}>
{APP_LANGUAGE_LABELS[candidateLocale]}
</SelectItem>
))}
</SelectContent>
</Select>
</section>
</SettingsCategory>
<SettingsCategory category="advanced">
{advanced && (
<section className="flex flex-col gap-4">
<SectionLabel>{tSettings("updates.title", "Updates")}</SectionLabel>
<div className="flex items-center justify-between gap-3 py-2">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings("updates.experimental", "Experimental updates")}
</div>
<div className="mt-0.5 text-xs text-muted-foreground/70">
{tSettings(
"updates.experimentalDescription",
"This is the front line of user testing - highly experimental so expect bugs",
)}
</div>
</div>
<Switch
checked={experimentalUpdatesEnabled}
disabled={savingExperimentalUpdates}
onCheckedChange={(enabled) =>
void updateExperimentalUpdatesPreference(enabled)
}
aria-label={tSettings(
"updates.experimental",
"Experimental updates",
)}
/>
</div>
</section>
)}
</SettingsCategory>
<SettingsCategory category="motion">
<section className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-3 py-2">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings("updates.experimental", "Experimental updates")}
{tSettings(
"effects.autoApplyFreshRecordingZooms",
"Auto-apply fresh recording zooms",
)}
</div>
<div className="mt-0.5 text-xs text-muted-foreground/70">
{tSettings(
"updates.experimentalDescription",
"This is the front line of user testing - highly experimental so expect bugs",
"effects.autoApplyFreshRecordingZoomsDescription",
"Suggest cursor-follow zooms automatically when you open a new recording.",
)}
</div>
</div>
<Switch
checked={experimentalUpdatesEnabled}
disabled={savingExperimentalUpdates}
onCheckedChange={(enabled) =>
void updateExperimentalUpdatesPreference(enabled)
}
aria-label={tSettings(
"updates.experimental",
"Experimental updates",
)}
/>
</div>
</section>
)}
<section className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-3 py-2">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings(
"effects.autoApplyFreshRecordingZooms",
"Auto-apply fresh recording zooms",
)}
</div>
<div className="mt-0.5 text-xs text-muted-foreground/70">
{tSettings(
"effects.autoApplyFreshRecordingZoomsDescription",
"Suggest cursor-follow zooms automatically when you open a new recording.",
)}
</div>
checked={autoApplyFreshRecordingAutoZooms}
onCheckedChange={onAutoApplyFreshRecordingAutoZoomsChange}
/>
</div>
<Switch
aria-label={tSettings(
"effects.autoApplyFreshRecordingZooms",
"Auto-apply fresh recording zooms",
)}
checked={autoApplyFreshRecordingAutoZooms}
onCheckedChange={onAutoApplyFreshRecordingAutoZoomsChange}
/>
</div>
<div className="flex items-center justify-between gap-3 py-2">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings("effects.connectZooms", "Connect neighboring zooms")}
</div>
<div className="mt-0.5 text-xs text-muted-foreground/70">
{tSettings(
"effects.connectZoomsDescription",
"Smooth consecutive zoom regions into a continuous camera move.",
)}
</div>
</div>
<Switch
aria-label={tSettings(
"effects.connectZooms",
"Connect neighboring zooms",
)}
checked={connectZooms}
onCheckedChange={onConnectZoomsChange}
/>
</div>
</section>
<section className="flex flex-col gap-4">
<MotionPresetCards
title={tSettings("effects.motionPresetsTitle", "Motion Presets")}
activePresetId={activeMotionPresetId}
onApply={applyMotionPreset}
tSettings={tSettings}
/>
</section>
<section className="flex flex-col gap-4">
<SectionLabel>{t("editor.keyboardShortcuts.title")}</SectionLabel>
<KeyboardShortcutsDialog
triggerLabel={t("editor.keyboardShortcuts.customize")}
triggerClassName="h-9 w-full justify-start rounded-xl border border-foreground/10 bg-foreground/5 px-3 text-sm text-foreground hover:bg-foreground/10 hover:text-foreground"
/>
</section>
{advanced && showDevMotionControls ? (
<section className="flex flex-col gap-4 rounded-xl border border-[#2563EB]/15 bg-[#2563EB]/5 p-3">
<div className="flex items-center justify-between gap-3">
<div>
<SectionLabel>
{tSettings("effects.devSection", "Dev")}
</SectionLabel>
<div className="mt-0.5 text-xs text-muted-foreground">
{tSettings(
"effects.devSectionHint",
"Temporary testing controls for native capture and motion tuning.",
)}
</div>
</div>
<span className="rounded-full bg-[#2563EB]/10 px-2 py-0.5 text-xs font-medium uppercase tracking-wider text-[#2563EB]">
DEV
</span>
</div>
<div className="rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings(
"effects.nativeCaptureWarningTester",
"Native capture warning",
)}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
{nativeCaptureUnavailableSession
? tSettings(
"effects.nativeCaptureWarningTesterUnavailable",
"This project is currently marked as native capture unavailable.",
)
: tSettings(
"effects.nativeCaptureWarningTesterAvailable",
"This project is not marked as unsupported, but you can still open the modal for UI testing.",
)}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => onOpenNativeCaptureUnavailableModal?.()}
className="h-8 shrink-0"
>
{tSettings("effects.openNativeCaptureWarning", "Open warning")}
</Button>
</div>
</div>
<div className="space-y-1.5 rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
<div className="flex items-center justify-between gap-3 py-2">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings("effects.cameraDebugTuning", "Camera Debug Tuning")}
{tSettings("effects.connectZooms", "Connect neighboring zooms")}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
<div className="mt-0.5 text-xs text-muted-foreground/70">
{tSettings(
"effects.cameraDebugTuningHint",
"Development-only spring tuning controls for camera motion.",
"effects.connectZoomsDescription",
"Smooth consecutive zoom regions into a continuous camera move.",
)}
</div>
</div>
<SliderControl
label={tSettings(
"effects.cameraSpringStiffnessMultiplier",
"Camera stiffness",
<Switch
aria-label={tSettings(
"effects.connectZooms",
"Connect neighboring zooms",
)}
value={cameraSpringStiffnessMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) =>
onCameraSpringStiffnessMultiplierChange?.(value)
}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
<SliderControl
label={tSettings(
"effects.cameraSpringDampingMultiplier",
"Camera damping",
)}
value={cameraSpringDampingMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) => onCameraSpringDampingMultiplierChange?.(value)}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
<SliderControl
label={tSettings(
"effects.cameraSpringMassMultiplier",
"Camera mass",
)}
value={cameraSpringMassMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) => onCameraSpringMassMultiplierChange?.(value)}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
</div>
<div className="space-y-1.5 rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings("effects.cursorDebugTuning", "Cursor Debug Tuning")}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
{tSettings(
"effects.cursorDebugTuningHint",
"Development-only spring tuning controls.",
)}
</div>
</div>
<SliderControl
label={tSettings(
"effects.cursorSpringStiffnessMultiplier",
"Spring stiffness",
)}
value={cursorSpringStiffnessMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) =>
onCursorSpringStiffnessMultiplierChange?.(value)
}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
<SliderControl
label={tSettings(
"effects.cursorSpringDampingMultiplier",
"Spring damping",
)}
value={cursorSpringDampingMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) => onCursorSpringDampingMultiplierChange?.(value)}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
<SliderControl
label={tSettings(
"effects.cursorSpringMassMultiplier",
"Spring mass",
)}
value={cursorSpringMassMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) => onCursorSpringMassMultiplierChange?.(value)}
formatValue={(value) => `${value.toFixed(2)}×`}
checked={connectZooms}
onCheckedChange={onConnectZoomsChange}
/>
</div>
</section>
) : null}
</div>
<section className="flex flex-col gap-4">
<MotionPresetCards
title={tSettings("effects.motionPresetsTitle", "Motion Presets")}
activePresetId={activeMotionPresetId}
onApply={applyMotionPreset}
tSettings={tSettings}
/>
</section>
</SettingsCategory>
<SettingsCategory category="general">
<section className="flex flex-col gap-4">
<SectionLabel>{t("editor.keyboardShortcuts.title")}</SectionLabel>
<KeyboardShortcutsDialog
triggerLabel={t("editor.keyboardShortcuts.customize")}
triggerClassName="h-9 w-full justify-start rounded-xl border border-foreground/10 bg-foreground/5 px-3 text-sm text-foreground hover:bg-foreground/10 hover:text-foreground"
/>
</section>
</SettingsCategory>
<SettingsCategory category="advanced">
{advanced && showDevMotionControls ? (
<section className="flex flex-col gap-4 rounded-xl border border-[#2563EB]/15 bg-[#2563EB]/5 p-3">
<div className="flex items-center justify-between gap-3">
<div>
<SectionLabel>
{tSettings("effects.devSection", "Dev")}
</SectionLabel>
<div className="mt-0.5 text-xs text-muted-foreground">
{tSettings(
"effects.devSectionHint",
"Temporary testing controls for native capture and motion tuning.",
)}
</div>
</div>
<span className="rounded-full bg-[#2563EB]/10 px-2 py-0.5 text-xs font-medium uppercase tracking-wider text-[#2563EB]">
DEV
</span>
</div>
<div className="rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings(
"effects.nativeCaptureWarningTester",
"Native capture warning",
)}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
{nativeCaptureUnavailableSession
? tSettings(
"effects.nativeCaptureWarningTesterUnavailable",
"This project is currently marked as native capture unavailable.",
)
: tSettings(
"effects.nativeCaptureWarningTesterAvailable",
"This project is not marked as unsupported, but you can still open the modal for UI testing.",
)}
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => onOpenNativeCaptureUnavailableModal?.()}
className="h-8 shrink-0"
>
{tSettings(
"effects.openNativeCaptureWarning",
"Open warning",
)}
</Button>
</div>
</div>
<div className="space-y-1.5 rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings(
"effects.cameraDebugTuning",
"Camera Debug Tuning",
)}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
{tSettings(
"effects.cameraDebugTuningHint",
"Development-only spring tuning controls for camera motion.",
)}
</div>
</div>
<SliderControl
label={tSettings(
"effects.cameraSpringStiffnessMultiplier",
"Camera stiffness",
)}
value={cameraSpringStiffnessMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) =>
onCameraSpringStiffnessMultiplierChange?.(value)
}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
<SliderControl
label={tSettings(
"effects.cameraSpringDampingMultiplier",
"Camera damping",
)}
value={cameraSpringDampingMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) =>
onCameraSpringDampingMultiplierChange?.(value)
}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
<SliderControl
label={tSettings(
"effects.cameraSpringMassMultiplier",
"Camera mass",
)}
value={cameraSpringMassMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) =>
onCameraSpringMassMultiplierChange?.(value)
}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
</div>
<div className="space-y-1.5 rounded-lg border border-foreground/10 bg-background/60 px-3 py-3">
<div>
<div className="text-sm font-medium text-foreground">
{tSettings(
"effects.cursorDebugTuning",
"Cursor Debug Tuning",
)}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
{tSettings(
"effects.cursorDebugTuningHint",
"Development-only spring tuning controls.",
)}
</div>
</div>
<SliderControl
label={tSettings(
"effects.cursorSpringStiffnessMultiplier",
"Spring stiffness",
)}
value={cursorSpringStiffnessMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) =>
onCursorSpringStiffnessMultiplierChange?.(value)
}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
<SliderControl
label={tSettings(
"effects.cursorSpringDampingMultiplier",
"Spring damping",
)}
value={cursorSpringDampingMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) =>
onCursorSpringDampingMultiplierChange?.(value)
}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
<SliderControl
label={tSettings(
"effects.cursorSpringMassMultiplier",
"Spring mass",
)}
value={cursorSpringMassMultiplier}
min={0.25}
max={3}
step={0.01}
onChange={(value) =>
onCursorSpringMassMultiplierChange?.(value)
}
formatValue={(value) => `${value.toFixed(2)}×`}
/>
</div>
</section>
) : null}
</SettingsCategory>
</SettingsSections>
);
const sceneSectionContent = (
@@ -0,0 +1,61 @@
import { createContext, useContext, useState, type ReactNode } from "react";
import { ChoiceGroup, ChoiceItem } from "@/components/ui/choice-group";
type Category = "general" | "motion" | "recording" | "files" | "advanced";
const labels: Record<Category, string> = {
general: "General",
motion: "Motion",
recording: "Recording",
files: "Files",
advanced: "Advanced",
};
const SettingsCategoryContext = createContext<Category | null>(null);
/** Dashboard and editor share the same controls and category selection. */
export function SettingsSections({
children,
categories,
}: {
children: ReactNode;
categories: Category[];
}) {
const parent = useContext(SettingsCategoryContext);
const [selected, setSelected] = useState<Category>("general");
if (parent) return <div className="space-y-6">{children}</div>;
const active = categories.includes(selected) ? selected : categories[0];
return (
<SettingsCategoryContext.Provider value={active}>
<div className="space-y-6">
<ChoiceGroup
aria-label="Settings sections"
value={active}
onValueChange={(value) => setSelected(value as Category)}
size="sm"
>
{categories.map((category) => (
<ChoiceItem key={category} value={category}>
{labels[category]}
</ChoiceItem>
))}
</ChoiceGroup>
<div role="region" aria-label={`${labels[active]} settings`} className="space-y-6">
{children}
</div>
</div>
</SettingsCategoryContext.Provider>
);
}
export function SettingsCategory({
category,
children,
}: {
category: Category | Category[];
children: ReactNode;
}) {
const active = useContext(SettingsCategoryContext);
const visible = Array.isArray(category)
? active !== null && category.includes(active)
: active === category;
return visible ? <div className="space-y-6">{children}</div> : null;
}
@@ -1,3 +1,4 @@
import { SettingsSections, SettingsCategory } from "../SettingsSections";
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
import { Switch } from "@/components/ui/switch";
import { supportsHudCaptureProtection } from "@/lib/hudCaptureProtection";
@@ -42,119 +43,131 @@ export function DashboardSettings({ onImportFile }: { onImportFile: () => Promis
return (
<section aria-label="Dashboard settings" className="dashboard-settings max-w-2xl py-10">
<h1 className="mb-8 text-lg font-semibold">Settings</h1>
<div className="space-y-8">
{settingsContent}
<div className="flex items-center justify-between gap-8">
<p className="text-sm">Open video or project</p>
<Button
variant="secondary"
disabled={busy}
onClick={() => void run(onImportFile)}
>
Open file
</Button>
</div>
<div className="flex items-center justify-between gap-8">
<div className="min-w-0">
<p className="text-sm">Recordings folder</p>
<p
className="mt-1 truncate text-xs text-muted-foreground"
title={recordings}
>
{recordings}
</p>
</div>
<Button
variant="secondary"
disabled={busy}
onClick={() =>
void run(async () => {
const result = await window.electronAPI.chooseRecordingsDirectory();
if (result.canceled) return;
if (!result.success || !result.path)
throw Error("Could not change recordings folder");
setRecordings(result.path);
})
}
>
Change folder
</Button>
</div>
{captureSupported && (
<SettingsSections categories={["general", "motion", "recording", "files", "advanced"]}>
<SettingsCategory category={["general", "motion", "advanced"]}>
{settingsContent}
</SettingsCategory>
<SettingsCategory category="files">
<div className="flex items-center justify-between gap-8">
<div>
<p className="text-sm">Hide HUD from recordings</p>
<p className="mt-1 text-xs text-muted-foreground">
Only while recording. The idle HUD stays visible in captures.
<p className="text-sm">Open video or project</p>
<Button
variant="secondary"
disabled={busy}
onClick={() => void run(onImportFile)}
>
Open file
</Button>
</div>
</SettingsCategory>
<SettingsCategory category="recording">
<div className="flex items-center justify-between gap-8">
<div className="min-w-0">
<p className="text-sm">Recordings folder</p>
<p
className="mt-1 truncate text-xs text-muted-foreground"
title={recordings}
>
{recordings}
</p>
</div>
<Switch
aria-label="Hide HUD from recordings"
checked={hideHud}
disabled={busy}
onCheckedChange={(enabled) =>
void run(async () => {
const result =
await window.electronAPI.setHudOverlayCaptureProtection(
enabled,
);
if (!result.success)
throw Error("Could not update capture protection");
setHideHud(result.enabled);
})
}
/>
</div>
)}
{import.meta.env.DEV && (
<div className="flex items-center justify-between gap-8">
<p className="text-sm">Preview update UI</p>
<Button
variant="secondary"
disabled={busy}
onClick={() =>
void run(async () => {
await window.electronAPI.previewUpdateToast();
const result =
await window.electronAPI.chooseRecordingsDirectory();
if (result.canceled) return;
if (!result.success || !result.path)
throw Error("Could not change recordings folder");
setRecordings(result.path);
})
}
>
Preview
Change folder
</Button>
</div>
)}
<div className="flex items-center justify-between gap-8">
<div>
<p className="text-sm">Projects folder</p>
{directory && (
<p
className="mt-1 max-w-xs truncate text-xs text-muted-foreground"
title={directory}
{captureSupported && (
<div className="flex items-center justify-between gap-8">
<div>
<p className="text-sm">Hide HUD from recordings</p>
<p className="mt-1 text-xs text-muted-foreground">
Only while recording. The idle HUD stays visible in captures.
</p>
</div>
<Switch
aria-label="Hide HUD from recordings"
checked={hideHud}
disabled={busy}
onCheckedChange={(enabled) =>
void run(async () => {
const result =
await window.electronAPI.setHudOverlayCaptureProtection(
enabled,
);
if (!result.success)
throw Error("Could not update capture protection");
setHideHud(result.enabled);
})
}
/>
</div>
)}
</SettingsCategory>
<SettingsCategory category="advanced">
{import.meta.env.DEV && (
<div className="flex items-center justify-between gap-8">
<p className="text-sm">Preview update UI</p>
<Button
variant="secondary"
disabled={busy}
onClick={() =>
void run(async () => {
await window.electronAPI.previewUpdateToast();
})
}
>
{directory}
</p>
)}
Preview
</Button>
</div>
)}
</SettingsCategory>
<SettingsCategory category="files">
<div className="flex items-center justify-between gap-8">
<div>
<p className="text-sm">Projects folder</p>
{directory && (
<p
className="mt-1 max-w-xs truncate text-xs text-muted-foreground"
title={directory}
>
{directory}
</p>
)}
</div>
<Button
variant="secondary"
onClick={async () => {
try {
const result = await window.electronAPI.getProjectsDirectory();
if (!result.success || !result.path)
throw Error("Could not open projects folder");
setDirectory(result.path);
await window.electronAPI.revealInFolder(result.path);
} catch (e) {
toast.error(String(e));
}
}}
>
Show folder
</Button>
</div>
<Button
variant="secondary"
onClick={async () => {
try {
const result = await window.electronAPI.getProjectsDirectory();
if (!result.success || !result.path)
throw Error("Could not open projects folder");
setDirectory(result.path);
await window.electronAPI.revealInFolder(result.path);
} catch (e) {
toast.error(String(e));
}
}}
>
Show folder
</Button>
</div>
<p className="text-xs text-muted-foreground">
Named projects save automatically. Previews refresh when you return to Projects.
</p>
</div>
<p className="text-xs text-muted-foreground">
Named projects save automatically. Previews refresh when you return to
Projects.
</p>
</SettingsCategory>
</SettingsSections>
</section>
);
}
@@ -3,6 +3,7 @@ import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback
import {
clampFocusToDepth,
DEFAULT_AUTO_ZOOM_DEPTH,
DEFAULT_ZOOM_DEPTH,
type EditorEffectSection,
type ZoomDepth,
type ZoomFocus,
@@ -72,7 +73,7 @@ export function useZoomRegionCommands({
const handleZoomAdded = useCallback(
(span: Span) => {
const id = `zoom-${nextZoomIdRef.current++}`;
const depth: ZoomDepth = 2;
const depth = DEFAULT_ZOOM_DEPTH;
const newRegion: ZoomRegion = {
id,
startMs: Math.round(span.start),
+1 -1
View File
@@ -647,7 +647,7 @@ export const ZOOM_DEPTH_SCALES: Record<ZoomDepth, number> = {
};
export const DEFAULT_ZOOM_DEPTH: ZoomDepth = 3;
export const DEFAULT_AUTO_ZOOM_DEPTH: ZoomDepth = 2;
export const DEFAULT_AUTO_ZOOM_DEPTH: ZoomDepth = DEFAULT_ZOOM_DEPTH;
export function clampFocusToDepth(focus: ZoomFocus, _depth: ZoomDepth): ZoomFocus {
return {
+1
View File
@@ -30,6 +30,7 @@ test("editor inspector and playback controls fit a smaller desktop", async ({ pa
await expect(page.getByRole("button", { name: "Export", exact: true })).toBeInViewport();
await expect(page.getByRole("button", { name: "Play", exact: true })).toBeInViewport();
await page.getByRole("radio", { name: "Settings", exact: true }).click();
await page.getByRole("row", { name: "Motion", exact: true }).click();
await expect(
page.getByRole("radiogroup", { name: "Motion Presets", exact: true }),
).toBeVisible();
+11
View File
@@ -140,3 +140,14 @@ test("header name edits in place and saves on blur without a boxed input", async
await input.press("Escape");
await expect(page.getByRole("button", { name: "Rename project" })).toContainText("Launch demo");
});
test("new zoom blocks default to 1.8x", async ({ page }) => {
await installDesktopBridge(page);
await page.goto("/?windowType=editor");
await page.getByRole("button", { name: "Add Zoom (Z)", exact: true }).click();
await page.locator('[data-variant="zoom"] .timeline-block').first().click();
await expect(page.getByRole("row", { name: "1.8×", exact: true })).toHaveAttribute(
"aria-selected",
"true",
);
});
+8 -32
View File
@@ -64,7 +64,9 @@ test("editor loads video, switches tools and edits export options", async ({ pag
animations: "disabled",
});
await page.getByRole("button", { name: "Home", exact: true }).click();
await expect(page.getByRole("dialog", { name: "Projects", exact: true })).toBeVisible();
await expect(
page.getByRole("dialog", { name: "Projects dashboard", exact: true }),
).toBeVisible();
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "Crop Video", exact: true }).click();
await expect(page.getByRole("dialog", { name: "Crop Video", exact: true })).toBeVisible();
@@ -72,15 +74,7 @@ test("editor loads video, switches tools and edits export options", async ({ pag
await page.getByRole("button", { name: "16:9", exact: true }).click();
await page.getByRole("menuitem", { name: "1:1", exact: true }).click();
await expect(page.getByRole("button", { name: "1:1", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Open presets", exact: true }).click();
await page
.getByRole("textbox", { name: "Preset name", exact: true })
.fill("HeroUI test preset");
await page.getByRole("button", { name: "Save", exact: true }).click();
await expect(
page.getByRole("button", { name: "Delete preset HeroUI test preset", exact: true }),
).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("button", { name: "Open presets", exact: true })).toHaveCount(0);
await page.getByRole("button", { name: "Add Layer", exact: true }).click();
await page.getByRole("menuitem", { name: "Annotation", exact: true }).click();
await expect
@@ -117,27 +111,9 @@ test("recorder opens device and source popovers", async ({ page }) => {
await page.getByRole("button", { name: "Built-in Display", exact: true }).click();
await expect(page.getByRole("dialog")).toBeVisible();
await page.keyboard.press("Escape");
await page.getByRole("button", { name: "More", exact: true }).click();
await page.getByRole("button", { name: "Dark", exact: true }).click();
await expect(page.locator("html")).toHaveClass(/dark/);
await page.screenshot({
path: "test-results/hud-dark.png",
fullPage: true,
animations: "disabled",
});
await page.getByRole("button", { name: "More", exact: true }).click();
await page.getByRole("button", { name: "Light", exact: true }).click();
await page.getByRole("button", { name: "More", exact: true }).click();
await page
.getByRole("dialog")
.getByRole("button", { name: "Open project", exact: false })
.click();
await expect(page.getByText("No saved projects yet", { exact: true })).toBeVisible();
await page.keyboard.press("Escape");
await page.screenshot({
path: "test-results/hud-light.png",
fullPage: true,
animations: "disabled",
});
await expect(page.getByRole("button", { name: "More", exact: true })).toHaveCount(0);
await page.getByRole("button", { name: "Home", exact: true }).click();
await expect(page.locator("html")).toHaveAttribute("data-dashboard-opened", "true");
expect(errors).toEqual([]);
});
+12 -2
View File
@@ -381,17 +381,27 @@ test("dashboard supports creation sort, independent folders, shared settings and
await expect(home.getByRole("textbox", { name: "Search projects" })).toHaveCount(0);
await expect(home.getByRole("button", { name: "Sort projects" })).toHaveCount(0);
await expect(home.getByRole("row", { name: "Dark", exact: true })).toBeVisible();
await expect(home.getByRole("grid", { name: "Settings sections" })).toHaveCount(1);
await expect(home.getByRole("switch", { name: "Connect Zooms" })).toHaveCount(0);
await home.getByRole("row", { name: "Advanced", exact: true }).click();
await expect(home.getByRole("switch", { name: "Experimental updates" })).toBeVisible();
await expect(home.getByText("Preview update UI", { exact: true })).toBeVisible();
await home.getByRole("row", { name: "Motion", exact: true }).click();
await expect(home.getByRole("switch", { name: "Connect Zooms" })).toBeVisible();
await home.getByRole("row", { name: "Recording", exact: true }).click();
await expect(home.getByText("Recordings folder", { exact: true })).toBeVisible();
await home.getByRole("button", { name: "Change folder" }).click();
await expect(home.getByText("/new-recordings", { exact: true })).toBeVisible();
const capture = home.getByRole("switch", { name: "Hide HUD from recordings" });
await capture.press("Space");
await expect(page.locator("html")).toHaveAttribute("data-hide-hud", "true");
await home.getByRole("row", { name: "Files", exact: true }).click();
await expect(home.getByRole("button", { name: "Open file", exact: true })).toBeVisible();
await expect(home.getByText("Preview update UI", { exact: true })).toBeVisible();
await page.screenshot({ path: "test-results/dashboard-settings.png" });
await home.getByRole("row", { name: "Recording", exact: true }).click();
await expect(home.getByText("/new-recordings", { exact: true })).toBeVisible();
await home.getByRole("row", { name: "General", exact: true }).click();
await expect(home.getByRole("row", { name: "Dark", exact: true })).toBeVisible();
await page.screenshot({ path: "test-results/dashboard-settings.png", animations: "disabled" });
});
test("Solar navigation selection, circular initials, and Raw sources are consistent", async ({