Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/check-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const LEGACY = [
"Bottombar",
"Titlebar",
"TranscriptEditor",
"BackgroundPane",
"ai-edition-roadmap",
"ai-edition-collision-analysis",
"openscreen-inventory",
Expand Down
141 changes: 141 additions & 0 deletions src/components/ai-edition/NewEditorShell.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -987,6 +987,99 @@
color: var(--fg-2);
margin: 4px var(--sp-4) 8px;
}
/* ─── action menu (a button's list of things it could do) ─────── */
/* Sits where a Toggle would; reads as the current value, not as on/off. */
.rowAction {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 8px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--fg-2);
font: 500 var(--fs-app-sm)/1 var(--font-body);
font-variant-numeric: tabular-nums;
cursor: pointer;
transition: border-color 150ms ease, color 150ms ease;
}
.rowAction:hover:not(:disabled) { border-color: var(--brand); color: var(--fg); }
.rowAction:disabled { cursor: default; opacity: 0.5; }
.actionMenu {
min-width: 216px;
padding: 6px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface-1);
box-shadow: var(--elev-pop);
/* Ten shapes must cost no more room than two. */
max-height: min(var(--radix-popover-content-available-height, 60vh), 320px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.actionMenuRow {
display: flex;
align-items: baseline;
gap: 8px;
width: 100%;
padding: 7px 8px;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--fg);
cursor: pointer;
text-align: left;
}
.actionMenuRow:hover { background: var(--surface-2); }
.actionMenuRow.isActive { background: color-mix(in oklab, var(--brand), transparent 82%); }
/* Separates the fixed presets from the shapes this timeline actually holds. */
.actionMenuGroup {
padding: 8px 8px 4px;
font: 600 10px/1 var(--font-body);
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
/* Resolution leads: it is the thing a user recognises about their own footage. */
.actionMenuMain { font: 600 12.5px var(--font-display); font-variant-numeric: tabular-nums; }
/* The ratio token second, for whoever does read them — `64:27` means nothing alone. */
.actionMenuMeta { font: 400 11px var(--font-body); color: var(--muted); }
.actionMenuCount {
margin-left: auto;
font: 500 11px var(--font-body);
font-variant-numeric: tabular-nums;
color: var(--meta);
}

/* A section title that carries an action on its right. */
.sectionHead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 4px var(--sp-4) 8px;
}
.sectionHead .sectionLabel { margin: 0; }
/* Deliberately quieter than the settings it overwrites: it is reached on purpose, not
stumbled into, and it must not read as a fifth control in the group. */
.sectionAction {
padding: 3px 8px;
border: 1px solid transparent;
border-radius: 999px;
background: var(--surface-2);
color: var(--muted);
font: 500 var(--fs-app-sm)/1.4 var(--font-body);
white-space: nowrap;
cursor: pointer;
transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
}
.sectionAction:hover:not(:disabled) {
border-color: var(--brand);
background: var(--bg);
color: var(--fg);
}
.sectionAction:disabled { cursor: default; opacity: 0.45; }
.uploadBtn {
display: flex;
align-items: center;
Expand Down Expand Up @@ -1050,6 +1143,54 @@
box-shadow: 0 0 0 2px color-mix(in oklab, var(--brand), transparent 70%);
}

/* ─── background picker (collapsed trigger + floating panel) ───── */
/* The trigger paints the CURRENT background as its own surface, so the pane shows what is
selected without spending the ~300px the inline swatch grid used to cost. */
.bgTrigger {
display: flex;
align-items: flex-end;
justify-content: flex-end;
height: 52px;
margin: 0 var(--sp-4) 12px;
padding: 6px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface-2);
cursor: pointer;
overflow: hidden;
transition: border-color 150ms ease;
}
.bgTrigger:hover { border-color: var(--brand); }
/* Sits ON an arbitrary photo, so it carries its own ground rather than trusting contrast. */
.bgTriggerChip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 7px;
border-radius: 999px;
background: color-mix(in oklab, var(--bg), transparent 10%);
color: var(--fg);
font: 500 var(--fs-app-sm)/1 var(--font-body);
}
.bgPopover {
width: 300px;
padding-top: 4px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface-1);
box-shadow: var(--elev-pop);
/* Radix measures the room actually left below the trigger and publishes it here, so the
picker uses the screen it has instead of a number guessed at authoring time. The second
term is the tallest it ever needs: the gradient tab is ~640px (tabs + preset grid + the
240px-wide editor, whose wheel is square). A fixed 460px cap cut that editor in half and
read as "the gradient panel is gone" — the content was there, below the fold, behind a
scroll nobody could see. Beyond 680px there is nothing left to reveal. */
max-height: min(var(--radix-popover-content-available-height, 80vh), 680px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}

/* ─── toggle (switch) ──────────────────────────────────────────── */
.toggle {
width: 36px;
Expand Down
30 changes: 22 additions & 8 deletions src/components/ai-edition/RightPanes.i18n.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
// localized text is what actually reaches the DOM.

import "@testing-library/jest-dom";
import { cleanup, render, screen } from "@testing-library/react";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import type { ReactElement } from "react";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { I18nProvider } from "@/contexts/I18nContext";
import { LOCALE_STORAGE_KEY } from "@/i18n/config";
import { BackgroundPane, CursorPane, LayoutPane, VideoEffectsPane } from "./RightPanes";
import { CursorPane, LayoutPane, VideoEffectsPane } from "./RightPanes";

function renderIn(locale: string, ui: ReactElement) {
localStorage.setItem(LOCALE_STORAGE_KEY, locale);
Expand All @@ -28,25 +28,39 @@ afterEach(() => {
});

describe("right-rail panes are localized", () => {
it("renders the background pane in French", () => {
renderIn("fr", <BackgroundPane />);
expect(screen.getByRole("heading", { name: "Arrière-plan" })).toBeInTheDocument();
it("renders the background section of the effects pane in French", () => {
// Background stopped being its own pane when the two facets merged; it is a section
// of the effects pane now, so the same strings must survive under the new heading.
renderIn("fr", <VideoEffectsPane />);
expect(screen.getByRole("heading", { name: "Composition" })).toBeInTheDocument();
expect(screen.getByText("Arrière-plan")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Aide" })).toBeInTheDocument();
});

it("renders the floating background picker in French once it is opened", () => {
// The picker collapsed into a popover so the frame sliders stay above the fold, so
// its strings are only in the DOM after the trigger is clicked — the assertion has
// to open it, or it would pass on an empty document forever.
renderIn("fr", <VideoEffectsPane />);
fireEvent.click(screen.getByRole("button", { name: "Arrière-plan" }));
expect(screen.getByRole("button", { name: "Téléverser une image" })).toBeInTheDocument();
// wallpaper swatches interpolate their index through the catalog
expect(screen.getByRole("button", { name: "Fond 1" })).toBeInTheDocument();
});

it("renders the video-effects pane in Spanish", () => {
renderIn("es", <VideoEffectsPane />);
expect(screen.getByRole("heading", { name: "Efectos de video" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Composición" })).toBeInTheDocument();
expect(screen.getByText("Desenfoque de movimiento")).toBeInTheDocument();
expect(screen.getByText("Sombra")).toBeInTheDocument();
// the merged pane's own section headers
expect(screen.getByText("Marco")).toBeInTheDocument();
expect(screen.getByText("Movimiento")).toBeInTheDocument();
});

it("renders the layout pane in Japanese", () => {
renderIn("ja-JP", <LayoutPane />);
expect(screen.getByRole("heading", { name: "レイアウト" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "カメラレイアウト" })).toBeInTheDocument();
// the preset <option> labels come from the shared layout.* catalog
expect(screen.getByRole("option", { name: "ピクチャーインピクチャ" })).toBeInTheDocument();
});
Expand All @@ -60,7 +74,7 @@ describe("right-rail panes are localized", () => {

it("falls back to English when the locale is English", () => {
renderIn("en", <VideoEffectsPane />);
expect(screen.getByRole("heading", { name: "Video Effects" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Composition" })).toBeInTheDocument();
expect(screen.getByText("Blur BG")).toBeInTheDocument();
});
});
4 changes: 2 additions & 2 deletions src/components/ai-edition/RightPanes.layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe("LayoutPane camera availability", () => {
it("shows No webcam without overwriting the saved camera preset", () => {
renderLayout(seedProject(false));

const preset = screen.getByRole("combobox", { name: "Layout" });
const preset = screen.getByRole("combobox", { name: "Preset" });
expect(preset).toBeDisabled();
expect(preset).toHaveValue("no-webcam");
expect(useProjectStore.getState().document?.legacyEditor).toMatchObject({
Expand All @@ -111,7 +111,7 @@ describe("LayoutPane camera availability", () => {
const user = userEvent.setup();
renderLayout(seedProject(true));

const preset = screen.getByRole("combobox", { name: "Layout" });
const preset = screen.getByRole("combobox", { name: "Preset" });
expect(preset).toBeEnabled();
expect(preset).toHaveValue("picture-in-picture");
expect(screen.getByText("Camera Shape")).toBeInTheDocument();
Expand Down
Loading
Loading