tablo_core/panel/shell/document.rs
1//! The HTML document around a page: head, assets, favicon and theme.
2
3use topcoat::{
4 Result,
5 context::Cx,
6 view::{BoxView, View, ViewExt, view},
7};
8
9use super::{ShellAssets, Theme, current};
10use crate::panel::{Panel, state::PanelState};
11
12impl Panel {
13 /// Renders a complete HTML document around a page, with the shell settings of the panel the
14 /// request is under. A page outside every prefix belongs to no panel, so it renders without
15 /// them; such a page renders through [`PanelHandle::document`](crate::PanelHandle::document).
16 pub async fn document<'a>(
17 cx: &'a Cx,
18 title: impl Into<String>,
19 body: impl View + 'a,
20 ) -> Result<BoxView<'a>> {
21 Self::render_document(
22 cx,
23 current(cx).map(|panel| &**panel),
24 title.into(),
25 body.boxed(),
26 )
27 .await
28 }
29
30 /// Renders the HTML document with `panel`'s assets, favicon and dark-mode default.
31 pub(crate) async fn render_document<'a>(
32 cx: &'a Cx,
33 panel: Option<&PanelState>,
34 title: String,
35 body: BoxView<'a>,
36 ) -> Result<BoxView<'a>> {
37 let head: BoxView<'_> = match panel.and_then(|panel| panel.shell_assets) {
38 Some(ShellAssets { stylesheet, font }) => view! {
39 cx =>
40 topcoat::dev::script()
41 topcoat::runtime::script()
42 topcoat::font::link(font: font)
43 <link rel="stylesheet" href=(stylesheet)>
44 // Modules, so each script's top-level names stay its own: as classic scripts they
45 // share one global scope, where a later script's `partsOf` replaces an earlier one's.
46 // A module is deferred like these were.
47 for script in tablo_ui::SHELL_SCRIPTS {
48 <script src=(script) type="module"></script>
49 }
50 // Where `rich_text.js` imports the editor from, once a page holds a rich text
51 // control: no page loads it before.
52 <meta name="tablo-tiptap" content=(tablo_ui::TIPTAP_JS)>
53 }
54 .boxed(),
55 None => view! { cx => topcoat::dev::script() }.boxed(),
56 };
57 // The server renders the stored choice on every page: runtime navigation copies
58 // `<html>`'s attributes from the next page and runs no script.
59 let html_class =
60 Theme::renders_dark(cx, panel.is_some_and(|panel| panel.dark_mode)).then_some("dark");
61 let favicon = panel
62 .and_then(|panel| panel.brand.as_ref())
63 .and_then(|brand| brand.favicon.clone());
64 // Runtime navigation copies `lang` and `dir` with the class, so a page reads in its own
65 // locale.
66 let (_, locale) = crate::i18n::catalog_and_locale(cx);
67 let (lang, dir) = (locale.id().clone(), locale.id().direction());
68 Ok(view! {
69 cx =>
70 <!DOCTYPE html>
71 <html lang=(lang) dir=(dir) class=(html_class)>
72 <head>
73 <meta charset="utf-8">
74 // Without it a phone lays the page out 980px wide and zooms it out.
75 <meta name="viewport" content="width=device-width, initial-scale=1">
76 <title>(title)</title>
77 if let Some(favicon) = favicon {
78 <link rel="icon" href=(favicon)>
79 }
80 (head)
81 </head>
82 <body>(body)</body>
83 </html>
84 }
85 .boxed())
86 }
87}