diff --git a/packages/playground/src/shared/utils/useExtensionButton.ts b/packages/playground/src/shared/utils/useExtensionButton.ts
index dbc03fd..985550e 100644
--- a/packages/playground/src/shared/utils/useExtensionButton.ts
+++ b/packages/playground/src/shared/utils/useExtensionButton.ts
@@ -32,5 +32,31 @@ export function useExtensionButton(
}, [id])
}
+export function extensionButton(
+ title: string,
+ callback: () => void,
+ svgSource?: string,
+) {
+ const id = 'useExtensionButton#' + idCounter++
+ studio.extend({
+ id: id,
+ toolbars: {
+ global(set) {
+ set([
+ {
+ type: 'Icon',
+ title,
+ onClick() {
+ callback()
+ },
+ svgSource: svgSource ?? stepForward,
+ },
+ ])
+ return () => {}
+ },
+ },
+ })
+}
+
// FontAwesome FaStepForward
const stepForward = ``
diff --git a/packages/playground/src/tests/reconfigure-extension/App.tsx b/packages/playground/src/tests/reconfigure-extension/App.tsx
new file mode 100644
index 0000000..daa8cdf
--- /dev/null
+++ b/packages/playground/src/tests/reconfigure-extension/App.tsx
@@ -0,0 +1,37 @@
+import {editable as e, SheetProvider} from '@theatre/r3f'
+import {Stars, TorusKnot} from '@react-three/drei'
+import {getProject} from '@theatre/core'
+import React from 'react'
+import {Canvas} from '@react-three/fiber'
+
+function App() {
+ return (
+
{
+ // return setBgIndex((bgIndex) => (bgIndex + 1) % bgs.length)
+ }}
+ style={{
+ height: '100vh',
+ }}
+ >
+
+
+ )
+}
+
+export default App
diff --git a/packages/playground/src/tests/reconfigure-extension/index.html b/packages/playground/src/tests/reconfigure-extension/index.html
new file mode 100644
index 0000000..b3a037a
--- /dev/null
+++ b/packages/playground/src/tests/reconfigure-extension/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Theatre.js Playground
+
+
+
+
+
+
diff --git a/packages/playground/src/tests/reconfigure-extension/index.tsx b/packages/playground/src/tests/reconfigure-extension/index.tsx
new file mode 100644
index 0000000..36f5730
--- /dev/null
+++ b/packages/playground/src/tests/reconfigure-extension/index.tsx
@@ -0,0 +1,86 @@
+import type {IExtension} from '@theatre/studio'
+import studio from '@theatre/studio'
+import '@theatre/core'
+import {extensionButton} from '../../shared/utils/useExtensionButton'
+
+const ext1: IExtension = {
+ id: '@theatre/hello-world-extension',
+ toolbars: {
+ global(set, studio) {
+ console.log('mount 1')
+
+ set([
+ {
+ type: 'Icon',
+ title: 'Icon 1',
+ svgSource: '1',
+ onClick: () => {
+ console.log('Icon 1')
+ },
+ },
+ ])
+
+ return () => {
+ console.log('unmount 1')
+ }
+ },
+ },
+ panes: [],
+}
+
+studio.initialize()
+
+let currentStep = -1
+
+extensionButton(
+ 'Forward',
+ () => {
+ if (currentStep < steps.length - 1) {
+ currentStep++
+ steps[currentStep]()
+ }
+ },
+ '>',
+)
+
+const steps = [
+ function step1() {
+ studio.extend(ext1)
+ },
+ function step2() {
+ studio.extend(
+ {
+ ...ext1,
+ toolbars: {
+ global(set, studio) {
+ console.log('mount 2')
+
+ set([
+ {
+ type: 'Icon',
+ title: 'Icon 2',
+ svgSource: '2',
+ onClick: () => {
+ console.log('Icon 2')
+ },
+ },
+ ])
+ return () => {
+ console.log('unmount 2')
+ }
+ },
+ },
+ },
+ {__experimental_reconfigure: true},
+ )
+ },
+ function step3() {
+ studio.extend(
+ {
+ ...ext1,
+ toolbars: {},
+ },
+ {__experimental_reconfigure: true},
+ )
+ },
+]
diff --git a/packages/playground/src/tests/reconfigure-extension/test.e2e.ts b/packages/playground/src/tests/reconfigure-extension/test.e2e.ts
new file mode 100644
index 0000000..fdd181c
--- /dev/null
+++ b/packages/playground/src/tests/reconfigure-extension/test.e2e.ts
@@ -0,0 +1,25 @@
+import {test, expect} from '@playwright/test'
+
+test.describe('reconfigure-extension', () => {
+ test('works', async ({page}) => {
+ await page.goto('./tests/reconfigure-extension/')
+
+ const toolbar = page.locator(
+ '[data-test-id="theatre-extensionToolbar-global"]',
+ )
+
+ const forwardButton = toolbar.getByRole('button', {name: '>'})
+ await forwardButton.click()
+
+ const otherButton = toolbar.getByRole('button').nth(1)
+
+ expect(await otherButton.textContent()).toEqual('1')
+
+ await forwardButton.click()
+ expect(await otherButton.textContent()).toEqual('2')
+ await forwardButton.click()
+
+ // expect otherButton not to exist
+ await expect(otherButton).not.toBeAttached()
+ })
+})
diff --git a/theatre/studio/src/TheatreStudio.ts b/theatre/studio/src/TheatreStudio.ts
index 2801951..f769279 100644
--- a/theatre/studio/src/TheatreStudio.ts
+++ b/theatre/studio/src/TheatreStudio.ts
@@ -374,6 +374,21 @@ export interface IStudio {
* The extension's definition
*/
extension: IExtension,
+ opts?: {
+ /**
+ * Whether to reconfigure the extension. This is useful if you're
+ * hot-reloading the extension.
+ *
+ * Mind you, that if the old version of the extension defines a pane,
+ * and the new version doesn't, all instances of that pane will disappear, as expected.
+ * _However_, if you again reconfigure the extension with the old version, the instances
+ * of the pane that pane will re-appear.
+ *
+ * We're not sure about whether this behavior makes sense or not. If not, let us know
+ * in the discord server or open an issue on github.
+ */
+ __experimental_reconfigure?: boolean
+ },
): void
/**
@@ -504,8 +519,11 @@ export default class TheatreStudio implements IStudio {
return studio.initialize(opts)
}
- extend(extension: IExtension): void {
- getStudio().extend(extension)
+ extend(
+ extension: IExtension,
+ opts?: {__experimental_reconfigure?: boolean},
+ ): void {
+ getStudio().extend(extension, opts)
}
transaction(fn: (api: ITransactionAPI) => void): void {
diff --git a/theatre/studio/src/toolbars/ExtensionToolbar/ExtensionToolbar.tsx b/theatre/studio/src/toolbars/ExtensionToolbar/ExtensionToolbar.tsx
index b4754f9..d3f0d48 100644
--- a/theatre/studio/src/toolbars/ExtensionToolbar/ExtensionToolbar.tsx
+++ b/theatre/studio/src/toolbars/ExtensionToolbar/ExtensionToolbar.tsx
@@ -31,14 +31,16 @@ const ExtensionToolsetRender: React.FC<{
}> = ({extension, toolbarId}) => {
const toolsetConfigBox = useMemo(() => new Atom([]), [])
+ const attachFn = extension.toolbars?.[toolbarId]
+
useLayoutEffect(() => {
- const detach = extension.toolbars?.[toolbarId]?.(
+ const detach = attachFn?.(
toolsetConfigBox.set.bind(toolsetConfigBox),
getStudio()!.publicApi,
)
if (typeof detach === 'function') return detach
- }, [extension, toolbarId])
+ }, [extension, toolbarId, attachFn])
const config = useVal(toolsetConfigBox.prism)
@@ -69,7 +71,7 @@ export const ExtensionToolbar: React.FC<{
if (groups.length === 0) return null
return (
-
+
{showLeftDivider ? : undefined}
{groups}
diff --git a/theatre/studio/src/toolbars/ExtensionToolbar/tools/IconButton.tsx b/theatre/studio/src/toolbars/ExtensionToolbar/tools/IconButton.tsx
index 1a53bcf..41f5868 100644
--- a/theatre/studio/src/toolbars/ExtensionToolbar/tools/IconButton.tsx
+++ b/theatre/studio/src/toolbars/ExtensionToolbar/tools/IconButton.tsx
@@ -15,10 +15,12 @@ const Container = styled(ToolbarIconButton)`
const IconButton: React.FC<{
config: ToolConfigIcon
-}> = ({config}) => {
+ testId?: string
+}> = ({config, testId}) => {
return (