From 81f0b8e11c09bc3eb2df0c0d9d141a30ebf25360 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Wed, 24 Sep 2025 15:12:20 -0400 Subject: [PATCH 1/3] Fix useEffect on tabify --- .../playground/components/Editor/Output.tsx | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/compiler/apps/playground/components/Editor/Output.tsx b/compiler/apps/playground/components/Editor/Output.tsx index 331aa66bcbe..bb6b60009f3 100644 --- a/compiler/apps/playground/components/Editor/Output.tsx +++ b/compiler/apps/playground/components/Editor/Output.tsx @@ -19,8 +19,8 @@ import { import parserBabel from 'prettier/plugins/babel'; import * as prettierPluginEstree from 'prettier/plugins/estree'; import * as prettier from 'prettier/standalone'; -import {memo, ReactNode, useEffect, useState} from 'react'; import {type Store} from '../../lib/stores'; +import {memo, ReactNode, use, useState, Suspense} from 'react'; import AccordionWindow from '../AccordionWindow'; import TabbedWindow from '../TabbedWindow'; import {monacoOptions} from './monacoOptions'; @@ -32,6 +32,8 @@ export default MemoizedOutput; export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap']; +let tabifyCache = new Map>>(); + export type PrintedCompilerPipelineValue = | { kind: 'hir'; @@ -200,6 +202,28 @@ ${code} return reorderedTabs; } +function tabifyCached( + store: Store, + compilerOutput: CompilerOutput, +): Promise> { + const key = store; + if (tabifyCache.has(key)) { + return tabifyCache.get(key)!; + } + const result = tabify(store.source, compilerOutput, store.showInternals); + tabifyCache.set(key, result); + return result; +} + +function Fallback(): JSX.Element { + console.log('Fallback'); + return ( +
+ Loading... +
+ ); +} + function utf16ToUTF8(s: string): string { return unescape(encodeURIComponent(s)); } @@ -213,12 +237,17 @@ function getSourceMapUrl(code: string, map: string): string | null { } function Output({store, compilerOutput}: Props): JSX.Element { + return ( + }> + + + ); +} + +function OutputContent({store, compilerOutput}: Props): JSX.Element { const [tabsOpen, setTabsOpen] = useState>( () => new Set(['Output']), ); - const [tabs, setTabs] = useState>( - () => new Map(), - ); const [activeTab, setActiveTab] = useState('Output'); /* @@ -233,13 +262,6 @@ function Output({store, compilerOutput}: Props): JSX.Element { setTabsOpen(new Set(['Output'])); setActiveTab('Output'); } - - useEffect(() => { - tabify(store.source, compilerOutput, store.showInternals).then(tabs => { - setTabs(tabs); - }); - }, [store.source, compilerOutput, store.showInternals]); - const changedPasses: Set = new Set(['Output', 'HIR']); // Initial and final passes should always be bold let lastResult: string = ''; for (const [passName, results] of compilerOutput.results) { @@ -254,6 +276,7 @@ function Output({store, compilerOutput}: Props): JSX.Element { lastResult = currResult; } } + const tabs = use(tabifyCached(store, compilerOutput)); if (!store.showInternals) { return ( From aecfb119111ea4988cf412ff09aa151364c6da37 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Wed, 24 Sep 2025 15:26:07 -0400 Subject: [PATCH 2/3] Add ViewTransition on config editor open/close --- .../playground/__tests__/e2e/page.spec.ts | 3 +- .../components/Editor/ConfigEditor.tsx | 178 ++++++++++-------- .../components/Editor/EditorImpl.tsx | 27 ++- compiler/apps/playground/next.config.js | 1 + compiler/apps/playground/styles/globals.css | 40 ++++ 5 files changed, 161 insertions(+), 88 deletions(-) diff --git a/compiler/apps/playground/__tests__/e2e/page.spec.ts b/compiler/apps/playground/__tests__/e2e/page.spec.ts index 4a10e8603bb..94abe40eeb8 100644 --- a/compiler/apps/playground/__tests__/e2e/page.spec.ts +++ b/compiler/apps/playground/__tests__/e2e/page.spec.ts @@ -23,7 +23,8 @@ function formatPrint(data: Array): Promise { async function expandConfigs(page: Page): Promise { const expandButton = page.locator('[title="Expand config editor"]'); - expandButton.click(); + await expandButton.click(); + await page.waitForSelector('.monaco-editor-config', {state: 'visible'}); } const TEST_SOURCE = `export default function TestComponent({ x }) { diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index c70cd10ba53..59ab2e5fdde 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -9,7 +9,13 @@ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; import {PluginOptions} from 'babel-plugin-react-compiler'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; -import React, {useState, useRef} from 'react'; +import React, { + useState, + useRef, + unstable_ViewTransition as ViewTransition, + unstable_addTransitionType as addTransitionType, + startTransition, +} from 'react'; import {Resizable} from 're-resizable'; import {useStore, useStoreDispatch} from '../StoreContext'; import {monacoOptions} from './monacoOptions'; @@ -36,7 +42,12 @@ export default function ConfigEditor({ display: isExpanded ? 'block' : 'none', }}> { + startTransition(() => { + addTransitionType('config-panel'); + setIsExpanded(false); + }); + }} appliedOptions={appliedOptions} /> @@ -44,7 +55,14 @@ export default function ConfigEditor({ style={{ display: !isExpanded ? 'block' : 'none', }}> - + { + startTransition(() => { + addTransitionType('config-panel'); + setIsExpanded(true); + }); + }} + />{' '} ); @@ -54,7 +72,7 @@ function ExpandedEditor({ onToggle, appliedOptions, }: { - onToggle: (expanded: boolean) => void; + onToggle: () => void; appliedOptions: PluginOptions | null; }): React.ReactElement { const store = useStore(); @@ -111,90 +129,92 @@ function ExpandedEditor({ : 'Invalid configs'; return ( - -
-
onToggle(false)} - style={{ - top: '50%', - marginTop: '-32px', - right: '-32px', - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - }}> - -
- -
-
-

- Config Overrides -

-
-
- + + +
+
+
-
-
-
-

- Applied Configs -

+ +
+
+

+ Config Overrides +

+
+
+ +
-
- +
+
+

+ Applied Configs +

+
+
+ +
-
- + + ); } function CollapsedEditor({ onToggle, }: { - onToggle: (expanded: boolean) => void; + onToggle: () => void; }): React.ReactElement { return (
onToggle(true)} + onClick={onToggle} style={{ top: '50%', marginTop: '-32px', diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index 696bbd2559c..90ed8989793 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -24,7 +24,11 @@ import BabelPluginReactCompiler, { printFunctionWithOutlined, type LoggerEvent, } from 'babel-plugin-react-compiler'; -import {useDeferredValue, useMemo} from 'react'; +import { + useDeferredValue, + useMemo, + unstable_ViewTransition as ViewTransition, +} from 'react'; import {useStore} from '../StoreContext'; import ConfigEditor from './ConfigEditor'; import Input from './Input'; @@ -342,14 +346,21 @@ export default function Editor(): JSX.Element {
-
-
- -
-
- + + +
+
+ +
+
+ +
-
+
); diff --git a/compiler/apps/playground/next.config.js b/compiler/apps/playground/next.config.js index fc8a9492e4e..f34f958ec6d 100644 --- a/compiler/apps/playground/next.config.js +++ b/compiler/apps/playground/next.config.js @@ -11,6 +11,7 @@ const path = require('path'); const nextConfig = { experimental: { reactCompiler: true, + viewTransition: true, }, reactStrictMode: true, webpack: (config, options) => { diff --git a/compiler/apps/playground/styles/globals.css b/compiler/apps/playground/styles/globals.css index c4558eb8b8a..3ff8bc0e39c 100644 --- a/compiler/apps/playground/styles/globals.css +++ b/compiler/apps/playground/styles/globals.css @@ -69,3 +69,43 @@ scrollbar-width: none; /* Firefox */ } } + +::view-transition-old(.slide-in) { + animation-name: slideOutLeft; +} +::view-transition-new(.slide-in) { + animation-name: slideInLeft; +} +::view-transition-group(.slide-in) { + z-index: 1; +} + +@keyframes slideOutLeft { + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } +} +@keyframes slideInLeft { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} +@keyframes slideInRight { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} + +::view-transition-old(.container), +::view-transition-new(.container) { + height: 100%; +} From ab7d7e1f4c59e80f9933646930de737b1c9b676d Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Thu, 25 Sep 2025 16:59:50 -0400 Subject: [PATCH 3/3] Improve config sliding animation --- .../playground/components/AccordionWindow.tsx | 28 +++++------ .../components/Editor/ConfigEditor.tsx | 10 ++-- .../components/Editor/EditorImpl.tsx | 25 ++-------- .../playground/components/Editor/Input.tsx | 20 ++++++-- .../playground/components/Editor/Output.tsx | 46 +++++++++++++------ .../playground/components/TabbedWindow.tsx | 42 +++++++++-------- .../apps/playground/lib/transitionTypes.ts | 8 ++++ compiler/apps/playground/styles/globals.css | 15 +++--- 8 files changed, 111 insertions(+), 83 deletions(-) create mode 100644 compiler/apps/playground/lib/transitionTypes.ts diff --git a/compiler/apps/playground/components/AccordionWindow.tsx b/compiler/apps/playground/components/AccordionWindow.tsx index bebbb0c4787..197f543b4ab 100644 --- a/compiler/apps/playground/components/AccordionWindow.tsx +++ b/compiler/apps/playground/components/AccordionWindow.tsx @@ -18,19 +18,21 @@ export default function AccordionWindow(props: { changedPasses: Set; }): React.ReactElement { return ( -
- {Array.from(props.tabs.keys()).map(name => { - return ( - - ); - })} +
+
+ {Array.from(props.tabs.keys()).map(name => { + return ( + + ); + })} +
); } diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index 59ab2e5fdde..d922f27c978 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -21,6 +21,7 @@ import {useStore, useStoreDispatch} from '../StoreContext'; import {monacoOptions} from './monacoOptions'; import {IconChevron} from '../Icons/IconChevron'; import prettyFormat from 'pretty-format'; +import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes'; // @ts-expect-error - webpack asset/source loader handles .d.ts files as strings import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts'; @@ -44,7 +45,7 @@ export default function ConfigEditor({ { startTransition(() => { - addTransitionType('config-panel'); + addTransitionType(CONFIG_PANEL_TRANSITION); setIsExpanded(false); }); }} @@ -58,11 +59,11 @@ export default function ConfigEditor({ { startTransition(() => { - addTransitionType('config-panel'); + addTransitionType(CONFIG_PANEL_TRANSITION); setIsExpanded(true); }); }} - />{' '} + />
); @@ -129,7 +130,8 @@ function ExpandedEditor({ : 'Invalid configs'; return ( - +
- - -
-
- -
-
- -
-
-
+
+ + +
); diff --git a/compiler/apps/playground/components/Editor/Input.tsx b/compiler/apps/playground/components/Editor/Input.tsx index 6cded7656b0..8c37b12975b 100644 --- a/compiler/apps/playground/components/Editor/Input.tsx +++ b/compiler/apps/playground/components/Editor/Input.tsx @@ -13,11 +13,17 @@ import { import invariant from 'invariant'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; -import {useEffect, useState} from 'react'; +import { + useEffect, + useState, + unstable_ViewTransition as ViewTransition, +} from 'react'; import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics'; import {useStore, useStoreDispatch} from '../StoreContext'; import TabbedWindow from '../TabbedWindow'; import {monacoOptions} from './monacoOptions'; +import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes'; + // @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack. import React$Types from '../../node_modules/@types/react/index.d.ts'; @@ -155,9 +161,13 @@ export default function Input({errors, language}: Props): JSX.Element { const [activeTab, setActiveTab] = useState('Input'); return ( -
-
-
+ +
+
-
+ ); } diff --git a/compiler/apps/playground/components/Editor/Output.tsx b/compiler/apps/playground/components/Editor/Output.tsx index bb6b60009f3..7cc05e6f88d 100644 --- a/compiler/apps/playground/components/Editor/Output.tsx +++ b/compiler/apps/playground/components/Editor/Output.tsx @@ -20,11 +20,19 @@ import parserBabel from 'prettier/plugins/babel'; import * as prettierPluginEstree from 'prettier/plugins/estree'; import * as prettier from 'prettier/standalone'; import {type Store} from '../../lib/stores'; -import {memo, ReactNode, use, useState, Suspense} from 'react'; +import { + memo, + ReactNode, + use, + useState, + Suspense, + unstable_ViewTransition as ViewTransition, +} from 'react'; import AccordionWindow from '../AccordionWindow'; import TabbedWindow from '../TabbedWindow'; import {monacoOptions} from './monacoOptions'; import {BabelFileResult} from '@babel/core'; +import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes'; const MemoizedOutput = memo(Output); @@ -280,22 +288,34 @@ function OutputContent({store, compilerOutput}: Props): JSX.Element { if (!store.showInternals) { return ( - + + + ); } return ( - + + + ); } diff --git a/compiler/apps/playground/components/TabbedWindow.tsx b/compiler/apps/playground/components/TabbedWindow.tsx index d2335687c22..1fd5f188c7d 100644 --- a/compiler/apps/playground/components/TabbedWindow.tsx +++ b/compiler/apps/playground/components/TabbedWindow.tsx @@ -17,26 +17,28 @@ export default function TabbedWindow({ onTabChange: (tab: string) => void; }): React.ReactElement { return ( -
-
- {Array.from(tabs.keys()).map(tab => { - const isActive = activeTab === tab; - return ( - - ); - })} -
-
- {tabs.get(activeTab)} +
+
+
+ {Array.from(tabs.keys()).map(tab => { + const isActive = activeTab === tab; + return ( + + ); + })} +
+
+ {tabs.get(activeTab)} +
); diff --git a/compiler/apps/playground/lib/transitionTypes.ts b/compiler/apps/playground/lib/transitionTypes.ts new file mode 100644 index 00000000000..0c39e586fed --- /dev/null +++ b/compiler/apps/playground/lib/transitionTypes.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export const CONFIG_PANEL_TRANSITION = 'config-panel'; diff --git a/compiler/apps/playground/styles/globals.css b/compiler/apps/playground/styles/globals.css index 3ff8bc0e39c..e8b92e6c7be 100644 --- a/compiler/apps/playground/styles/globals.css +++ b/compiler/apps/playground/styles/globals.css @@ -96,16 +96,15 @@ transform: translateX(0); } } -@keyframes slideInRight { - from { - transform: translateX(100%); - } - to { - transform: translateX(0); - } -} ::view-transition-old(.container), ::view-transition-new(.container) { height: 100%; } + +::view-transition-old(.accordion-container), +::view-transition-new(.accordion-container) { + height: 100%; + object-fit: none; + object-position: left; +}