solara-ui 1.39.0__py2.py3-none-any.whl → 1.40.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. solara/__init__.py +1 -1
  2. solara/components/__init__.py +1 -0
  3. solara/components/input_text_area.py +86 -0
  4. solara/components/markdown.py +1 -1
  5. solara/hooks/use_thread.py +4 -4
  6. solara/lab/components/chat.py +8 -2
  7. solara/server/assets/style.css +2 -1
  8. solara/server/flask.py +1 -1
  9. solara/server/jupyter/server_extension.py +11 -1
  10. solara/server/jupyter/solara.py +91 -0
  11. solara/server/patch.py +1 -0
  12. solara/server/pyinstaller/__init__.py +9 -0
  13. solara/server/pyinstaller/hook-ipyreact.py +5 -0
  14. solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
  15. solara/server/pyinstaller/hook-solara.py +9 -0
  16. solara/server/server.py +6 -1
  17. solara/server/starlette.py +18 -6
  18. solara/server/static/highlight-dark.css +1 -1
  19. solara/server/static/main-vuetify.js +1 -1
  20. solara/server/static/solara_bootstrap.py +1 -1
  21. solara/server/templates/solara.html.j2 +30 -6
  22. solara/website/assets/custom.css +20 -57
  23. solara/website/components/__init__.py +2 -2
  24. solara/website/components/algolia_api.vue +23 -6
  25. solara/website/components/breadcrumbs.py +28 -0
  26. solara/website/components/contact.py +144 -0
  27. solara/website/components/docs.py +11 -9
  28. solara/website/components/header.py +31 -20
  29. solara/website/components/markdown.py +12 -1
  30. solara/website/components/markdown_nav.vue +34 -0
  31. solara/website/components/sidebar.py +7 -1
  32. solara/website/pages/__init__.py +87 -254
  33. solara/website/pages/about/__init__.py +9 -0
  34. solara/website/pages/about/about.md +3 -0
  35. solara/website/pages/careers/__init__.py +27 -0
  36. solara/website/pages/changelog/__init__.py +2 -2
  37. solara/website/pages/changelog/changelog.md +12 -0
  38. solara/website/pages/contact/__init__.py +30 -6
  39. solara/website/pages/documentation/__init__.py +25 -33
  40. solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +2 -1
  41. solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +1 -1
  42. solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +2 -1
  43. solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +1 -1
  44. solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +5 -2
  45. solara/website/pages/documentation/api/hooks/use_thread.md +6 -0
  46. solara/website/pages/documentation/components/data/pivot_table.py +2 -2
  47. solara/website/pages/documentation/components/input/input.py +2 -0
  48. solara/website/pages/documentation/components/output/sql_code.py +3 -3
  49. solara/website/pages/documentation/examples/__init__.py +2 -2
  50. solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +2 -2
  51. solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +19 -14
  52. solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +205 -15
  53. solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +3 -1
  54. solara/website/pages/home.vue +1199 -0
  55. solara/website/pages/our_team/__init__.py +83 -0
  56. solara/website/pages/pricing/__init__.py +31 -0
  57. solara/website/pages/roadmap/__init__.py +11 -0
  58. solara/website/pages/roadmap/roadmap.md +41 -0
  59. solara/website/pages/scale_ipywidgets.py +45 -0
  60. {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/METADATA +2 -2
  61. {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/RECORD +65 -49
  62. solara/website/components/hero.py +0 -15
  63. solara/website/pages/contact/contact.md +0 -17
  64. {solara_ui-1.39.0.data → solara_ui-1.40.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  65. {solara_ui-1.39.0.data → solara_ui-1.40.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  66. {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/WHEEL +0 -0
  67. {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/licenses/LICENSE +0 -0
solara/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Build webapps using IPywidgets"""
2
2
 
3
- __version__ = "1.39.0"
3
+ __version__ = "1.40.0"
4
4
  github_url = "https://github.com/widgetti/solara"
5
5
  git_branch = "master"
6
6
 
@@ -33,6 +33,7 @@ from .togglebuttons import ( # noqa: F401 F403
33
33
  ToggleButtonsSingle,
34
34
  )
35
35
  from .input import InputText, InputFloat, InputInt # noqa: F401 F403
36
+ from .input_text_area import InputTextArea # noqa: F401 F403
36
37
  from .pivot_table import PivotTableView, PivotTable, PivotTableCard # noqa: F401 F403
37
38
  from .head import Head # noqa: F401 F403
38
39
  from .title import Title # noqa: F401 F403
@@ -0,0 +1,86 @@
1
+ from typing import Callable, Optional, Union, List
2
+ from .input import use_change
3
+ import solara
4
+ from solara.alias import rv as v
5
+
6
+
7
+ @solara.component
8
+ def InputTextArea(
9
+ label: str,
10
+ value: Union[str, solara.Reactive[str]] = "",
11
+ on_value: Callable[[str], None] = None,
12
+ disabled: bool = False,
13
+ continuous_update: bool = False,
14
+ update_events: List[str] = ["focusout"],
15
+ error: Union[bool, str] = False,
16
+ message: Optional[str] = None,
17
+ auto_grow: bool = True,
18
+ rows: int = 5,
19
+ ):
20
+ r"""Free form text area input.
21
+
22
+ ### Basic example:
23
+
24
+ ```solara
25
+ import solara
26
+
27
+ text = solara.reactive("Hello\nWorld\n!!!")
28
+ continuous_update = solara.reactive(True)
29
+
30
+ @solara.component
31
+ def Page():
32
+ solara.Checkbox(label="Continuous update", value=continuous_update)
33
+ solara.InputTextArea("Enter some text", value=text, continuous_update=continuous_update.value)
34
+ with solara.Row():
35
+ solara.Button("Clear", on_click=lambda: text.set(""))
36
+ solara.Button("Reset", on_click=lambda: text.set("Hello\nWorld\n!!!"))
37
+ solara.Markdown(f"**You entered**: {text.value}")
38
+ ```
39
+
40
+
41
+ ## Arguments
42
+
43
+ * `label`: Label to display next to the slider.
44
+ * `value`: The currently entered value.
45
+ * `on_value`: Callback to call when the value changes.
46
+ * `disabled`: Whether the input is disabled.
47
+ * `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed.
48
+ * `update_events`: A list of events that should trigger `on_value`. If continuous update is enabled, this will effectively be ignored,
49
+ since updates will happen every change.
50
+ * `auto_grow`: Whether the text area auto grows with more text.
51
+ * `rows`: Number of empty rows to display.
52
+ * `error`: If truthy, show the input as having an error (in red). If a string is passed, it will be shown as the error message.
53
+ * `message`: Message to show below the input. If `error` is a string, this will be ignored.
54
+ * `classes`: List of CSS classes to apply to the input.
55
+ * `style`: CSS style to apply to the input.
56
+ """
57
+ reactive_value = solara.use_reactive(value, on_value)
58
+ del value, on_value
59
+
60
+ def set_value_cast(value):
61
+ reactive_value.value = str(value)
62
+
63
+ def on_v_model(value):
64
+ if continuous_update:
65
+ set_value_cast(value)
66
+
67
+ messages = []
68
+ if error and isinstance(error, str):
69
+ messages.append(error)
70
+ elif message:
71
+ messages.append(message)
72
+ text_area = v.Textarea(
73
+ v_model=reactive_value.value,
74
+ on_v_model=on_v_model,
75
+ label=label,
76
+ disabled=disabled,
77
+ error=bool(error),
78
+ messages=messages,
79
+ solo=True,
80
+ hide_details=True,
81
+ outlined=True,
82
+ rows=rows,
83
+ auto_grow=auto_grow,
84
+ )
85
+ use_change(text_area, set_value_cast, enabled=not continuous_update, update_events=update_events)
86
+ return text_area
@@ -128,7 +128,7 @@ module.exports = {
128
128
  href = location.pathname + href.substr(1);
129
129
  a.attributes['href'].href = href;
130
130
  }
131
- let authLink = href.starswith("/_solara/auth/");
131
+ let authLink = href.startsWith("/_solara/auth/");
132
132
  if( (href.startsWith("./") || href.startsWith("/")) && !authLink) {
133
133
  a.onclick = e => {
134
134
  console.log("clicked", href)
@@ -15,7 +15,7 @@ logger = logging.getLogger("solara.hooks.use_thread")
15
15
 
16
16
 
17
17
  def use_thread(
18
- callback=Union[
18
+ callback: Union[
19
19
  Callable[[threading.Event], T],
20
20
  Iterator[Callable[[threading.Event], T]],
21
21
  Callable[[], T],
@@ -69,11 +69,11 @@ def use_thread(
69
69
  # result.current = None
70
70
  set_result_state(ResultState.RUNNING)
71
71
 
72
- sig = inspect.signature(callback)
72
+ sig = inspect.signature(callback) # type: ignore
73
73
  if sig.parameters:
74
- f = functools.partial(callback, cancel)
74
+ f = functools.partial(callback, cancel) # type: ignore
75
75
  else:
76
- f = callback
76
+ f = callback # type: ignore
77
77
  try:
78
78
  try:
79
79
  # we only use the cancel_guard context manager around
@@ -46,7 +46,9 @@ def ChatInput(
46
46
  send_callback: Optional[Callable] = None,
47
47
  disabled: bool = False,
48
48
  style: Optional[Union[str, Dict[str, str]]] = None,
49
+ input_text_style: Optional[Union[str, Dict[str, str]]] = None,
49
50
  classes: List[str] = [],
51
+ input_text_classes: List[str] = [],
50
52
  ):
51
53
  """
52
54
  The ChatInput component renders a text input together with a send button.
@@ -56,11 +58,14 @@ def ChatInput(
56
58
  * `send_callback`: A callback function for when the user presses enter or clicks the send button.
57
59
  * `disabled`: Whether the input should be disabled. Useful for disabling sending further messages while a chatbot is replying,
58
60
  among other things.
59
- * `style`: CSS styles to apply to the component. Either a string or a dictionary. These styles are applied to the container component.
61
+ * `style`: CSS styles to apply to the `solara.Row` containing the input field and submit button. Either a string or a dictionary.
62
+ * `input_text_style`: CSS styles to apply to the `InputText` part of the component. Either a string or a dictionary.
60
63
  * `classes`: A list of CSS classes to apply to the component. Also applied to the container.
64
+ * `input_text_classes`: A list of CSS classes to apply to the `InputText` part of the component.
61
65
  """
62
66
  message, set_message = solara.use_state("") # type: ignore
63
67
  style_flat = solara.util._flatten_style(style)
68
+ input_text_style_flat = solara.util._flatten_style(input_text_style)
64
69
 
65
70
  if "align-items" not in style_flat:
66
71
  style_flat += " align-items: center;"
@@ -79,8 +84,9 @@ def ChatInput(
79
84
  rounded=True,
80
85
  filled=True,
81
86
  hide_details=True,
82
- style_="flex-grow: 1;",
87
+ style_="flex-grow: 1;" + input_text_style_flat,
83
88
  disabled=disabled,
89
+ class_=" ".join(input_text_classes),
84
90
  )
85
91
 
86
92
  use_change(message_input, send, update_events=["keyup.enter"])
@@ -75,7 +75,8 @@ div.solara-markdown-output {
75
75
 
76
76
 
77
77
  div.highlight {
78
- background-color: #eee;
78
+ border: 1px solid var(--color-border-appbar);
79
+ background-color: #fafafa;
79
80
  padding: 30px;
80
81
  border-radius: 20px;
81
82
  margin-top: 15px;
solara/server/flask.py CHANGED
@@ -192,7 +192,7 @@ def assets(path):
192
192
  return flask.Response("not found", status=404)
193
193
 
194
194
 
195
- @blueprint.route("/static/nbextensions/<dir>/<filename>")
195
+ @blueprint.route("/jupyter/nbextensions/<dir>/<filename>")
196
196
  def nbext(dir, filename):
197
197
  if not allowed():
198
198
  abort(401)
@@ -2,6 +2,7 @@ from jupyter_server.utils import url_path_join
2
2
 
3
3
  from solara.server.cdn_helper import cdn_url_path
4
4
  from solara.server.jupyter.cdn_handler import CdnHandler
5
+ from .solara import SolaraHandler, Assets, ReadyZ
5
6
 
6
7
 
7
8
  def _jupyter_server_extension_paths():
@@ -9,6 +10,11 @@ def _jupyter_server_extension_paths():
9
10
 
10
11
 
11
12
  def _load_jupyter_server_extension(server_app):
13
+ # a dummy app, so that server.read_root can be used
14
+ import solara.server.app
15
+
16
+ solara.server.app.apps["__default__"] = solara.server.app.AppScript("solara.server.jupyter.solara:Page")
17
+
12
18
  web_app = server_app.web_app
13
19
 
14
20
  host_pattern = ".*$"
@@ -17,7 +23,11 @@ def _load_jupyter_server_extension(server_app):
17
23
  web_app.add_handlers(
18
24
  host_pattern,
19
25
  [
20
- (url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}),
26
+ (url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}), # kept for backward compatibility
27
+ (url_path_join(base_url, f"/solara/{cdn_url_path}/(.*)"), CdnHandler, {}),
28
+ (url_path_join(base_url, "/solara/static/assets/(.*)"), Assets, {}),
29
+ (url_path_join(base_url, "/solara/readyz"), ReadyZ, {}),
30
+ (url_path_join(base_url, "/solara(.*)"), SolaraHandler, {}),
21
31
  ],
22
32
  )
23
33
 
@@ -0,0 +1,91 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import tornado.web
7
+ from jupyter_server.base.handlers import JupyterHandler
8
+ import solara.server.server as server
9
+
10
+ from solara.server.utils import path_is_child_of
11
+ import solara
12
+
13
+ logger = logging.getLogger("solara.server.jupyter.solara")
14
+
15
+
16
+ @solara.component
17
+ def Page():
18
+ solara.Error("Hi, you should not see this, we only support ipypopout for now")
19
+
20
+
21
+ class SolaraHandler(JupyterHandler):
22
+ async def get(self, path=None):
23
+ try:
24
+ # base url ends with /
25
+ base_url = self.settings["base_url"]
26
+ # root_path's do not end with /
27
+ jupyter_root_path = ""
28
+ if base_url and base_url.endswith("/"):
29
+ jupyter_root_path = base_url[:-1]
30
+ root_path = f"{jupyter_root_path}/solara"
31
+ content = server.read_root(path="", root_path=root_path, jupyter_root_path=jupyter_root_path)
32
+ except Exception as e:
33
+ logger.exception(e)
34
+ raise tornado.web.HTTPError(500)
35
+
36
+ if content is None:
37
+ raise tornado.web.HTTPError(404)
38
+
39
+ self.set_header("Content-Type", "text/html")
40
+ self.write(content)
41
+
42
+
43
+ # similar to voila
44
+ class MultiStaticFileHandler(tornado.web.StaticFileHandler):
45
+ """A static file handler that 'merges' a list of directories
46
+
47
+ If initialized like this::
48
+
49
+ application = web.Application([
50
+ (r"/content/(.*)", web.MultiStaticFileHandler, {"paths": ["/var/1", "/var/2"]}),
51
+ ])
52
+
53
+ A file will be looked up in /var/1 first, then in /var/2.
54
+
55
+ """
56
+
57
+ def initialize(self, paths, default_filename=None): # type: ignore
58
+ self.roots = paths
59
+ super().initialize(path=paths[0], default_filename=default_filename)
60
+
61
+ def get_absolute_path(self, root: str, path: str) -> str: # type: ignore
62
+ # find the first absolute path that exists
63
+ self.root = self.roots[0]
64
+ abspath = os.path.abspath(os.path.join(root, path))
65
+ for root in self.roots[1:]:
66
+ abspath = os.path.abspath(os.path.join(root, path))
67
+ if os.path.exists(abspath):
68
+ self.root = root # make sure all the other methods in the base class know how to find the file
69
+ break
70
+
71
+ # tornado probably already does a version of this, to make sure it behaves as the rest of the solara
72
+ # server, we do it again
73
+ if not path_is_child_of(Path(abspath), Path(self.root)):
74
+ raise PermissionError(f"Trying to read from outside of cache directory: {abspath} is not a subdir of {self.root}")
75
+
76
+ return abspath
77
+
78
+
79
+ class Assets(MultiStaticFileHandler):
80
+ def initialize(self): # type: ignore
81
+ super().initialize(server.asset_directories())
82
+ logging.error("Using %r as assets directories", self.roots)
83
+
84
+
85
+ class ReadyZ(JupyterHandler):
86
+ def get(self):
87
+ json_data, status = server.readyz()
88
+ json_response = json.dumps(json_data)
89
+ self.set_header("Content-Type", "application/json")
90
+ self.set_status(status)
91
+ self.write(json_response)
solara/server/patch.py CHANGED
@@ -38,6 +38,7 @@ class FakeIPython:
38
38
  # needed for the pyplot interface of matplotlib
39
39
  # (although we don't really support it)
40
40
  self.events = mock.MagicMock()
41
+ self.user_ns: Dict[Any, Any] = {}
41
42
 
42
43
  def enable_gui(self, gui):
43
44
  logger.error("ignoring call to enable_gui(%s)", gui)
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+
4
+ HERE = Path(__file__).parent
5
+
6
+
7
+ def get_hook_dirs():
8
+ # used for pyinstaller
9
+ return [str(HERE)]
@@ -0,0 +1,5 @@
1
+ from PyInstaller.utils.hooks import collect_data_files, copy_metadata, collect_submodules
2
+
3
+ hiddenimports = collect_submodules("ipyreact")
4
+ datas = collect_data_files("ipyreact") # codespell:ignore datas
5
+ datas += copy_metadata("ipyreact") # codespell:ignore datas
@@ -0,0 +1,5 @@
1
+ from PyInstaller.utils.hooks import collect_data_files, copy_metadata, collect_submodules
2
+
3
+ hiddenimports = collect_submodules("ipyvuetify")
4
+ datas = collect_data_files("ipyvuetify") # codespell:ignore datas
5
+ datas += copy_metadata("ipyvuetify") # codespell:ignore datas
@@ -0,0 +1,9 @@
1
+ from PyInstaller.utils.hooks import collect_data_files, collect_submodules
2
+
3
+ hiddenimports = collect_submodules("solara")
4
+ datas = collect_data_files("solara") # codespell:ignore datas
5
+ datas += collect_data_files("solara-ui") # codespell:ignore datas
6
+
7
+ # this makes sure that inspect.getfile works, which is used for the
8
+ # vue component decorator
9
+ module_collection_mode = "pyz+py"
solara/server/server.py CHANGED
@@ -6,7 +6,7 @@ import os
6
6
  import sys
7
7
  import time
8
8
  from pathlib import Path
9
- from typing import Dict, List, Optional, Tuple, TypeVar
9
+ from typing import Dict, List, Optional, Tuple, TypeVar, Union
10
10
 
11
11
  import ipykernel
12
12
  import ipyvue
@@ -268,6 +268,7 @@ def asset_directories():
268
268
  def read_root(
269
269
  path: str,
270
270
  root_path: str = "",
271
+ jupyter_root_path: Union[str, None] = None,
271
272
  render_kwargs={},
272
273
  use_nbextensions=True,
273
274
  ssg_data=None,
@@ -373,10 +374,14 @@ def read_root(
373
374
  else:
374
375
  cdn = solara.settings.assets.cdn
375
376
 
377
+ if jupyter_root_path is None:
378
+ jupyter_root_path = f"{root_path}/jupyter"
379
+
376
380
  render_settings = {
377
381
  "title": title,
378
382
  "path": path,
379
383
  "root_path": root_path,
384
+ "jupyter_root_path": jupyter_root_path,
380
385
  "resources": resources,
381
386
  "theme": settings.theme.dict(),
382
387
  "production": settings.main.mode == "production",
@@ -391,16 +391,16 @@ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
391
391
  """
392
392
  if "script-name" in request.headers:
393
393
  msg += f"""It looks like the reverse proxy sets the script-name header to {request.headers['script-name']!r}
394
- """
394
+ """
395
395
  if "x-script-name" in request.headers:
396
396
  msg += f"""It looks like the reverse proxy sets the x-script-name header to {request.headers['x-script-name']!r}
397
- """
397
+ """
398
398
  if configured_root_path:
399
399
  msg += f"""It looks like the root path was configured to {configured_root_path!r} in the settings
400
- """
400
+ """
401
401
  if root_path_asgi:
402
402
  msg += f"""It looks like the root path set by the asgi framework was configured to {root_path_asgi!r}
403
- """
403
+ """
404
404
  warnings.warn(msg)
405
405
  if host and forwarded_host and forwarded_proto:
406
406
  port = request.base_url.port
@@ -451,6 +451,18 @@ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
451
451
  if request.scope["scheme"] == "https" or request.headers.get("x-forwarded-proto", "http") == "https" or request.base_url.hostname == "localhost":
452
452
  samesite = "none"
453
453
  secure = True
454
+ elif request.base_url.hostname != "localhost":
455
+ warnings.warn(f"""Cookies with samesite=none require https, but according to the asgi framework, the scheme is {request.scope['scheme']!r}
456
+ and the x-forwarded-proto header is {request.headers.get('x-forwarded-proto', 'http')!r}. We will fallback to samesite=lax.
457
+
458
+ If you embed solara in an iframe, make sure you forward the x-forwarded-proto header correctly so that the session cookie can be set.
459
+
460
+ See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite for more information on samesite cookies.
461
+
462
+ Also check out the following Solara documentation:
463
+ * https://solara.dev/documentation/getting_started/deploying/self-hosted
464
+ * https://solara.dev/documentation/advanced/howto/embed
465
+ """)
454
466
  response.set_cookie(
455
467
  server.COOKIE_KEY_SESSION_ID,
456
468
  value=session_id,
@@ -483,9 +495,9 @@ class StaticNbFiles(StaticFilesOptionalAuth):
483
495
  # from https://github.com/encode/starlette/pull/1377/files
484
496
  def lookup_path(self, path: str) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
485
497
  for directory in self.all_directories:
498
+ directory = os.path.realpath(directory)
486
499
  original_path = os.path.join(directory, path)
487
500
  full_path = os.path.realpath(original_path)
488
- directory = os.path.realpath(directory)
489
501
  # return early if someone tries to access a file outside of the directory
490
502
  if not path_is_child_of(Path(original_path), Path(directory)):
491
503
  return "", None
@@ -668,7 +680,7 @@ routes = [
668
680
  *([Mount(f"/{cdn_url_path}", app=StaticCdn(directory=settings.assets.proxy_cache_dir))] if solara.settings.assets.proxy else []),
669
681
  Mount(f"{prefix}/static/public", app=StaticPublic()),
670
682
  Mount(f"{prefix}/static/assets", app=StaticAssets()),
671
- Mount(f"{prefix}/static/nbextensions", app=StaticNbFiles()),
683
+ Mount(f"{prefix}/jupyter/nbextensions", app=StaticNbFiles()),
672
684
  Mount(f"{prefix}/static", app=StaticFilesOptionalAuth(directory=server.solara_static)),
673
685
  Route("/{fullpath:path}", endpoint=root),
674
686
  ]
@@ -2,7 +2,7 @@
2
2
  and then remove the overlapping first lines with highlight.css
3
3
  */
4
4
  .theme--dark.v-application .highlight .hll { background-color: #ffffcc }
5
- .theme--dark.v-application .highlight { background: #282C34; color: #ABB2BF }
5
+ .theme--dark.v-application .highlight { background: #1d1f22; color: #ABB2BF }
6
6
  .theme--dark.v-application .highlight .c { color: #7F848E } /* Comment */
7
7
  .theme--dark.v-application .highlight .err { color: #ABB2BF } /* Error */
8
8
  .theme--dark.v-application .highlight .esc { color: #ABB2BF } /* Escape */
@@ -127,7 +127,7 @@ async function solaraInit(mountId, appName) {
127
127
  window.navigator.sendBeacon(close_url);
128
128
  }
129
129
  });
130
- let kernel = await solara.connectKernel(solara.rootPath + '/jupyter', kernelId)
130
+ let kernel = await solara.connectKernel(solara.jupyterRootPath, kernelId)
131
131
  if (!kernel) {
132
132
  return;
133
133
  }
@@ -119,7 +119,7 @@ async def main():
119
119
  ]
120
120
  for dep in requirements:
121
121
  await micropip.install(dep, keep_going=True)
122
- await micropip.install("/wheels/solara-1.39.0-py2.py3-none-any.whl", keep_going=True)
122
+ await micropip.install("/wheels/solara-1.40.0-py2.py3-none-any.whl", keep_going=True)
123
123
  import solara
124
124
 
125
125
  el = solara.Warning("lala")
@@ -31,7 +31,7 @@
31
31
 
32
32
  <script id="jupyter-config-data" type="application/json">
33
33
  {
34
- "baseUrl": "{{root_path}}/",
34
+ "baseUrl": "{{jupyter_root_path}}/",
35
35
  "kernelId": "1234"
36
36
  }
37
37
  </script>
@@ -243,12 +243,13 @@
243
243
  {% endif %}
244
244
  <script>
245
245
  solara.rootPath = {{ root_path | tojson | safe}};
246
+ solara.jupyterRootPath = {{ jupyter_root_path | tojson | safe}};
246
247
  solara.cdn = {{ cdn | tojson | safe }};
247
248
  // the vue templates expect it to not have a trailing slash
248
249
  solara.cdn = solara.cdn.replace(/\/$/, '');
249
250
  // keep this for backwards compatibility
250
251
  window.solara_cdn = solara.cdn;
251
- console.log("rootPath", solara.rootPath);
252
+ console.log("solara config", {rootPath: solara.rootPath, jupyterRootPath: solara.jupyterRootPath, cdn: solara.cdn});
252
253
 
253
254
  async function changeThemeCSS(theme) {
254
255
  let css = await fetch(`${solara.rootPath}/static/assets/theme-${theme}.css`).then(r => r.text());
@@ -269,6 +270,12 @@
269
270
  solara.production = {{ production | tojson | safe }};
270
271
  const themeVariants = ['light', 'dark', 'auto']
271
272
  solara.preRendered = {{ pre_rendered_html | safe | length | tojson }} > 0
273
+ if(solara.preRendered) {
274
+ document.body.classList.add('solara-ssg', 'solara-pre-rendered');
275
+ } else {
276
+ document.body.classList.add('solara-no-ssg');
277
+ }
278
+
272
279
  </script>
273
280
 
274
281
  <script>
@@ -329,8 +336,25 @@
329
336
  // if preRendered, the app is not mounted yet
330
337
  // so we mount it when loading becomes false
331
338
  if (solara.preRendered && !this.mounted) {
332
- this.isMounted = true;
333
- this.$mount("#app")
339
+ function waitForAnimation(element) {
340
+ return new Promise((resolve) => {
341
+ element.addEventListener('animationend', resolve, { once: true });
342
+ });
343
+ }
344
+
345
+ function waitForAllAnimations(elements) {
346
+ const animationPromises = Array.from(elements).map(element => waitForAnimation(element));
347
+ return Promise.all(animationPromises);
348
+ }
349
+
350
+ (async () => {
351
+ await waitForAllAnimations(document.querySelectorAll('.solara-ssg-wait-for-animation'));
352
+ console.log("animation finished");
353
+ document.body.classList.remove('solara-pre-rendered');
354
+ document.body.classList.add('solara-pre-rendered-finished');
355
+ this.isMounted = true;
356
+ this.$mount("#app")
357
+ })();
334
358
  } else {
335
359
  this.isMounted = true;
336
360
  }
@@ -418,7 +442,7 @@
418
442
  {% endif -%}
419
443
  nbextensionHashes = {{ resources.nbextensions_hashes | tojson | safe }};
420
444
  requirejs.config({
421
- baseUrl: '{{root_path}}/static/',
445
+ baseUrl: '{{jupyter_root_path}}',
422
446
  waitSeconds: 3000,
423
447
  map: {
424
448
  '*': {
@@ -443,7 +467,7 @@
443
467
  });
444
468
  requirejs([
445
469
  {% for ext in resources.nbextensions if ext != 'jupyter-vuetify/extension' and ext != 'jupyter-vue/extension' -%}
446
- "{{root_path}}/static/nbextensions/{{ ext }}.js",
470
+ "{{jupyter_root_path}}/nbextensions/{{ ext }}.js",
447
471
  {% endfor %}
448
472
  ]);
449
473
  (async function () {