solara-ui 1.39.0__py2.py3-none-any.whl → 1.41.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 (85) hide show
  1. solara/__init__.py +1 -1
  2. solara/__main__.py +23 -10
  3. solara/components/__init__.py +1 -0
  4. solara/components/component_vue.py +3 -2
  5. solara/components/input_text_area.py +86 -0
  6. solara/components/markdown.py +1 -1
  7. solara/hooks/use_thread.py +4 -4
  8. solara/lab/components/chat.py +8 -2
  9. solara/server/assets/style.css +4 -1
  10. solara/server/flask.py +1 -1
  11. solara/server/jupyter/server_extension.py +11 -1
  12. solara/server/jupyter/solara.py +91 -0
  13. solara/server/patch.py +1 -0
  14. solara/server/pyinstaller/__init__.py +9 -0
  15. solara/server/pyinstaller/hook-ipyreact.py +5 -0
  16. solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
  17. solara/server/pyinstaller/hook-solara.py +9 -0
  18. solara/server/qt.py +113 -0
  19. solara/server/server.py +6 -1
  20. solara/server/settings.py +1 -0
  21. solara/server/starlette.py +18 -6
  22. solara/server/static/highlight-dark.css +1 -1
  23. solara/server/static/main-vuetify.js +11 -1
  24. solara/server/static/solara_bootstrap.py +1 -1
  25. solara/server/templates/loader-solara.html +1 -1
  26. solara/server/templates/solara.html.j2 +36 -7
  27. solara/website/assets/custom.css +20 -57
  28. solara/website/components/__init__.py +2 -2
  29. solara/website/components/algolia_api.vue +23 -6
  30. solara/website/components/breadcrumbs.py +28 -0
  31. solara/website/components/contact.py +144 -0
  32. solara/website/components/docs.py +11 -9
  33. solara/website/components/header.py +31 -20
  34. solara/website/components/markdown.py +12 -1
  35. solara/website/components/markdown_nav.vue +34 -0
  36. solara/website/components/sidebar.py +9 -1
  37. solara/website/pages/__init__.py +93 -254
  38. solara/website/pages/about/__init__.py +9 -0
  39. solara/website/pages/about/about.md +3 -0
  40. solara/website/pages/careers/__init__.py +27 -0
  41. solara/website/pages/changelog/__init__.py +2 -2
  42. solara/website/pages/changelog/changelog.md +12 -0
  43. solara/website/pages/contact/__init__.py +30 -6
  44. solara/website/pages/documentation/__init__.py +25 -33
  45. solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +2 -1
  46. solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +1 -1
  47. solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +17 -1
  48. solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +2 -1
  49. solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +1 -1
  50. solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +5 -2
  51. solara/website/pages/documentation/api/hooks/use_thread.md +6 -0
  52. solara/website/pages/documentation/components/data/pivot_table.py +2 -2
  53. solara/website/pages/documentation/components/input/input.py +2 -0
  54. solara/website/pages/documentation/components/output/sql_code.py +3 -3
  55. solara/website/pages/documentation/examples/__init__.py +14 -22
  56. solara/website/pages/documentation/examples/ai/chatbot.py +1 -1
  57. solara/website/pages/documentation/examples/general/vue_component.py +1 -1
  58. solara/website/pages/documentation/examples/libraries/altair.py +1 -0
  59. solara/website/pages/documentation/examples/libraries/bqplot.py +1 -1
  60. solara/website/pages/documentation/examples/libraries/ipyleaflet.py +1 -1
  61. solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +1 -1
  62. solara/website/pages/documentation/examples/utilities/countdown_timer.py +18 -20
  63. solara/website/pages/documentation/examples/visualization/annotator.py +1 -3
  64. solara/website/pages/documentation/examples/visualization/linked_views.py +4 -4
  65. solara/website/pages/documentation/getting_started/content/00-quickstart.md +18 -0
  66. solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +2 -2
  67. solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +19 -14
  68. solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +205 -15
  69. solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +3 -1
  70. solara/website/pages/home.vue +1199 -0
  71. solara/website/pages/our_team/__init__.py +83 -0
  72. solara/website/pages/pricing/__init__.py +31 -0
  73. solara/website/pages/roadmap/__init__.py +11 -0
  74. solara/website/pages/roadmap/roadmap.md +41 -0
  75. solara/website/pages/scale_ipywidgets.py +45 -0
  76. solara/widgets/vue/navigator.vue +46 -16
  77. solara/widgets/vue/vegalite.vue +18 -0
  78. {solara_ui-1.39.0.dist-info → solara_ui-1.41.0.dist-info}/METADATA +2 -2
  79. {solara_ui-1.39.0.dist-info → solara_ui-1.41.0.dist-info}/RECORD +83 -66
  80. solara/website/components/hero.py +0 -15
  81. solara/website/pages/contact/contact.md +0 -17
  82. {solara_ui-1.39.0.data → solara_ui-1.41.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  83. {solara_ui-1.39.0.data → solara_ui-1.41.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  84. {solara_ui-1.39.0.dist-info → solara_ui-1.41.0.dist-info}/WHEEL +0 -0
  85. {solara_ui-1.39.0.dist-info → solara_ui-1.41.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
  }
@@ -138,15 +138,25 @@ async function solaraInit(mountId, appName) {
138
138
  });
139
139
 
140
140
  window.addEventListener('solara.router', function (event) {
141
+ app.$data.urlHasChanged = true;
141
142
  if(kernel.status == 'busy') {
142
143
  app.$data.loadingPage = true;
143
144
  }
144
145
  });
145
146
  kernel.statusChanged.connect(() => {
147
+ // When navigation is triggered from the front-end, kernel.status becoming busy and
148
+ // solara.router event happen in a different order than when navigating through Python, so
149
+ // if the URL has changed when the kernel becomes busy, we set loadingPage to true
150
+ if (kernel.status == 'busy' && app.$data.urlHasChanged) {
151
+ app.$data.loadingPage = true;
152
+ }
146
153
  // the first idle after a loadingPage == true (a router event)
147
154
  // will be used as indicator that the page is loaded
148
155
  if (app.$data.loadingPage && kernel.status == 'idle') {
149
156
  app.$data.loadingPage = false;
157
+ app.$data.urlHasChanged = false;
158
+ const event = new Event('solara.pageReady');
159
+ window.dispatchEvent(event);
150
160
  }
151
161
  });
152
162
 
@@ -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.41.0-py2.py3-none-any.whl", keep_going=True)
123
123
  import solara
124
124
 
125
125
  el = solara.Warning("lala")
@@ -32,7 +32,7 @@
32
32
  <jupyter-widget-mount-point mount-id="solara-main">
33
33
  A widget with mount-id="solara-main" should go here
34
34
  </jupyter-widget-mount-point>
35
- <div style="position: absolute; right: 0px; bottom: 0px; padding: 10px;">
35
+ <div v-if="showBanner" style="position: absolute; right: 0px; bottom: 0px; padding: 10px;">
36
36
  <b>This website runs on <a href="https://solara.dev">Solara</a></b>
37
37
  </div>
38
38
  </div>
@@ -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>
@@ -172,7 +172,7 @@
172
172
  </script>
173
173
  {% endraw -%}
174
174
 
175
- <body data-base-url="{{root_path}}/static/">
175
+ <body data-base-url="{{root_path}}/jupyter/">
176
176
  {% if perform_check %}
177
177
  <iframe src="https://solara.dev/static/public/success.html?system=solara&check=html&version={{solara_version}}" style="display: none"></iframe>
178
178
  {% endif %}
@@ -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>
@@ -289,6 +296,10 @@
289
296
  this.forceUpdateTrigger += 1;
290
297
  original_$forceUpdate();
291
298
  });
299
+ // in case we are showing a popop (e.g. using ipypopout), hide the banner
300
+ if(searchParams.has('modelid')) {
301
+ this.showBanner = false;
302
+ }
292
303
  },
293
304
  mounted() {
294
305
  document.querySelector('#app').removeAttribute("style");
@@ -329,8 +340,25 @@
329
340
  // if preRendered, the app is not mounted yet
330
341
  // so we mount it when loading becomes false
331
342
  if (solara.preRendered && !this.mounted) {
332
- this.isMounted = true;
333
- this.$mount("#app")
343
+ function waitForAnimation(element) {
344
+ return new Promise((resolve) => {
345
+ element.addEventListener('animationend', resolve, { once: true });
346
+ });
347
+ }
348
+
349
+ function waitForAllAnimations(elements) {
350
+ const animationPromises = Array.from(elements).map(element => waitForAnimation(element));
351
+ return Promise.all(animationPromises);
352
+ }
353
+
354
+ (async () => {
355
+ await waitForAllAnimations(document.querySelectorAll('.solara-ssg-wait-for-animation'));
356
+ console.log("animation finished");
357
+ document.body.classList.remove('solara-pre-rendered');
358
+ document.body.classList.add('solara-pre-rendered-finished');
359
+ this.isMounted = true;
360
+ this.$mount("#app")
361
+ })();
334
362
  } else {
335
363
  this.isMounted = true;
336
364
  }
@@ -389,6 +417,7 @@
389
417
  loadingPage: false,
390
418
  _lastBusyTimer: null,
391
419
  kernelBusyLong: false,
420
+ showBanner: theme.show_banner,
392
421
  // outputMessages: [{ name: 'stderr', text: 'lala' }],
393
422
  }
394
423
  }
@@ -418,7 +447,7 @@
418
447
  {% endif -%}
419
448
  nbextensionHashes = {{ resources.nbextensions_hashes | tojson | safe }};
420
449
  requirejs.config({
421
- baseUrl: '{{root_path}}/static/',
450
+ baseUrl: '{{jupyter_root_path}}',
422
451
  waitSeconds: 3000,
423
452
  map: {
424
453
  '*': {
@@ -443,7 +472,7 @@
443
472
  });
444
473
  requirejs([
445
474
  {% for ext in resources.nbextensions if ext != 'jupyter-vuetify/extension' and ext != 'jupyter-vue/extension' -%}
446
- "{{root_path}}/static/nbextensions/{{ ext }}.js",
475
+ "{{jupyter_root_path}}/nbextensions/{{ ext }}.js",
447
476
  {% endfor %}
448
477
  ]);
449
478
  (async function () {
@@ -34,6 +34,17 @@
34
34
  --docs-social-twitter: #000000;
35
35
  }
36
36
 
37
+ .theme--dark {
38
+ --color-border-appbar: hsla(0,0%,100%,.12);
39
+ --color-material-background: #1e1e1e;
40
+ --color-text-fade: hsla(0,0%,100%,.7);
41
+ }
42
+ .theme--light {
43
+ --color-border-appbar: rgba(0,0,0,.12);
44
+ --color-material-background: #fff;
45
+ --color-text-fade: rgba(0,0,0,.6);
46
+ }
47
+
37
48
  html,
38
49
  body,
39
50
  p,
@@ -199,9 +210,8 @@ blockquote p:last-child {
199
210
  }
200
211
  .v-sheet.docs-card-container{
201
212
  width: 100%;
202
- padding: 0 10%;
203
213
  justify-content: start;
204
- align-content: center;
214
+ align-content: stretch;
205
215
  flex-wrap: wrap !important;
206
216
  flex-grow: 1;
207
217
  row-gap: 20px;
@@ -211,11 +221,14 @@ blockquote p:last-child {
211
221
  }
212
222
  .v-sheet.docs-card-container > a > .docs-card{
213
223
  display: flex;
214
- width: 20rem;
215
- height: 12rem;
224
+ flex-grow: 1;
225
+ height: 4rem;
216
226
  border-radius: 16px !important;
217
227
  color: var(--color-grey-light);
218
228
  }
229
+ .v-sheet.docs-card-container > a > .docs-card > h2 {
230
+ padding: 0 24px;
231
+ }
219
232
  .v-sheet.docs-card-container > a .docs-card-icon{
220
233
  padding-right: 1.5rem;
221
234
  transition: ease padding 0.25s;
@@ -274,6 +287,8 @@ blockquote p:last-child {
274
287
  /* header */
275
288
  .header-logo {
276
289
  display: block;
290
+ height: 100%;
291
+ margin: 0;
277
292
  }
278
293
  .news {
279
294
  background-color: var(--color-primary);
@@ -289,46 +304,6 @@ blockquote p:last-child {
289
304
  color: white !important;
290
305
  }
291
306
 
292
- /* hero */
293
- .hero {
294
- background: var(--color-primary-lightest) url(https://dxhl76zpt6fap.cloudfront.net/public/hero.webp);
295
- background-size: cover;
296
- min-height: 660px;
297
- padding: 7rem 0;
298
- }
299
-
300
- .theme--dark .hero {
301
- background: var(--dark-color-primary-lightest) url(https://dxhl76zpt6fap.cloudfront.net/public/hero.webp);
302
- background-size: cover;
303
- }
304
-
305
- .hero h1 {
306
- font-size: 4.5rem;
307
- }
308
-
309
- .hero h2 {
310
- padding-left: 150px;
311
- padding-right: 150px;
312
- }
313
-
314
- .hero b {
315
- color: var(--color-primary);
316
- }
317
-
318
- .theme--dark .hero b {
319
- color: var(--dark-color-primary);
320
- }
321
-
322
- @media screen and (max-width: 960px) {
323
- .hero {
324
- padding: 4rem 0;
325
- min-height: auto;
326
- }
327
-
328
- .hero h1 {
329
- font-size: 3rem;
330
- }
331
- }
332
307
 
333
308
  /* fixes padding issue on drawer menu */
334
309
  .v-list-group__items {
@@ -362,7 +337,7 @@ blockquote p:last-child {
362
337
 
363
338
  .menu li a:link {
364
339
  display: block;
365
- padding: 1rem 2rem;
340
+ padding: 0.75rem;
366
341
  }
367
342
 
368
343
  .menu .active {
@@ -427,11 +402,6 @@ blockquote p:last-child {
427
402
  font-weight: bolder;
428
403
  }
429
404
 
430
- .solara-autorouter-content {
431
- padding-top: 2rem;
432
- max-width: 100%;
433
- }
434
-
435
405
  .jp-RenderedHTMLCommon {
436
406
  padding-right: 0;
437
407
  }
@@ -457,13 +427,6 @@ blockquote p:last-child {
457
427
  /* MOBILE */
458
428
 
459
429
  @media screen and (aspect-ratio < 1) {
460
- .hero{
461
- background-position-x: 29%;
462
- }
463
- .hero h2{
464
- padding-left: 0;
465
- padding-right: 0;
466
- }
467
430
  .container.fill-height > .ma-8.row{
468
431
  max-width: 80%;
469
432
  }
@@ -1,5 +1,5 @@
1
1
  from .docs import CategoryLayout, Gallery, NoPage, SubCategoryLayout, WithCode # noqa
2
2
  from .header import Header
3
- from .hero import Hero
3
+ from .markdown import MarkdownWithMetadata
4
4
 
5
- __all__ = ["Header", "Hero"]
5
+ __all__ = ["Header", "MarkdownWithMetadata", "CategoryLayout", "Gallery", "NoPage", "SubCategoryLayout", "WithCode"]
@@ -7,14 +7,19 @@
7
7
  v-model="query"
8
8
  prepend-inner-icon="mdi-magnify"
9
9
  hide-details
10
+ dense
10
11
  :placeholder="mac ? '⌘K to search' : 'Ctrl+K to search'"
12
+ filled
11
13
  outlined
12
- rounded
13
14
  clearable
14
15
  ref="search"
15
16
  style="flex-grow: 1; max-width: 650px;"
16
17
  @click="show($event, on);"
17
- @keyup.enter="item = 0"
18
+ @keyup.enter="hoverItem === null ? item = 0 : item = hoverItem"
19
+ @keyup.esc="close();"
20
+ @keyup.down="hoverItem = hoverItem == null ? 0 : Math.min(hoverItem + 1, results.hits.length - 1, 9)"
21
+ @keyup.up="hoverItem = hoverItem == null ? 9 : Math.max(hoverItem - 1, 0)"
22
+ @focus="selectText();"
18
23
  class="algolia"
19
24
  ></v-text-field>
20
25
  </template>
@@ -27,7 +32,7 @@
27
32
  </v-list>
28
33
  <v-list v-else :style="{width: menuWidth + 'px'}">
29
34
  <v-list-item-group v-model="item">
30
- <v-list-item v-for="(element, index) in this.results.hits" :key="element['url']">
35
+ <v-list-item v-for="(element, index) in this.results.hits" :key="element['url']" :input-value="index === hoverItem">
31
36
  <v-list-item-content>
32
37
  <v-list-item-title>
33
38
  {{ element.hierarchy.lvl1 }}
@@ -39,7 +44,7 @@
39
44
  </v-list-item-content>
40
45
  </v-list-item>
41
46
  </v-list-item-group>
42
- <v-list-item>
47
+ <v-list-item v-if="this.results.nbHits > 10">
43
48
  <v-list-item-content>
44
49
  <v-list-item-title>And {{ this.results.nbHits - 10}} More...</v-list-item-title>
45
50
  </v-list-item-content>
@@ -58,7 +63,6 @@ module.exports = {
58
63
  window.search = this;
59
64
  this.updateMenuWidth();
60
65
  window.addEventListener('resize', this.updateMenuWidth);
61
-
62
66
  },
63
67
  watch: {
64
68
  query ( value ) {
@@ -66,12 +70,17 @@ module.exports = {
66
70
  this.search();
67
71
  },
68
72
  item ( value ) {
69
- if ( this.results.hits != null && this.results.hits.length > 0 ) {
73
+ if ( value === null ) return;
74
+ if ( this.results.hits != null && this.results.hits.length >= value ) {
70
75
  let url = this.results.hits[value].url;
71
76
  if (url.startsWith("https://solara.dev")) {
72
77
  url = url.slice(18);
73
78
  }
74
79
  solara.router.push( url );
80
+ this.close();
81
+ // reset the search
82
+ this.item = null;
83
+ this.hoverItem = null;
75
84
  }
76
85
  },
77
86
  },
@@ -82,6 +91,10 @@ module.exports = {
82
91
  on.click( e )
83
92
  } );
84
93
  },
94
+ close() {
95
+ this.show_results = false;
96
+ this.$refs.search.blur();
97
+ },
85
98
  initSearch() {
86
99
  this.client = this.algoliasearch( '9KW9L7O5EQ', '647ca12ba642437cc40c2adee4a78d08' );
87
100
  this.index = this.client.initIndex( 'solara' );
@@ -107,6 +120,9 @@ module.exports = {
107
120
  this.$set(this.results, []);
108
121
  }
109
122
  },
123
+ selectText() {
124
+ this.$refs.search.$refs.input.select();
125
+ },
110
126
  getSnippet( element ) {
111
127
  if (element.type == "content") {
112
128
  return element._highlightResult.content.value;
@@ -176,6 +192,7 @@ module.exports = {
176
192
  query: '',
177
193
  results: [],
178
194
  item: null,
195
+ hoverItem: null,
179
196
  show_results: false,
180
197
  mac: false,
181
198
  menuWidth: 0,
@@ -0,0 +1,28 @@
1
+ from typing import List
2
+ import solara
3
+
4
+
5
+ @solara.component
6
+ def BreadCrumbs():
7
+ router = solara.use_router()
8
+ routes = router.path_routes
9
+
10
+ with solara.Row(style={"align-items": "center", "flex-wrap": "wrap"}) as main:
11
+ for i, route in enumerate(routes):
12
+ if i == len(routes) - 1:
13
+ solara.Text(route.label or route.path, style={"color": "var(--color-text-fade)"})
14
+ else:
15
+ with solara.Link(solara.resolve_path(route), style={"color": "var(--color-text-fade)"}):
16
+ solara.Text(route.label or route.path)
17
+ if i != len(routes) - 1:
18
+ solara.Text("/", style={"font-size": "1.5rem", "color": "var(--color-text-fade)"})
19
+ return main
20
+
21
+
22
+ def _resolve_path_to_route(path_to_find: List[str], all_routes: List[solara.Route], routes: List[solara.Route] = []):
23
+ if len(path_to_find) == 0:
24
+ return routes
25
+ for route in all_routes:
26
+ if path_to_find[0] == route.path:
27
+ routes += [route]
28
+ return _resolve_path_to_route(path_to_find[1:], route.children, routes)
@@ -0,0 +1,144 @@
1
+ import json
2
+ import os
3
+ import requests
4
+
5
+ from typing import Any, Dict, Optional
6
+ import solara
7
+
8
+
9
+ postmark_api_key = None
10
+ contact_email_address = None
11
+
12
+ try:
13
+ postmark_api_key = os.environ["POSTMARK_API_KEY"]
14
+ contact_email_address = os.environ["SOLARA_CONTACT_EMAIL_ADDRESS"]
15
+ except Exception:
16
+ pass
17
+
18
+
19
+ @solara.component
20
+ def Contact(
21
+ style: Dict[str, Any] = {},
22
+ title="Contact Us",
23
+ subtitle="We'd love to hear from you!",
24
+ submit_label="Submit",
25
+ email_subject="Contact Form Submission",
26
+ ):
27
+ first_name = solara.use_reactive("")
28
+ last_name = solara.use_reactive("")
29
+ email = solara.use_reactive("")
30
+ company = solara.use_reactive("")
31
+ message = solara.use_reactive("")
32
+ error: solara.Reactive[Optional[str]] = solara.use_reactive(None)
33
+ success = solara.use_reactive(False)
34
+
35
+ def send(*_ignore):
36
+ if postmark_api_key is None or contact_email_address is None:
37
+ error.set("Email service not properly configured. Please contact the site administrator at solara@widgetti.io.")
38
+ elif not first_name.value or not last_name.value or not email.value or not message.value:
39
+ error.set("Please fill out all required fields.")
40
+ else:
41
+ # Create the email content
42
+ msg = {}
43
+ msg["From"] = contact_email_address
44
+ msg["To"] = contact_email_address
45
+ msg["Subject"] = email_subject
46
+ msg["ReplyTo"] = email.value
47
+
48
+ # Email body
49
+ msg["HtmlBody"] = f"""
50
+ <b>First Name</b>: {first_name.value}<br />
51
+ <b>Last Name</b>: {last_name.value}<br />
52
+ <b>Email</b>: {email.value}<br />
53
+ <b>Company</b>: {company.value}<br />
54
+ <b>Message</b>: {message.value}<br />
55
+ """
56
+
57
+ # Send emails
58
+ try:
59
+ requests.post(
60
+ "https://api.postmarkapp.com/email",
61
+ headers={
62
+ "Accept": "application/json",
63
+ "Content-Type": "application/json",
64
+ "X-Postmark-Server-Token": postmark_api_key,
65
+ },
66
+ data=json.dumps(msg),
67
+ )
68
+ requests.post(
69
+ "https://api.postmarkapp.com/email",
70
+ headers={
71
+ "Accept": "application/json",
72
+ "Content-Type": "application/json",
73
+ "X-Postmark-Server-Token": postmark_api_key,
74
+ },
75
+ data=json.dumps(
76
+ {
77
+ "From": contact_email_address,
78
+ "To": email.value,
79
+ "Subject": "Thank you for contacting Solara",
80
+ "HtmlBody": f"""
81
+ <p>Hi {first_name.value},</p>
82
+ <p>Thank you for contacting us! We will get back to you as soon as possible.</p>
83
+ <p>Best regards,<br />The Solara Team</p>
84
+ """,
85
+ }
86
+ ),
87
+ )
88
+ except Exception as e:
89
+ error.set(f"Error sending email: {e}")
90
+ else:
91
+ success.set(True)
92
+ error.set(None)
93
+ first_name.set("")
94
+ last_name.set("")
95
+ email.set("")
96
+ company.set("")
97
+ message.set("")
98
+
99
+ with solara.Card(title=title, style={"width": "100%", "max-width": "1024px", **style}):
100
+ solara.Markdown(subtitle)
101
+ solara.Text("* Required fields")
102
+ with solara.Row():
103
+ solara.InputText(label="First Name *", value=first_name)
104
+ solara.InputText(label="Last Name *", value=last_name)
105
+ with solara.Row():
106
+ solara.InputText(label="Email *", value=email)
107
+ solara.InputText(label="Company", value=company)
108
+ solara.v.Textarea(placeholder="Message *", v_model=message.value, on_v_model=message.set)
109
+ with solara.CardActions():
110
+ solara.Button(label=submit_label, color="primary", on_click=send)
111
+ solara.Button(
112
+ label="Clear",
113
+ color="secondary",
114
+ text=True,
115
+ on_click=lambda: [first_name.set(""), last_name.set(""), email.set(""), company.set(""), message.set("")],
116
+ )
117
+
118
+ solara.Style(
119
+ """
120
+ .v-snack__wrapper {
121
+ box-shadow: none;
122
+ }
123
+ """
124
+ )
125
+
126
+ with solara.v.Snackbar(
127
+ v_model=error.value is not None,
128
+ timeout=5000,
129
+ on_v_model=lambda *_: error.set(None),
130
+ left=True,
131
+ color="error",
132
+ ):
133
+ solara.Markdown(error.value or "", style={"--dark-color-text": "white", "--color-text": "white"})
134
+ solara.Button(icon=True, icon_name="mdi-close", color="white", on_click=lambda: error.set(None))
135
+
136
+ with solara.v.Snackbar(
137
+ v_model=success.value,
138
+ timeout=5000,
139
+ on_v_model=lambda *_: success.set(False),
140
+ left=True,
141
+ color="success",
142
+ ):
143
+ solara.Markdown("Your message has been sent!", style={"--dark-color-text": "white", "--color-text": "white"})
144
+ solara.Button(icon=True, icon_name="mdi-close", color="white", on_click=lambda: success.set(False))