diff --git a/app.css b/app.css index c612581..7e75d52 100644 --- a/app.css +++ b/app.css @@ -39,8 +39,10 @@ a:hover { color: var(--link-hover); } /* shell */ .shell { display: flex; flex-wrap: wrap; min-height: 100vh; } .side { flex: 1 1 230px; background: var(--ink); color: #c9c9c3; padding: 20px 12px; display: flex; flex-direction: column; gap: 2px; } -.brand { display: flex; align-items: center; gap: 10px; padding: 4px 10px 18px; color: #fff; font-family: var(--mono); font-weight: 500; font-size: 16px; } -.mark { width: 22px; height: 22px; border-radius: 6px; background: var(--down); display: grid; place-items: center; color: #fff; } +.brand { display: flex; align-items: center; gap: 12px; padding: 4px 10px 18px; color: #fff; } +.brand svg { flex: none; } +.brand .wm { font-family: var(--mono); font-weight: 600; font-size: 16px; letter-spacing: 0.06em; line-height: 1.2; } +.brand .kana { font-family: "Hiragino Sans", "Yu Gothic", "Noto Sans JP", system-ui, sans-serif; font-size: 10px; letter-spacing: 0.3em; color: #8d8e93; margin-top: 2px; } .srv { display: flex; align-items: center; gap: 8px; margin: 0 6px 16px; padding: 10px; border: 1px solid #2c2d32; border-radius: 8px; font-size: 12px; color: #c9c9c3; } .srv strong { color: #fff; font-weight: 500; } .side a.nav { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 0 12px; border-radius: 8px; color: #c9c9c3; text-decoration: none; font-weight: 500; } @@ -206,7 +208,9 @@ dialog::backdrop { background: rgba(22, 23, 26, .55); } /* login */ .loginpage { min-height: 100vh; display: grid; place-items: center; padding: 32px 16px; background: var(--ink); } .loginbox { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: 20px; } -.loginbox .brand { justify-content: center; font-size: 18px; padding: 0; } +.loginbox .brand { justify-content: center; padding: 0; } +.loginbox .brand .wm { font-size: 20px; } +.loginbox .brand .kana { font-size: 11px; } .loginform { background: var(--surface); border-radius: 14px; padding: 28px; display: flex; flex-direction: column; gap: 16px; } .loginform h1 { font-size: 20px; } .loginform input { min-height: 44px; } diff --git a/app.js b/app.js index 77be5cb..c9c1735 100644 --- a/app.js +++ b/app.js @@ -48,9 +48,24 @@ server: '', settings: '', plus: '', - mark: '', }; + // The Kamon logo: a crest ring around the ghost. Same drawing as favicon.svg. + const LOGO = '' + + '' + + ''; + + function logo(size) { + const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + for (const [k, v] of Object.entries({ width: size, height: size, viewBox: '0 0 64 64', 'aria-hidden': 'true' })) s.setAttribute(k, v); + s.innerHTML = LOGO; + return s; + } + + // brand is the logo lockup: mark, wordmark and the katakana reading. + const brand = (size) => h('div', { class: 'brand' }, logo(size), + h('div', null, h('div', { class: 'wm' }, APP), h('div', { class: 'kana', lang: 'ja' }, 'ゴーストワイヤー'))); + // Icons are constant markup from ICONS, never data. function icon(name, size = 18, width = 1.7) { const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); @@ -274,7 +289,7 @@ peerCount = h('span', { class: 'count' }); navLinks = {}; const nav = h('nav', { class: 'side', 'aria-label': 'Main' }, - h('div', { class: 'brand' }, h('span', { class: 'mark' }, icon('mark', 14, 2.4)), APP), + brand(34), srvBox, NAV.map(([href, ic, label]) => (navLinks[href] = h('a', { class: 'nav', href }, icon(ic), label, ic === 'peers' ? peerCount : null))), h('div', { class: 'foot' }, @@ -375,7 +390,7 @@ err, btn, h('p', { class: 'hint' }, 'After 5 failed attempts, sign-in is locked for 15 minutes.')); app.replaceChildren(h('div', { class: 'loginpage' }, h('div', { class: 'loginbox' }, - h('div', { class: 'brand' }, h('span', { class: 'mark' }, icon('mark', 14, 2.4)), APP), + brand(44), form))); pw.focus(); } diff --git a/apple-touch-icon.png b/apple-touch-icon.png new file mode 100644 index 0000000..dacf939 Binary files /dev/null and b/apple-touch-icon.png differ diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..f5387b2 --- /dev/null +++ b/favicon.svg @@ -0,0 +1 @@ +GHOSTWIRE diff --git a/index.html b/index.html index 0b3ee7f..335f9e7 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,8 @@ GHOSTWIRE + + diff --git a/web.go b/web.go index e200e7e..67262a5 100644 --- a/web.go +++ b/web.go @@ -5,19 +5,22 @@ import ( "net/http" ) -// The web UI is three static files built into the binary. It talks only to +// The web UI and its icons are built into the binary. The UI talks only to // /api/v1, the same API the iOS app uses. // -//go:embed index.html app.js app.css +//go:embed index.html app.js app.css favicon.svg apple-touch-icon.png var webFiles embed.FS func webHandler() http.Handler { files := http.FileServerFS(webFiles) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/", "/app.js", "/app.css": + case "/", "/app.js", "/app.css", "/favicon.svg", "/apple-touch-icon.png": w.Header().Set("Cache-Control", "no-cache") files.ServeHTTP(w, r) + case "/favicon.ico": + // Browsers ask for this by default; point them to the SVG. + http.Redirect(w, r, "/favicon.svg", http.StatusMovedPermanently) default: http.NotFound(w, r) }