diff --git a/apps/web/public/apple-touch-icon-dev.png b/apps/web/public/apple-touch-icon-dev.png new file mode 100644 index 0000000000..fd64ccaa03 Binary files /dev/null and b/apps/web/public/apple-touch-icon-dev.png differ diff --git a/apps/web/public/favicon-dev-16x16.png b/apps/web/public/favicon-dev-16x16.png new file mode 100644 index 0000000000..bba08c01d5 Binary files /dev/null and b/apps/web/public/favicon-dev-16x16.png differ diff --git a/apps/web/public/favicon-dev-32x32.png b/apps/web/public/favicon-dev-32x32.png new file mode 100644 index 0000000000..fbef83004a Binary files /dev/null and b/apps/web/public/favicon-dev-32x32.png differ diff --git a/apps/web/public/favicon-dev.ico b/apps/web/public/favicon-dev.ico new file mode 100644 index 0000000000..0113a4ae07 Binary files /dev/null and b/apps/web/public/favicon-dev.ico differ diff --git a/apps/web/public/manifest-t3-dev.webmanifest b/apps/web/public/manifest-t3-dev.webmanifest new file mode 100644 index 0000000000..530f6fce11 --- /dev/null +++ b/apps/web/public/manifest-t3-dev.webmanifest @@ -0,0 +1,31 @@ +{ + "id": "/", + "name": "T3 Code", + "short_name": "T3 Code", + "description": "A minimal web GUI for using code agents like Codex.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#ffffff", + "icons": [ + { + "src": "/apple-touch-icon-dev.png", + "sizes": "180x180", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/favicon-dev-32x32.png", + "sizes": "32x32", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/favicon-dev-16x16.png", + "sizes": "16x16", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/apps/web/public/sw.js b/apps/web/public/sw.js index 7c9477a8d2..245099f79b 100644 --- a/apps/web/public/sw.js +++ b/apps/web/public/sw.js @@ -1,12 +1,17 @@ -const APP_SHELL_CACHE = "t3code-app-shell-v1"; +const APP_SHELL_CACHE = "t3code-app-shell-v2"; const APP_SHELL_URL = "/"; const APP_SHELL_ASSETS = [ APP_SHELL_URL, "/manifest.webmanifest", + "/manifest-t3-dev.webmanifest", "/apple-touch-icon.png", + "/apple-touch-icon-dev.png", "/favicon.ico", + "/favicon-dev.ico", "/favicon-32x32.png", + "/favicon-dev-32x32.png", "/favicon-16x16.png", + "/favicon-dev-16x16.png", ]; function isAppNavigation(request, url) { diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index bc07f4139a..f0827501e1 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -5,17 +5,20 @@ import { createHashHistory, createBrowserHistory } from "@tanstack/react-router" import "@xterm/xterm/css/xterm.css"; import "./index.css"; +import "./overrides.css"; import { isElectron } from "./env"; import { registerServiceWorker } from "./pwa"; import { getRouter } from "./router"; import { APP_DISPLAY_NAME } from "./branding"; +import { applyRuntimeBranding } from "./runtimeBranding"; // Electron loads the app from a file-backed shell, so hash history avoids path resolution issues. const history = isElectron ? createHashHistory() : createBrowserHistory(); const router = getRouter(history); +applyRuntimeBranding(document, window.location.hostname); document.title = APP_DISPLAY_NAME; void registerServiceWorker(); diff --git a/apps/web/src/overrides.css b/apps/web/src/overrides.css new file mode 100644 index 0000000000..27e5af119c --- /dev/null +++ b/apps/web/src/overrides.css @@ -0,0 +1,42 @@ +/* Local, intentionally brittle UI tweaks that we want to keep isolated from upstream edits. */ + +:root[data-host-variant="t3-dev"] + .flex.min-w-0.flex-1.items-center.gap-1.ml-1.cursor-pointer + > span:last-child { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 0.1rem; + padding-inline: 0.42rem; + padding-block: 0.18rem; + background: color-mix(in srgb, var(--color-red-500) 12%, transparent); + color: transparent; + font-size: 0; + line-height: 1; + letter-spacing: 0; +} + +:root[data-host-variant="t3-dev"] + .flex.min-w-0.flex-1.items-center.gap-1.ml-1.cursor-pointer + > span:last-child::after { + content: "DEVELOP"; + color: color-mix(in srgb, var(--color-red-600) 82%, var(--foreground)); + font-size: 8px; + font-weight: 600; + line-height: 1; + letter-spacing: 0.16em; + transform: translateY(0.5px); +} + +:root.dark[data-host-variant="t3-dev"] + .flex.min-w-0.flex-1.items-center.gap-1.ml-1.cursor-pointer + > span:last-child { + background: color-mix(in srgb, var(--color-red-500) 18%, transparent); +} + +:root.dark[data-host-variant="t3-dev"] + .flex.min-w-0.flex-1.items-center.gap-1.ml-1.cursor-pointer + > span:last-child::after { + color: color-mix(in srgb, var(--color-red-400) 88%, var(--foreground)); +} diff --git a/apps/web/src/runtimeBranding.test.ts b/apps/web/src/runtimeBranding.test.ts new file mode 100644 index 0000000000..07bb1abf65 --- /dev/null +++ b/apps/web/src/runtimeBranding.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; + +import { T3_DEV_HOST_VARIANT, resolveRuntimeBranding } from "./runtimeBranding"; + +describe("resolveRuntimeBranding", () => { + it("uses the red PWA assets on t3-dev", () => { + expect(resolveRuntimeBranding("t3-dev.claude.do")).toEqual({ + hostVariant: T3_DEV_HOST_VARIANT, + manifestPath: "/manifest-t3-dev.webmanifest", + appleTouchIconPath: "/apple-touch-icon-dev.png", + faviconPath: "/favicon-dev.ico", + }); + }); + + it("matches the dev host case-insensitively", () => { + expect(resolveRuntimeBranding("T3-DEV.CLAUDE.DO").hostVariant).toBe(T3_DEV_HOST_VARIANT); + }); + + it("leaves other hosts unchanged", () => { + expect(resolveRuntimeBranding("t3.claude.do")).toEqual({}); + expect(resolveRuntimeBranding("localhost")).toEqual({}); + }); +}); diff --git a/apps/web/src/runtimeBranding.ts b/apps/web/src/runtimeBranding.ts new file mode 100644 index 0000000000..132e6efdc6 --- /dev/null +++ b/apps/web/src/runtimeBranding.ts @@ -0,0 +1,51 @@ +export const T3_DEV_HOSTNAME = "t3-dev.claude.do"; +export const T3_DEV_HOST_VARIANT = "t3-dev"; + +export interface RuntimeBranding { + readonly hostVariant?: string; + readonly manifestPath?: string; + readonly appleTouchIconPath?: string; + readonly faviconPath?: string; +} + +export function resolveRuntimeBranding(hostname: string): RuntimeBranding { + if (hostname.trim().toLowerCase() !== T3_DEV_HOSTNAME) { + return {}; + } + + return { + hostVariant: T3_DEV_HOST_VARIANT, + manifestPath: "/manifest-t3-dev.webmanifest", + appleTouchIconPath: "/apple-touch-icon-dev.png", + faviconPath: "/favicon-dev.ico", + }; +} + +export function applyRuntimeBranding(doc: Document, hostname: string): void { + const branding = resolveRuntimeBranding(hostname); + + if (branding.hostVariant) { + doc.documentElement.dataset.hostVariant = branding.hostVariant; + } else { + delete doc.documentElement.dataset.hostVariant; + } + + if (branding.manifestPath) { + setLinkHref(doc, 'link[rel="manifest"]', branding.manifestPath); + } + + if (branding.appleTouchIconPath) { + setLinkHref(doc, 'link[rel="apple-touch-icon"]', branding.appleTouchIconPath); + } + + if (branding.faviconPath) { + setLinkHref(doc, 'link[rel="icon"]', branding.faviconPath); + setLinkHref(doc, 'link[rel="shortcut icon"]', branding.faviconPath); + } +} + +function setLinkHref(doc: Document, selector: string, href: string): void { + for (const link of doc.querySelectorAll(selector)) { + link.setAttribute("href", href); + } +}