diff --git a/packages/playground/src/shared/remote/index.html b/packages/playground/src/shared/remote/index.html new file mode 100644 index 0000000..b3a037a --- /dev/null +++ b/packages/playground/src/shared/remote/index.html @@ -0,0 +1,11 @@ + + + + + Theatre.js Playground + + + +
+ + diff --git a/theatre/studio/src/Studio.ts b/theatre/studio/src/Studio.ts index a7332e6..394b938 100644 --- a/theatre/studio/src/Studio.ts +++ b/theatre/studio/src/Studio.ts @@ -1,6 +1,6 @@ import Scrub from '@theatre/studio/Scrub' import type {StudioHistoricState} from '@theatre/studio/store/types/historic' -import type UI from '@theatre/studio/UI' +import UI from '@theatre/studio/UI/UI' import type {Pointer, Ticker} from '@theatre/dataverse' import {Atom, PointerProxy, pointerToPrism} from '@theatre/dataverse' import type { @@ -32,9 +32,6 @@ const DEFAULT_PERSISTENCE_KEY = 'theatre-0.4' export type CoreExports = typeof _coreExports -const UIConstructorModule = - typeof window !== 'undefined' ? import('./UI').then((M) => M.default) : null - const STUDIO_NOT_INITIALIZED_MESSAGE = `You seem to have imported '@theatre/studio' but haven't initialized it. You can initialize the studio by: \`\`\` import studio from '@theatre/studio' @@ -64,16 +61,9 @@ studio.initialize() ` export class Studio { - protected _ui: UI | null = null + readonly ui: UI // this._uiInitDeferred.promise will resolve once this._ui is set - private _uiInitDeferred = defer() - get ui() { - if (!this._ui) { - debugger - throw new Error(`Studio.ui called before UI is initialized`) - } - return this._ui - } + readonly publicApi: IStudio readonly address: {studioId: string} readonly _projectsProxy: PointerProxy> = @@ -130,17 +120,7 @@ export class Studio { this.address = {studioId: nanoid(10)} this.publicApi = new TheatreStudio(this) - // initialize UI if we're in the browser - if (process.env.NODE_ENV !== 'test' && typeof window !== 'undefined') { - UIConstructorModule! - .then((M) => { - this._ui = new M(this) - this._uiInitDeferred.resolve(null) - }) - .catch((error) => { - console.error(`Failed initializing the UI at @theatre/studio.`, error) - }) - } + this.ui = new UI(this) this._attachToIncomingProjects() this.paneManager = new PaneManager(this) @@ -215,13 +195,15 @@ export class Studio { return } + if (process.env.NODE_ENV !== 'test' && typeof window !== 'undefined') { + await this.ui.ready + } + this._initializedDeferred.resolve() if (process.env.NODE_ENV !== 'test') { - this._uiInitDeferred.promise.then(() => { - this.ui.render() - checkForUpdates() - }) + this.ui.render() + checkForUpdates() } } diff --git a/theatre/studio/src/UI/UI.ts b/theatre/studio/src/UI/UI.ts new file mode 100644 index 0000000..b7d9704 --- /dev/null +++ b/theatre/studio/src/UI/UI.ts @@ -0,0 +1,70 @@ +import type {Studio} from '@theatre/studio/Studio' +import {val} from '@theatre/dataverse' + +const NonSSRBitsClass = + typeof window !== 'undefined' + ? import('./UINonSSRBits').then((M) => M.default) + : null + +export default class UI { + private _rendered = false + private _nonSSRBits = NonSSRBitsClass + ? NonSSRBitsClass.then((NonSSRBitsClass) => new NonSSRBitsClass()) + : Promise.reject() + readonly ready: Promise = this._nonSSRBits.then( + () => undefined, + () => undefined, + ) + + constructor(readonly studio: Studio) {} + + render() { + if (this._rendered) { + return + } + this._rendered = true + + this._nonSSRBits.then((b) => { + b.render() + }) + } + + hide() { + this.studio.transaction(({drafts}) => { + drafts.ahistoric.visibilityState = 'everythingIsHidden' + }) + } + + restore() { + this.render() + this.studio.transaction(({drafts}) => { + drafts.ahistoric.visibilityState = 'everythingIsVisible' + }) + } + + get isHidden() { + return ( + val(this.studio.atomP.ahistoric.visibilityState) === 'everythingIsHidden' + ) + } + + renderToolset(toolsetId: string, htmlNode: HTMLElement) { + let shouldUnmount = false + + let unmount: null | (() => void) = null + + this._nonSSRBits.then((nonSSRBits) => { + if (shouldUnmount) return // unmount requested before the toolset is mounted, so, abort + unmount = nonSSRBits.renderToolset(toolsetId, htmlNode) + }) + + return () => { + if (unmount) { + unmount() + return + } + if (shouldUnmount) return + shouldUnmount = true + } + } +} diff --git a/theatre/studio/src/UI.ts b/theatre/studio/src/UI/UINonSSRBits.ts similarity index 69% rename from theatre/studio/src/UI.ts rename to theatre/studio/src/UI/UINonSSRBits.ts index 890cc0d..d91c3f9 100644 --- a/theatre/studio/src/UI.ts +++ b/theatre/studio/src/UI/UINonSSRBits.ts @@ -2,20 +2,17 @@ import UIRoot from '@theatre/studio/UIRoot/UIRoot' import type {$IntentionalAny} from '@theatre/shared/utils/types' import React from 'react' import ReactDOM from 'react-dom' -import type {Studio} from './Studio' -import {val} from '@theatre/dataverse' -import {getMounter} from './utils/renderInPortalInContext' -import {withStyledShadow} from './css' -import ExtensionToolbar from './toolbars/ExtensionToolbar/ExtensionToolbar' +import {getMounter} from '@theatre/studio/utils/renderInPortalInContext' +import {withStyledShadow} from '@theatre/studio/css' +import ExtensionToolbar from '@theatre/studio/toolbars/ExtensionToolbar/ExtensionToolbar' -export default class UI { +export default class UINonSSRBits { readonly containerEl = document.createElement('div') - private _rendered = false private _renderTimeout: NodeJS.Timer | undefined = undefined private _documentBodyUIIsRenderedIn: HTMLElement | undefined = undefined readonly containerShadow: ShadowRoot & HTMLElement - constructor(readonly studio: Studio) { + constructor() { // @todo we can't bootstrap Theatre.js (as in, to design Theatre.js using theatre), if we rely on IDed elements this.containerEl.id = 'theatrejs-studio-root' @@ -49,15 +46,6 @@ export default class UI { } render() { - if (this._rendered) { - return - } - this._rendered = true - - this._render() - } - - protected _render() { const renderCallback = () => { if (!document.body) { this._renderTimeout = setTimeout(renderCallback, 5) @@ -66,30 +54,14 @@ export default class UI { this._renderTimeout = undefined this._documentBodyUIIsRenderedIn = document.body this._documentBodyUIIsRenderedIn.appendChild(this.containerEl) - ReactDOM.render(React.createElement(UIRoot), this.containerShadow) + ReactDOM.render( + React.createElement(UIRoot, {containerShadow: this.containerShadow}), + this.containerShadow, + ) } this._renderTimeout = setTimeout(renderCallback, 10) } - hide() { - this.studio.transaction(({drafts}) => { - drafts.ahistoric.visibilityState = 'everythingIsHidden' - }) - } - - restore() { - this.render() - this.studio.transaction(({drafts}) => { - drafts.ahistoric.visibilityState = 'everythingIsVisible' - }) - } - - get isHidden() { - return ( - val(this.studio.atomP.ahistoric.visibilityState) === 'everythingIsHidden' - ) - } - renderToolset(toolsetId: string, htmlNode: HTMLElement) { const s = getMounter() diff --git a/theatre/studio/src/UIRoot/UIRoot.tsx b/theatre/studio/src/UIRoot/UIRoot.tsx index 2168fbb..6025589 100644 --- a/theatre/studio/src/UIRoot/UIRoot.tsx +++ b/theatre/studio/src/UIRoot/UIRoot.tsx @@ -46,7 +46,9 @@ const INTERNAL_LOGGING = /Playground.+Theatre\.js/.test( (typeof document !== 'undefined' ? document?.title : null) ?? '', ) -export default function UIRoot() { +export default function UIRoot(props: { + containerShadow: ShadowRoot & HTMLElement +}) { const studio = getStudio() const [portalLayerRef, portalLayer] = useRefAndState( undefined as $IntentionalAny, @@ -87,7 +89,7 @@ export default function UIRoot() { target={ window.__IS_VISUAL_REGRESSION_TESTING === true ? undefined - : getStudio()!.ui.containerShadow + : props.containerShadow } > <>