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 metadata::{metadata, tag},
7 view::{BoxView, View, ViewExt, view},
8};
9
10use super::{ShellAssets, Theme, current};
11use crate::panel::{Panel, state::PanelState};
12
13impl Panel {
14 /// Renders a complete HTML document around a page, with the shell settings of the panel the
15 /// request is under. A page outside every prefix belongs to no panel, so it renders without
16 /// them; such a page renders through [`PanelHandle::document`](crate::PanelHandle::document).
17 pub async fn document<'a>(
18 cx: &'a Cx,
19 title: impl Into<String>,
20 body: impl View + 'a,
21 ) -> Result<BoxView<'a>> {
22 Self::render_document(
23 cx,
24 current(cx).map(|panel| &**panel),
25 title.into(),
26 body.boxed(),
27 )
28 .await
29 }
30
31 /// Renders the HTML document with `panel`'s assets, favicon and dark-mode default.
32 pub(crate) async fn render_document<'a>(
33 cx: &'a Cx,
34 panel: Option<&PanelState>,
35 title: String,
36 body: BoxView<'a>,
37 ) -> Result<BoxView<'a>> {
38 let head: BoxView<'_> = match panel.and_then(|panel| panel.shell_assets) {
39 Some(ShellAssets { stylesheet, font }) => view! {
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! { topcoat::dev::script() }.boxed(),
56 };
57 // Runtime navigation keeps the socket, and the socket answers as the panel of the page
58 // that opened it. A script this document has not run makes the browser load the page
59 // instead, so a script of the request's panel alone keeps a socket on its panel's pages.
60 // It says nothing and does nothing: only its text counts. Asked of the request and not of
61 // `panel`, because a page outside the prefix opens a socket of no panel, and has a mark
62 // of its own so that leaving a panel loads the page too.
63 let panel_mark = match current(cx) {
64 Some(panel) => format!("// {}", panel.realm.prefix),
65 None => "// no panel".to_owned(),
66 };
67 // The server renders the stored choice on every page: runtime navigation copies
68 // `<html>`'s attributes from the next page and runs no script.
69 let html_class =
70 Theme::renders_dark(cx, panel.is_some_and(|panel| panel.dark_mode)).then_some("dark");
71 let favicon = panel
72 .and_then(|panel| panel.brand.as_ref())
73 .and_then(|brand| brand.favicon.clone());
74 // Runtime navigation copies `lang` and `dir` with the class, so a page reads in its own
75 // locale.
76 let (_, locale) = crate::i18n::catalog_and_locale(cx);
77 let (lang, dir) = (locale.id().clone(), locale.id().direction());
78 let page = metadata(cx).await?;
79 // The brand's colors over the shipped theme, after its stylesheet so they win.
80 let style = panel
81 .and_then(|panel| panel.brand.as_ref())
82 .and_then(super::Brand::style);
83 let environment = panel.and_then(|panel| panel.environment.as_ref());
84 let title = match environment {
85 Some(environment) => format!("[{}] {title}", environment.name(cx)),
86 None => title,
87 };
88 let ribbon = environment.map(|environment| environment.ribbon(cx));
89 Ok(view! {
90 <!DOCTYPE html>
91 <html lang=(lang) dir=(dir) class=(html_class)>
92 <head>
93 <meta charset="utf-8">
94 // Without it a phone lays the page out 980px wide and zooms it out.
95 <meta name="viewport" content="width=device-width, initial-scale=1">
96 tag::robots(metadata: page)
97 <title>(title)</title>
98 if let Some(favicon) = favicon {
99 <link rel="icon" href=(favicon)>
100 }
101 (head)
102 if let Some(style) = style {
103 <style>(style)</style>
104 }
105 <script>(panel_mark)</script>
106 </head>
107 <body>
108 (body)
109 if let Some(ribbon) = ribbon {
110 (ribbon)
111 }
112 </body>
113 </html>
114 }
115 .boxed())
116 }
117}