diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-15 00:10:12 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-15 00:10:12 +0200 |
| commit | ed2e0640b48148b69eca00ad7dd06789506e12ac (patch) | |
| tree | 85c9c8a43fe91d40ce3e755ef9fd77fc42ca668c /packages | |
| parent | 0355167e02a710c0e40484592ac794810cde3922 (diff) | |
| download | meshbay-ed2e0640b48148b69eca00ad7dd06789506e12ac.tar.gz | |
One stroked icon set, matching the Administration shield
The site drew its icons with colour emoji, which means a different
drawing on every operating system and never the same line weight twice —
except the Administration entry, whose shield is a text-default glyph and
so rendered as a thin outline. That one was the odd one out and also the
one that looked right.
Fifteen inline SVG paths now replace it: menu, bell, shield, globe, gear,
sun, moon, power, lock, envelope, door, clip, check, chevron, close. They
are stroked in currentColor and sized in em, so an icon takes the colour
and size of the text beside it and needs no rule of its own — the caret
in the dark nav bar and the gear in a light dropdown are the same file.
The explorer keeps its emoji. There the icon says what kind of file this
is and the colour is doing real work; a wall of identical grey outlines
would be a worse file list.
One bug fell out of the change: .nav-notif was painted with --text, the
page's text colour, on a nav bar that is nearly black. The emoji bell
carried its own colours and showed anyway; a stroked one was invisible
until it was given --nav-text.
Checked by rendering the real stylesheet in headless Chrome, light and
dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 88 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/style.css | 29 |
2 files changed, 94 insertions, 23 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 3197c31..a5b74f0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -223,6 +223,55 @@ function navigate(path) { const AuthContext = createContext(null); function useAuth() { return useContext(AuthContext); } +// ── Icons ──────────────────────────────────────────────────────────────────── +// +// One stroked set, drawn in currentColor and sized in em, so an icon takes the +// weight and colour of the text beside it. The Administration entry was already +// an outline shield while the rest of the site was colour emoji — a different +// drawing on every operating system, and never the same line weight twice. +// +// The explorer keeps its emoji on purpose. There the icon says what kind of file +// this is, and the colour is doing real work; a wall of identical grey outlines +// would be a worse file list. + +const ICON_PATHS = { + menu: ['M4 7h16M4 12h16M4 17h16'], + bell: ['M18 9a6 6 0 1 0-12 0c0 6-2.5 7.5-2.5 7.5h17S18 15 18 9', + 'M10.3 20a2 2 0 0 0 3.4 0'], + shield: ['M12 3l7.5 3v5.2c0 4.6-3.1 8.6-7.5 10.3-4.4-1.7-7.5-5.7-7.5-10.3V6z'], + globe: ['M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18', + 'M3.4 9.2h17.2M3.4 14.8h17.2', + 'M12 3c-2.6 2.4-4 5.6-4 9s1.4 6.6 4 9c2.6-2.4 4-5.6 4-9s-1.4-6.6-4-9'], + gear: ['M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6', + 'M19.2 14.4a1.7 1.7 0 0 0 .3 1.9 2 2 0 1 1-2.8 2.8 1.7 1.7 0 0 0-2.9 1.2 2 2 0 0 1-4 0 1.7 1.7 0 0 0-2.9-1.2 2 2 0 1 1-2.8-2.8 1.7 1.7 0 0 0-1.2-2.9 2 2 0 0 1 0-4 1.7 1.7 0 0 0 1.2-2.9 2 2 0 1 1 2.8-2.8 1.7 1.7 0 0 0 2.9-1.2 2 2 0 0 1 4 0 1.7 1.7 0 0 0 2.9 1.2 2 2 0 1 1 2.8 2.8 1.7 1.7 0 0 0 1.2 2.9 2 2 0 0 1 0 4 1.7 1.7 0 0 0-1.5 1.1z'], + sun: ['M12 8.2a3.8 3.8 0 1 0 0 7.6 3.8 3.8 0 0 0 0-7.6', + 'M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.2 5.2l1.4 1.4M17.4 17.4l1.4 1.4M18.8 5.2l-1.4 1.4M6.6 17.4l-1.4 1.4'], + moon: ['M20.8 13.4A8.6 8.6 0 1 1 10.6 3.2a6.9 6.9 0 0 0 10.2 10.2z'], + power: ['M12 3.2v8.4', 'M6.9 6.6a7.6 7.6 0 1 0 10.2 0'], + lock: ['M5.5 11h13a1 1 0 0 1 1 1v7.5a1 1 0 0 1-1 1h-13a1 1 0 0 1-1-1V12a1 1 0 0 1 1-1z', + 'M8 11V7.4a4 4 0 0 1 8 0V11'], + envelope: ['M4 5.5h16a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-11a1 1 0 0 1 1-1z', + 'M3.4 6.6L12 13.4l8.6-6.8'], + door: ['M13.5 3.5H6a1 1 0 0 0-1 1v15a1 1 0 0 0 1 1h7.5', + 'M10.5 12H21', 'M17.8 8.8L21 12l-3.2 3.2'], + clip: ['M20.5 11.8l-8.4 8.4a5.4 5.4 0 0 1-7.6-7.6l8.8-8.8a3.6 3.6 0 0 1 5.1 5.1l-8.8 8.8a1.8 1.8 0 0 1-2.5-2.5l8.1-8.1'], + check: ['M4.5 12.5l5 5 10-11'], + chevron: ['M6 9.5l6 6 6-6'], + close: ['M6 6l12 12M18 6L6 18'], +}; + +function Icon({ name, cls = '' }) { + const paths = ICON_PATHS[name]; + if (!paths) return null; + return html` + <svg class="icon ${cls}" viewBox="0 0 24 24" aria-hidden="true" focusable="false" + fill="none" stroke="currentColor" stroke-width="1.6" + stroke-linecap="round" stroke-linejoin="round"> + ${paths.map((d, i) => html`<path key=${i} d=${d} />`)} + </svg> + `; +} + // ── User Menu ──────────────────────────────────────────────────────────────── function UserMenu({ user, theme, onThemeChange, onLogout }) { @@ -246,7 +295,7 @@ function UserMenu({ user, theme, onThemeChange, onLogout }) { <button class="user-menu-trigger" onClick=${() => setOpen(o => !o)}> <span class="user-menu-avatar">${user.username[0].toUpperCase()}</span> <span class="user-menu-name">${user.username}</span> - <span class="user-menu-caret">${open ? '▴' : '▾'}</span> + <${Icon} name="chevron" cls="user-menu-caret ${open ? 'flip' : ''}" /> </button> ${open && html` <div class="user-menu-dropdown"> @@ -259,28 +308,29 @@ function UserMenu({ user, theme, onThemeChange, onLogout }) { </div> <div class="user-menu-divider"></div> <button class="user-menu-item" onClick=${(e) => { e.stopPropagation(); setLangOpen(o => !o); }}> - <span class="umi-icon">${'\u{1F310}'}</span> ${t('usermenu.language')} - <span class="umi-arrow">${langOpen ? '▴' : '▾'}</span> + <${Icon} name="globe" cls="umi-icon" /> ${t('usermenu.language')} + <${Icon} name="chevron" cls="umi-arrow ${langOpen ? 'flip' : ''}" /> </button> ${langOpen && LOCALES.map(l => html` <button key=${l.code} class="user-menu-item user-menu-sub" onClick=${() => { setLocale(l.code); window.location.reload(); }}> <span class="umi-icon">${l.flag}</span> ${l.name} - ${getLocale() === l.code && html`<span class="umi-check">${'✓'}</span>`} + ${getLocale() === l.code + && html`<${Icon} name="check" cls="umi-check" />`} </button> `)} <button class="user-menu-item" onClick=${() => { setOpen(false); navigate('/settings'); }}> - <span class="umi-icon">${'⚙'}</span> ${t('usermenu.settings')} + <${Icon} name="gear" cls="umi-icon" /> ${t('usermenu.settings')} </button> <button class="user-menu-item" onClick=${() => { onThemeChange(resolved === 'dark' ? 'light' : 'dark'); }}> - <span class="umi-icon">${resolved === 'dark' ? '☀' : '☾'}</span> + <${Icon} name=${resolved === 'dark' ? 'sun' : 'moon'} cls="umi-icon" /> ${' '}${resolved === 'dark' ? t('usermenu.theme_light') : t('usermenu.theme_dark')} </button> <div class="user-menu-divider"></div> <button class="user-menu-item user-menu-logout" onClick=${onLogout}> - <span class="umi-icon">${'⏻'}</span> ${t('usermenu.logout')} + <${Icon} name="power" cls="umi-icon" /> ${t('usermenu.logout')} </button> </div> `} @@ -296,14 +346,15 @@ function Nav({ user, theme, onThemeChange, onLogout, onMenuToggle, unreadCount } <div class="nav-left"> ${user && html` <button class="nav-hamburger" onClick=${onMenuToggle} - aria-label="${t('nav.toggle_menu')}">≡</button> + aria-label="${t('nav.toggle_menu')}"><${Icon} name="menu" /></button> `} <a class="nav-brand" href="#/">MeshBay</a> </div> <div class="nav-right"> ${user && html` <a class="nav-notif" href="#/" title=${t('notif.title')}> - ${'\u{1F514}'}${unreadCount > 0 && html`<span class="notif-badge">${unreadCount}</span>`} + <${Icon} name="bell" />${unreadCount > 0 + && html`<span class="notif-badge">${unreadCount}</span>`} </a> `} ${user ? html` @@ -327,7 +378,7 @@ function Sidebar({ groups, route, menuOpen, role }) { <div class="sidebar-section"> <div class="sidebar-heading">${t('sidebar.admin')}</div> <a class="sidebar-item ${route === '/admin' ? 'active' : ''}" - href="#/admin">${'\u{1F6E1}'} ${t('admin.title')}</a> + href="#/admin"><${Icon} name="shield" /> ${t('admin.title')}</a> </div> `} <div class="sidebar-section"> @@ -715,13 +766,13 @@ function CreateGroupPage({ token, onCreated }) { <div class="option-cards"> <button type="button" class="option-card ${visibility === 'private' ? 'selected' : ''}" onClick=${() => setVisibility('private')}> - <span class="option-icon">🔒</span> + <${Icon} name="lock" cls="option-icon" /> <span class="option-title">${t('create_group.private')}</span> <span class="option-desc">Only invited members can see and access files</span> </button> <button type="button" class="option-card ${visibility === 'public' ? 'selected' : ''}" onClick=${() => setVisibility('public')}> - <span class="option-icon">🌐</span> + <${Icon} name="globe" cls="option-icon" /> <span class="option-title">${t('create_group.public')}</span> <span class="option-desc">Anyone can discover and browse this group</span> </button> @@ -733,13 +784,13 @@ function CreateGroupPage({ token, onCreated }) { <div class="option-cards"> <button type="button" class="option-card ${joinPolicy === 'invite' ? 'selected' : ''}" onClick=${() => setJoinPolicy('invite')}> - <span class="option-icon">✉</span> + <${Icon} name="envelope" cls="option-icon" /> <span class="option-title">${t('create_group.invite')}</span> <span class="option-desc">Members must be invited by an admin</span> </button> <button type="button" class="option-card ${joinPolicy === 'open' ? 'selected' : ''}" onClick=${() => setJoinPolicy('open')}> - <span class="option-icon">🚪</span> + <${Icon} name="door" cls="option-icon" /> <span class="option-title">${t('create_group.open')}</span> <span class="option-desc">Anyone can join without approval</span> </button> @@ -1481,7 +1532,8 @@ function FilePreview({ entry, transportRef, gekRef, onClose }) { }}> <div class="video-top-bar"> <span class="video-title">${entry.name} (${formatSize(entry.size)})</span> - <button class="video-close" onClick=${onClose} title="${t('video.close')}">✕</button> + <button class="video-close" onClick=${onClose} title="${t('video.close')}"> + <${Icon} name="close" /></button> </div> ${phase === 'loading' && html` <div class="video-loading"> @@ -1855,7 +1907,8 @@ function ChatPanel({ transportRef, username, entries, gekRef, onRefreshIndex, on </div> <div class="chat-input-row"> <label class="chat-attach" title="${t('chat.attach')}"> - ${attaching ? html`<span class="spinner"></span>` : '\u{1F4CE}'} + ${attaching ? html`<span class="spinner"></span>` + : html`<${Icon} name="clip" />`} <input type="file" style="display:none" onChange=${attachFile} disabled=${attaching} /> </label> <textarea class="chat-input" rows="1" @@ -2036,7 +2089,8 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose }) { }}> <div class="video-top-bar"> <span class="video-title">${entry.name} (${formatSize(entry.size)})</span> - <button class="video-close" onClick=${onClose} title="${t('video.close')}">✕</button> + <button class="video-close" onClick=${onClose} title="${t('video.close')}"> + <${Icon} name="close" /></button> </div> ${phase === 'loading' && html` diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index da29027..2d1eb65 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -57,6 +57,19 @@ body { a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } +/* ── Icons ────────────────────────────────────────────────────────────────── */ + +/* Sized in em and stroked in currentColor, so an icon inherits the size and + colour of whatever it sits next to and needs no rule of its own. Only the + places that want a fixed size say so below. */ +.icon { + width: 1em; + height: 1em; + vertical-align: -0.14em; + flex-shrink: 0; +} +.icon.flip { transform: rotate(180deg); } + /* ── Navigation ───────────────────────────────────────────────────────────── */ .nav { @@ -166,9 +179,10 @@ a:hover { text-decoration: underline; } .user-menu-item:hover { background: var(--bg-raised); } .user-menu-logout { color: var(--error); } -.umi-icon { width: 18px; text-align: center; font-size: 0.95em; flex-shrink: 0; } -.umi-arrow { margin-left: auto; font-size: 0.7em; color: var(--text-dim); } -.umi-check { margin-left: auto; color: var(--accent); font-size: 0.85em; } +.umi-icon { width: 17px; height: 17px; flex-shrink: 0; color: var(--text-secondary); } +.umi-arrow { margin-left: auto; width: 13px; height: 13px; color: var(--text-dim); } +.umi-check { margin-left: auto; width: 14px; height: 14px; color: var(--accent); } +.user-menu-caret { width: 13px; height: 13px; color: var(--nav-text); opacity: 0.8; } .user-menu-sub { padding-left: 32px; } .nav-btn { @@ -1250,9 +1264,11 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } .nav-notif { position: relative; - color: var(--text); + /* --text is the page's text colour and the nav is dark: the emoji bell was + coloured so it showed anyway, a stroked one would have been invisible. */ + color: var(--nav-text); text-decoration: none; - font-size: 1.1rem; + font-size: 1.25rem; margin-right: 4px; } .notif-badge { @@ -1360,12 +1376,13 @@ button:disabled { opacity: 0.5; cursor: not-allowed; } transition: border-color 0.12s, background 0.12s; } .option-card:hover { border-color: var(--accent); background: var(--bg-base); } +.option-card.selected .option-icon { color: var(--accent); } .option-card.selected { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--bg-base)); } -.option-icon { font-size: 1.5em; margin-bottom: 2px; } +.option-icon { width: 26px; height: 26px; margin-bottom: 2px; color: var(--text-secondary); } .option-title { font-weight: 600; font-size: 0.9em; color: var(--text); } .option-desc { font-size: 0.75em; color: var(--text-dim); line-height: 1.3; } |