diff --git a/scripts/build-assets.ts b/scripts/build-assets.ts
index d56de65..442f5ad 100644
--- a/scripts/build-assets.ts
+++ b/scripts/build-assets.ts
@@ -11,7 +11,9 @@ const sourceDirectory = `${projectRoot}src/miniapp`;
const outputDirectory = `${projectRoot}dist/miniapp/public`;
await mkdir(outputDirectory, { recursive: true });
-await copyFile(`${sourceDirectory}/index.html`, `${outputDirectory}/index.html`);
+for (const name of ["index.html", "favicon.ico"]) {
+ await copyFile(`${sourceDirectory}/${name}`, `${outputDirectory}/${name}`);
+}
const result = await Bun.build({
entrypoints: [`${sourceDirectory}/client.tsx`],
diff --git a/src/miniapp/app.tsx b/src/miniapp/app.tsx
index 37beea5..0a489cc 100644
--- a/src/miniapp/app.tsx
+++ b/src/miniapp/app.tsx
@@ -1,5 +1,5 @@
/** Shared browser and Telegram application shell. */
-import { CalendarClock, LogOut, SlidersHorizontal, Sparkles, Terminal } from "lucide-react";
+import { CalendarClock, LogOut, SlidersHorizontal, Sparkles } from "lucide-react";
import { type ReactElement, useEffect, useState } from "react";
import { requestSnapshot } from "./api.js";
import { SchedulesManager } from "./schedules.js";
@@ -97,17 +97,7 @@ export function SettingsApp({
)}
-
-
-
- WirebotYour Codex workspace
-
- >
- }
- >
+ }>
{tabs.map(({ id, label, icon: Icon }) => (
}
>
-
+
);
@@ -200,3 +190,23 @@ function SettingsLoading({
);
}
+
+/** Full logo from https://wirebot.ai/. */
+function WirebotLogo({ className }: { readonly className?: string }): ReactElement {
+ return (
+
+ );
+}
diff --git a/src/miniapp/favicon.ico b/src/miniapp/favicon.ico
new file mode 100644
index 0000000..a3fc79d
Binary files /dev/null and b/src/miniapp/favicon.ico differ
diff --git a/src/miniapp/index.html b/src/miniapp/index.html
index d2dab26..fa81fbc 100644
--- a/src/miniapp/index.html
+++ b/src/miniapp/index.html
@@ -7,9 +7,10 @@
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
-
-
+
+
Wirebot
+
diff --git a/src/miniapp/server.ts b/src/miniapp/server.ts
index 4629c4b..c6ef7c4 100644
--- a/src/miniapp/server.ts
+++ b/src/miniapp/server.ts
@@ -29,13 +29,17 @@ const MAX_REQUEST_BYTES = 32 * 1_024;
const MAX_AUTH_AGE_SECONDS = 60 * 60;
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
-const staticAssets = new Map([
+const staticAssets = new Map<
+ string,
+ readonly ["index.html" | "app.js" | "app.css" | "favicon.ico", string]
+>([
...["/", "/app", "/app/", "/app/settings", "/app/skills", "/app/schedules"].map(
(path): [string, readonly ["index.html", string]] => [
path,
["index.html", "text/html; charset=utf-8"],
],
),
+ ["/favicon.ico", ["favicon.ico", "image/x-icon"]],
["/miniapp", ["index.html", "text/html; charset=utf-8"]],
["/miniapp/", ["index.html", "text/html; charset=utf-8"]],
["/miniapp/app.js", ["app.js", "text/javascript; charset=utf-8"]],
@@ -135,6 +139,7 @@ export class MiniAppServer {
access(join(this.#assetDirectory, "index.html")),
access(join(this.#assetDirectory, "app.js")),
access(join(this.#assetDirectory, "app.css")),
+ access(join(this.#assetDirectory, "favicon.ico")),
]);
await new Promise((resolve, reject) => {
const onError = (error: Error): void => {
@@ -503,7 +508,7 @@ export class MiniAppServer {
private async sendAsset(
response: ServerResponse,
method: string,
- name: "index.html" | "app.js" | "app.css",
+ name: "index.html" | "app.js" | "app.css" | "favicon.ico",
contentType: string,
): Promise {
// Build outputs never change while the process runs; serve them from memory.
diff --git a/src/miniapp/styles.css b/src/miniapp/styles.css
index 84937c7..006c2fb 100644
--- a/src/miniapp/styles.css
+++ b/src/miniapp/styles.css
@@ -26,26 +26,26 @@
:root {
color-scheme: light;
--radius: 0.875rem;
- --background: #f5f7fb;
- --foreground: #172337;
+ --background: #f5f2ec;
+ --foreground: #171717;
--card: #ffffff;
--card-foreground: var(--foreground);
- --primary: #285bd4;
+ --primary: #126bdd;
--primary-foreground: #ffffff;
- --secondary: #eaf0fa;
- --secondary-foreground: #253b60;
- --muted: #edf1f7;
- --muted-foreground: #5d6c81;
- --accent: #e9effb;
+ --secondary: #e8e5df;
+ --secondary-foreground: #242424;
+ --muted: #eae7e1;
+ --muted-foreground: #66635f;
+ --accent: #e8e5df;
--accent-foreground: var(--foreground);
--destructive: #c32e3d;
--destructive-foreground: #ffffff;
--switch-thumb: #ffffff;
- --border: #dce3ee;
- --input: #c7d2e2;
+ --border: #d7d3cc;
+ --input: #b8b3aa;
--ring: var(--primary);
- --section-header: #5d6c81;
- --tabbar-background: #ffffff;
+ --section-header: #66635f;
+ --tabbar-background: #efede7;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -54,22 +54,22 @@
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
- --background: #10141c;
- --foreground: #e5ebf5;
- --card: #191f2b;
- --primary: #90b4ff;
- --primary-foreground: #122544;
- --secondary: #263248;
- --secondary-foreground: #d9e5ff;
- --muted: #242d3c;
- --muted-foreground: #a0aec3;
- --accent: #293650;
+ --background: #171717;
+ --foreground: #f5f2ec;
+ --card: #202020;
+ --primary: #1e83ff;
+ --primary-foreground: #ffffff;
+ --secondary: #2e2e2e;
+ --secondary-foreground: #f5f2ec;
+ --muted: #2a2a2a;
+ --muted-foreground: #b4b2ad;
+ --accent: #303030;
--destructive: #ff8793;
--destructive-foreground: #35151a;
- --border: #303c50;
- --input: #42516a;
- --section-header: #a0aec3;
- --tabbar-background: #191f2b;
+ --border: #4e4e4e;
+ --input: #5b5b5b;
+ --section-header: #b4b2ad;
+ --tabbar-background: #171717;
}
}
@@ -1542,6 +1542,39 @@ button {
min-height: 100dvh;
}
+/* Website palette and shapes, with system typography and accessible button contrast. */
+.browserApp .ui-button,
+.browserApp .navItems a {
+ border-radius: 999px;
+}
+.browserApp .ui-button.bg-primary {
+ background: color-mix(in srgb, var(--primary) 85%, black);
+}
+.browserApp .navItems a:is(:hover, [aria-current="page"]) {
+ background: var(--secondary);
+ color: var(--foreground);
+}
+.browserApp .ui-section-body,
+.browserApp .scheduleCard,
+.browserApp .resetDialog,
+.browserApp .loadingRoot > div {
+ border-radius: 9px;
+ box-shadow: none;
+}
+.browserApp .nativeControl {
+ border-radius: 9px;
+}
+.browserApp .pageHeader {
+ padding-bottom: 28px;
+}
+.browserApp .pageHeader h1 {
+ font-size: clamp(1.75rem, 4vw, 2.25rem);
+ letter-spacing: -0.035em;
+}
+.browserApp .sectionStack {
+ gap: 28px;
+}
+
.browserHeader {
display: flex;
justify-content: space-between;
@@ -1602,24 +1635,12 @@ button {
border-right: 1px solid var(--border);
}
.browserApp .navBrand {
- display: flex;
- align-items: center;
- gap: 12px;
+ display: block;
padding: 0 12px 36px;
- font-size: 1.2rem;
- font-weight: 700;
}
.browserApp .navBrand svg {
- width: 32px;
- height: 32px;
- color: var(--primary);
- }
- .browserApp .navBrand small {
- display: block;
- margin-top: 3px;
- font-size: 0.7rem;
- font-weight: 400;
- color: var(--muted-foreground);
+ width: 160px;
+ height: auto;
}
.browserApp .navItems {
display: flex;
diff --git a/test/fixtures/web-app.ts b/test/fixtures/web-app.ts
index 628d22e..3bea336 100644
--- a/test/fixtures/web-app.ts
+++ b/test/fixtures/web-app.ts
@@ -64,7 +64,7 @@ export async function startTestApp(assetDirectory?: string, includeTelegram = fa
const ownedDirectory = assetDirectory === undefined ? assets : undefined;
if (ownedDirectory !== undefined) {
await Promise.all(
- ["index.html", "app.js", "app.css"].map((name) =>
+ ["index.html", "app.js", "app.css", "favicon.ico"].map((name) =>
Bun.write(
join(assets, name),
name === "index.html" ? "Wirebot" : "",
diff --git a/test/miniapp-browser.test.ts b/test/miniapp-browser.test.ts
index a26bfab..93fdbea 100644
--- a/test/miniapp-browser.test.ts
+++ b/test/miniapp-browser.test.ts
@@ -35,10 +35,12 @@ describe("Browser HTTP app", () => {
"/miniapp/",
"/miniapp/app.js",
"/miniapp/app.css",
+ "/favicon.ico",
]) {
expect((await request(path)).status).toBe(200);
expect((await request(path, { method: "HEAD" })).status).toBe(200);
}
+ expect((await request("/favicon.ico")).headers.get("content-type")).toBe("image/x-icon");
expect((await request("/app/unknown")).status).toBe(404);
expect((await request("/app", { method: "POST" })).status).toBe(405);
for (const path of [