summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-15 00:10:12 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-15 00:10:12 +0200
commited2e0640b48148b69eca00ad7dd06789506e12ac (patch)
tree85c9c8a43fe91d40ce3e755ef9fd77fc42ca668c /packages
parent0355167e02a710c0e40484592ac794810cde3922 (diff)
downloadmeshbay-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.js88
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css29
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; }