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
}
>
<>