diff --git a/apps/browser/assets/about-newtab.css b/apps/browser/assets/about-newtab.css
new file mode 100644
index 000000000..25b1657c9
--- /dev/null
+++ b/apps/browser/assets/about-newtab.css
@@ -0,0 +1,28 @@
+.about-newtab {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f0f0f0;
+}
+.about-newtab .container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+}
+.about-newtab img {
+ width: 160px;
+ height: 160px;
+}
+.about-newtab .search-input {
+ width: 320px;
+ padding: 10px 14px;
+ font-size: 16px;
+ border: 1px solid #ccc;
+ border-radius: 8px;
+ outline: none;
+}
+.about-newtab .search-input:focus {
+ border-color: #5e9ed6;
+}
diff --git a/apps/browser/assets/about-stub.css b/apps/browser/assets/about-stub.css
new file mode 100644
index 000000000..eab8336c3
--- /dev/null
+++ b/apps/browser/assets/about-stub.css
@@ -0,0 +1,13 @@
+.about-stub {
+ height: 100%;
+ background: #f0f0f0;
+ color: #333;
+ padding: 40px;
+}
+.about-stub h1 {
+ margin: 0 0 8px;
+ font-size: 28px;
+}
+.about-stub p {
+ color: #888;
+}
diff --git a/apps/browser/assets/browser.css b/apps/browser/assets/browser.css
index 81e9781f9..920655968 100644
--- a/apps/browser/assets/browser.css
+++ b/apps/browser/assets/browser.css
@@ -241,12 +241,19 @@ body,
width: 16px;
}
+.menu-item-separator {
+ height: 1px;
+ background: #e8e8e8;
+ margin: 4px 0;
+}
-.webview {
+
+.tab-content {
flex: 1 1 0px;
height: 100%;
width: 100%;
background: white;
+ font-family: sans-serif;
}
.fps-overlay {
diff --git a/apps/browser/assets/start.html b/apps/browser/assets/start.html
deleted file mode 100644
index cb56daaea..000000000
--- a/apps/browser/assets/start.html
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/browser/src/about_pages.rs b/apps/browser/src/about_pages.rs
new file mode 100644
index 000000000..d1d8e9a25
--- /dev/null
+++ b/apps/browser/src/about_pages.rs
@@ -0,0 +1,152 @@
+use blitz_traits::net::{Request, Url};
+use dioxus_native::prelude::*;
+
+use crate::nav::{is_enter_key, req_from_string};
+
+const NEWTAB_CSS: Asset = asset!("../assets/about-newtab.css");
+const STUB_CSS: Asset = asset!("../assets/about-stub.css");
+const BLITZ_LOGO: Asset = asset!("../assets/blitz-logo.png");
+
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum AboutPage {
+ NewTab,
+ Settings,
+ History,
+ Bookmarks,
+}
+
+impl AboutPage {
+ pub fn from_url(url: &Url) -> Option {
+ if url.scheme() != "about" {
+ return None;
+ }
+ match url.path() {
+ "newtab" | "" => Some(Self::NewTab),
+ "settings" => Some(Self::Settings),
+ "history" => Some(Self::History),
+ "bookmarks" => Some(Self::Bookmarks),
+ _ => None,
+ }
+ }
+
+ pub const fn title(self) -> &'static str {
+ match self {
+ Self::NewTab => "New Tab",
+ Self::Settings => "Settings",
+ Self::History => "History",
+ Self::Bookmarks => "Bookmarks",
+ }
+ }
+
+ pub const fn url_str(self) -> &'static str {
+ match self {
+ Self::NewTab => "about:newtab",
+ Self::Settings => "about:settings",
+ Self::History => "about:history",
+ Self::Bookmarks => "about:bookmarks",
+ }
+ }
+
+ #[allow(
+ clippy::unwrap_used,
+ reason = "URL strings are static literals — Url::parse cannot fail"
+ )]
+ pub fn parsed_url(self) -> Url {
+ Url::parse(self.url_str()).unwrap()
+ }
+}
+
+#[component]
+pub fn AboutPageView(page: AboutPage, on_navigate: Callback) -> Element {
+ match page {
+ AboutPage::NewTab => rsx!(NewTabPage { on_navigate }),
+ AboutPage::Settings => rsx!(StubPage { name: "Settings" }),
+ AboutPage::History => rsx!(StubPage { name: "History" }),
+ AboutPage::Bookmarks => rsx!(StubPage { name: "Bookmarks" }),
+ }
+}
+
+#[component]
+fn NewTabPage(on_navigate: Callback) -> Element {
+ let mut query = use_signal(String::new);
+ rsx! {
+ document::Link { rel: "stylesheet", href: NEWTAB_CSS }
+ div { class: "about-newtab",
+ div { class: "container",
+ img { src: BLITZ_LOGO, alt: "Blitz" }
+ input {
+ class: "search-input",
+ r#type: "text",
+ name: "q",
+ autofocus: true,
+ value: "{query}",
+ oninput: move |evt| query.set(evt.value()),
+ onkeydown: move |evt| {
+ if is_enter_key(&evt.key()) {
+ evt.prevent_default();
+ let q = query.read().clone();
+ if !q.is_empty() {
+ if let Some(req) = req_from_string(&q) {
+ on_navigate(req);
+ }
+ }
+ }
+ },
+ }
+ }
+ }
+ }
+}
+
+#[component]
+fn StubPage(name: &'static str) -> Element {
+ rsx! {
+ document::Link { rel: "stylesheet", href: STUB_CSS }
+ div { class: "about-stub",
+ h1 { "{name}" }
+ p { "Coming soon." }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_known_paths() {
+ for (path, expected) in [
+ ("about:newtab", AboutPage::NewTab),
+ ("about:settings", AboutPage::Settings),
+ ("about:history", AboutPage::History),
+ ("about:bookmarks", AboutPage::Bookmarks),
+ ] {
+ let url = Url::parse(path).unwrap();
+ assert_eq!(AboutPage::from_url(&url), Some(expected), "{path}");
+ }
+ }
+
+ #[test]
+ fn rejects_unknown() {
+ assert_eq!(
+ AboutPage::from_url(&Url::parse("about:nope").unwrap()),
+ None
+ );
+ assert_eq!(
+ AboutPage::from_url(&Url::parse("https://example.com").unwrap()),
+ None
+ );
+ }
+
+ #[test]
+ fn url_roundtrip() {
+ for p in [
+ AboutPage::NewTab,
+ AboutPage::Settings,
+ AboutPage::History,
+ AboutPage::Bookmarks,
+ ] {
+ assert_eq!(AboutPage::from_url(&p.parsed_url()), Some(p));
+ }
+ }
+}
diff --git a/apps/browser/src/document_loader.rs b/apps/browser/src/document_loader.rs
index f0b1820d0..654da8734 100644
--- a/apps/browser/src/document_loader.rs
+++ b/apps/browser/src/document_loader.rs
@@ -74,22 +74,6 @@ impl DocumentLoader {
}
pub async fn load_document(&self, req: Request) -> LoadedDocument {
- if req.url.scheme() == "about" && req.url.path() == "newtab" {
- let config = make_doc_config(
- None,
- Arc::clone(&self.net_provider),
- self.history,
- self.font_ctx.clone(),
- );
- let html = include_str!("../assets/start.html");
- let document = HtmlDocument::from_html(html, config).into_inner();
- return LoadedDocument {
- document: SubDocumentAttr::new(document),
- html_source: html.to_string(),
- title: String::new(),
- };
- }
-
let net_provider = Arc::clone(&self.net_provider);
let font_ctx = self.font_ctx.clone();
let history = self.history;
diff --git a/apps/browser/src/main.rs b/apps/browser/src/main.rs
index c67b0845e..2b3f73f23 100644
--- a/apps/browser/src/main.rs
+++ b/apps/browser/src/main.rs
@@ -18,6 +18,7 @@ use winit::platform::macos::WindowAttributesMacOS;
pub(crate) type StdNetProvider = blitz_net::Provider;
+mod about_pages;
#[cfg(any(feature = "screenshot", feature = "capture"))]
mod capture;
mod document_loader;
@@ -25,11 +26,13 @@ mod document_loader;
mod fps_overlay;
mod history;
mod icons;
+mod nav;
mod status_bar;
mod tab;
mod tab_strip;
mod toolbar;
+use about_pages::AboutPage;
use status_bar::StatusBar;
use tab::{Tab, TabId, TabStoreImplExt, TabWebView, active_tab, open_tab, tab_title_or_url};
use tab_strip::TabStrip;
@@ -37,7 +40,6 @@ use toolbar::Toolbar;
static BROWSER_UI_STYLES: Asset = asset!("../assets/browser.css");
pub(crate) const IS_MOBILE: bool = cfg!(any(target_os = "android", target_os = "ios"));
-pub(crate) const HOME_URL_STR: &str = "about:newtab";
#[unsafe(no_mangle)]
#[cfg(target_os = "android")]
@@ -63,7 +65,7 @@ fn main() {
}
fn app() -> Element {
- let home_url = use_hook(|| Url::parse(HOME_URL_STR).unwrap());
+ let home_url = use_hook(|| AboutPage::NewTab.parsed_url());
let net_provider = use_context::>();
let url_input_handle: Signal