+
+ {{ name === '..' ? 'mdi-keyboard-backspace' : is_file ? 'mdi-file-document' : 'mdi-folder' }}
+
{{ name }} - {{ size }}
-
+
@@ -60,19 +59,31 @@ module.exports = {
}
-.solara-file-list .v-list-item__icon,
-.solara-file-list .v-list-item__list {
- margin-top: 0;
- margin-bottom: 0;
+.solara-file-list .solara-file-list-row {
+ align-items: center;
+ display: flex;
+ min-height: 28px;
+ width: 100%;
}
-.v-application--is-ltr .solara-file-list .v-list-item__icon {
+.solara-file-list .solara-file-list-icon {
+ align-items: center;
+ display: flex;
+ flex: 0 0 32px;
+ justify-content: center;
margin-right: 8px;
}
+.solara-file-list .solara-file-list-item.v-list-item,
.solara-file-list .v-list-item {
height: 28px;
min-height: 0;
- padding-left: 0;
+ padding: 0;
+ padding-inline-end: 0;
+ padding-inline-start: 0;
+}
+
+.solara-file-list .v-list-item-title {
+ line-height: 28px;
}
diff --git a/solara/server/server.py b/solara/server/server.py
index 4f586f809..bcb071017 100644
--- a/solara/server/server.py
+++ b/solara/server/server.py
@@ -346,8 +346,10 @@ def include_js(path: str, module=False) -> Markup:
content, hash = solara.util.get_file_hash(filepath)
content_utf8 = content.decode("utf-8")
url = f"{root_path}{path}?v={hash}"
- # when < 10k we embed, but if we use currentScript, it can break things
- embed = len(content) < 1024 * 10 and b"currentScript" not in content
+ # when < 20k we embed, but if we use currentScript, it can break things
+ # main-vuetify.js must be embedded on the Jupyter server extension page,
+ # where /solara/static/main-vuetify.js is not a valid static asset URL.
+ embed = (len(content) < 1024 * 20 or path == "/static/main-vuetify.js") and b"currentScript" not in content
if embed:
if module:
code = f''
diff --git a/solara/server/static/main-vuetify.js b/solara/server/static/main-vuetify.js
index d4bce7148..40bcc0a8c 100644
--- a/solara/server/static/main-vuetify.js
+++ b/solara/server/static/main-vuetify.js
@@ -4,6 +4,7 @@ var jupyterWidgetMountPoint = {
return {
renderFn: undefined,
elem: undefined,
+ component: undefined,
}
},
props: ['mount-id'],
@@ -11,11 +12,18 @@ var jupyterWidgetMountPoint = {
requestWidget(this.mountId);
},
mounted() {
- const vue3 = Vue.version.startsWith('3');
requestWidget(this.mountId)
- .then(widgetView => {
- if (['VuetifyView', 'VuetifyTemplateView'].includes(widgetView.model.get('_view_name'))) {
- this.renderFn = createElement => widgetView.vueRender(createElement);
+ .then(async widgetView => {
+ const model = widgetView.model;
+ if (['VuetifyView', 'VuetifyTemplateView'].includes(model.get('_view_name'))) {
+ if (['VueTemplateModel', 'VuetifyTemplateModel'].includes(model.get('_model_name'))) {
+ await registerVueComponents(this, widgetView);
+ }
+ if (Vue.h) {
+ this.component = widgetView.vueComponent();
+ } else {
+ this.renderFn = createElement => widgetView.vueRender(createElement);
+ }
} else {
while (this.$el.firstChild) {
this.$el.removeChild(this.$el.firstChild);
@@ -31,6 +39,9 @@ var jupyterWidgetMountPoint = {
render(createElement) {
// in vue3 we have Vue.h, otherwise fall back to createElement (vue2)
let h = Vue.h || createElement;
+ if (this.component) {
+ return h(this.component);
+ }
if (this.renderFn) {
/* workaround for v-menu click */
if (!this.elem) {
@@ -43,6 +54,28 @@ var jupyterWidgetMountPoint = {
}
};
+async function registerVueComponents(vueComponent, widgetView) {
+ const app = vueComponent.$.appContext.app;
+ await loadWidgetModule('jupyter-vue', jupyterVue =>
+ jupyterVue.addApp(app, widgetView.model.widget_manager)
+ );
+ if (widgetView.model.get('_view_module') === 'jupyter-vuetify') {
+ await loadWidgetModule('jupyter-vuetify', jupyterVuetify =>
+ jupyterVuetify.addApp(app)
+ );
+ }
+}
+
+function loadWidgetModule(name, callback) {
+ return new Promise(resolve => {
+ if (!requirejs.defined(name) && !requirejs.specified(name)) {
+ resolve();
+ return;
+ }
+ requirejs([name], module => resolve(callback(module)), () => resolve());
+ });
+}
+
const widgetResolveFns = {};
const widgetPromises = {};
@@ -115,6 +148,9 @@ async function solaraInit(mountId, appName) {
console.log('solara init', mountId, appName);
define("vue", [], () => Vue);
define("vuetify", [], () => Vuetify);
+ if (typeof vuetifyPlugin !== "undefined") {
+ define("solara-vuetify-plugin", [], () => ({ vuetifyPlugin }));
+ }
cookies = getCookiesMap(document.cookie);
const searchParams = new URLSearchParams(window.location.search);
let kernelId = searchParams.get('kernelid') || generateUuid()
diff --git a/tests/integration/popout_external_widget_test.py b/tests/integration/popout_external_widget_test.py
new file mode 100644
index 000000000..c657128b6
--- /dev/null
+++ b/tests/integration/popout_external_widget_test.py
@@ -0,0 +1,186 @@
+from pathlib import Path
+from typing import TYPE_CHECKING, cast
+
+import ipyvue
+import ipywidgets as widgets
+import traitlets
+from reacton.core import _RenderContext
+
+import solara
+from solara.server import kernel_context
+
+if TYPE_CHECKING:
+ import playwright.sync_api
+
+HERE = Path(__file__).parent
+
+
+ipyvue.register_component_from_string(
+ "external-split-widget",
+ """
+