diff --git a/web/priv/static/index.html b/web/priv/static/index.html
index 5748841..a29fd2c 100644
--- a/web/priv/static/index.html
+++ b/web/priv/static/index.html
@@ -3,23 +3,19 @@
-
-
+
+
crate
diff --git a/web/src/crate_web.gleam b/web/src/crate_web.gleam
index 493fe9b..a95e1d7 100644
--- a/web/src/crate_web.gleam
+++ b/web/src/crate_web.gleam
@@ -141,7 +141,7 @@ fn initial_display() -> model.Display {
}
fn initial_theme() -> model.Theme {
- prefs.get_theme()
+ prefs.get(prefs.theme_key)
|> result.map(model.theme_from_string)
|> result.unwrap(model.System)
}
diff --git a/web/src/crate_web/ffi.ts b/web/src/crate_web/ffi.ts
index 4f22d4c..1ac100c 100644
--- a/web/src/crate_web/ffi.ts
+++ b/web/src/crate_web/ffi.ts
@@ -19,9 +19,26 @@ export function applyTheme(theme: string): void {
}
if (theme === "light" || theme === "dark") {
root.setAttribute("data-theme", theme);
+ syncThemeColor(theme);
return;
}
root.removeAttribute("data-theme");
+ syncThemeColor(theme);
+}
+
+// The browser chrome follows the app's own theme, not the OS: an explicit
+// light/dark choice has to beat prefers-color-scheme, which a media-keyed
+// pair cannot express.
+function syncThemeColor(theme: string): void {
+ const meta = globalThis.document?.getElementById("theme-color");
+ if (!meta) {
+ return;
+ }
+ const prefersDark =
+ !!globalThis.matchMedia &&
+ globalThis.matchMedia("(prefers-color-scheme: dark)").matches;
+ const dark = theme === "dark" || (theme !== "light" && prefersDark);
+ meta.setAttribute("content", dark ? "#191008" : "#f1e7d2");
}
// A real full-page navigation, since modem would otherwise intercept a same-origin click.
@@ -157,14 +174,6 @@ export function prefsSet(key: string, value: string): void {
}
}
-export function prefsRemove(key: string): void {
- try {
- globalThis.localStorage?.removeItem(key);
- } catch {
- // best-effort
- }
-}
-
// The page origin, so a relative BFF path can be made absolute for the
// gleam_http request the async xrpc transport builds (fetch resolves relative
// urls itself, but request.to() needs an absolute one).
diff --git a/web/src/crate_web/prefs.gleam b/web/src/crate_web/prefs.gleam
index 925b4f1..ea983e3 100644
--- a/web/src/crate_web/prefs.gleam
+++ b/web/src/crate_web/prefs.gleam
@@ -16,18 +16,11 @@ fn ffi_get(key: String) -> Dynamic
@external(javascript, "./ffi.ts", "prefsSet")
fn ffi_set(key: String, value: String) -> Nil
-@external(javascript, "./ffi.ts", "prefsRemove")
-fn ffi_remove(key: String) -> Nil
-
/// The crate page's grid/rows toggle.
pub const display_key = "display"
pub const theme_key = "theme"
-/// Every key not-theme started fresh under "crate:"; theme is the only one
-/// that shipped real user data under the pre-rename "at-record:" namespace.
-const legacy_theme_key = "at-record:theme"
-
/// Locally-ignored `catalog.edit` proposal uri+cid pairs, capped and JSON-encoded.
pub const ignored_proposals_key = "ignored-proposals"
@@ -51,24 +44,6 @@ pub fn set(key: String, value: String) -> Nil {
ffi_set(namespaced(key), value)
}
-/// Reads the theme, migrating a value left under the pre-rename key on first
-/// read: adopt it under the new key, then drop the old one so this only
-/// costs a lookup once per user.
-pub fn get_theme() -> Result(String, Nil) {
- case get(theme_key) {
- Ok(value) -> Ok(value)
- Error(Nil) ->
- ffi_get(legacy_theme_key)
- |> decode.run(decode.string)
- |> result.map(fn(value) {
- set(theme_key, value)
- ffi_remove(legacy_theme_key)
- value
- })
- |> result.replace_error(Nil)
- }
-}
-
pub fn encode_ignored_proposals(ignored: List(#(String, String))) -> String {
ignored
|> json.array(fn(pair) {