Unread counts, a window margin, blurred dialogs, softer group names
Each rise in an app's unread count while you are elsewhere adds to a tally beside its name, and a dot on its icon in the rail. Looking at the app is the only thing that clears it. The window has no frame of its own, so the shell now keeps a 6px margin in a normal window - the only part of it that is not a web page, and so the only place left to grab. Full screen gives the room back. Dialogs get the app blurred behind them. An app's webview paints above the shell, so it has to be moved aside before a dialog can be seen at all, and once moved there is nothing left to blur - a still taken on the way out is the only way to keep the background there. It runs on a blocking worker: its completion handler is on the main thread, and waiting there deadlocks until the timeout and returns nothing. Settings is much larger, the cog moved to the foot of the nav, group names read as names rather than shouted headings, and "Refresh icons" bumps a version every favicon URL carries, for the ones that cache wrong.
This commit is contained in:
+56
-17
@@ -1,11 +1,13 @@
|
||||
import type { Config, Group, WorkApp } from "../types";
|
||||
import { Back, Cog, Collapse, Forward, Reload } from "./icons";
|
||||
import { Favicon, HEADING, ICON_CHROME } from "./ui";
|
||||
import { Favicon, GROUP_LABEL, ICON_CHROME } from "./ui";
|
||||
|
||||
interface Props {
|
||||
config: Config;
|
||||
activeId: string | null;
|
||||
collapsed: boolean;
|
||||
/** How much has arrived in each app since it was last looked at. */
|
||||
unread: Record<string, number>;
|
||||
onSelect: (id: string) => void;
|
||||
onToggleCollapse: () => void;
|
||||
onOpenSettings: () => void;
|
||||
@@ -27,10 +29,11 @@ const TITLEBAR = 36;
|
||||
export const navWidth = (collapsed: boolean) => (collapsed ? RAIL : PANEL);
|
||||
|
||||
export default function Nav({
|
||||
config, activeId, collapsed,
|
||||
config, activeId, collapsed, unread,
|
||||
onSelect, onToggleCollapse, onOpenSettings, onToggleGroup,
|
||||
onBack, onForward, onReload,
|
||||
}: Props) {
|
||||
const iconV = config.settings.faviconVersion ?? 0;
|
||||
const groups = [...config.groups].sort((a, b) => a.order - b.order);
|
||||
const inGroup = (id: string | null) =>
|
||||
config.apps.filter((a) => a.groupId === id).sort((a, b) => a.order - b.order);
|
||||
@@ -55,12 +58,36 @@ export default function Nav({
|
||||
<button onClick={onReload} disabled={disabled} title="Reload" className={ICON_CHROME}>
|
||||
<Reload />
|
||||
</button>
|
||||
<button onClick={onOpenSettings} title="Settings" className={ICON_CHROME}>
|
||||
<Cog />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
||||
/* Settings lives at the foot of both layouts: it is the thing you reach for
|
||||
least, and putting it there leaves the top for what you use constantly. */
|
||||
const settingsButton = (
|
||||
<button onClick={onOpenSettings} title="Settings" className={ICON_CHROME}>
|
||||
<Cog />
|
||||
</button>
|
||||
);
|
||||
|
||||
/* A count of what arrived while you were elsewhere. Absent, rather than
|
||||
zero, when there is nothing — a row of noughts is just noise. */
|
||||
const badge = (id: string, active: boolean) => {
|
||||
const n = unread[id] ?? 0;
|
||||
if (n === 0) return null;
|
||||
return (
|
||||
<span
|
||||
title={`${n} new since you last looked`}
|
||||
className={
|
||||
"ml-auto min-w-[18px] shrink-0 rounded-full px-1.5 py-0.5 text-center " +
|
||||
"font-mono text-[10px] font-semibold tabular-nums " +
|
||||
(active ? "bg-white/25 text-white dark:bg-slate-900/20 dark:text-slate-900" : "bg-sky-500 text-white")
|
||||
}
|
||||
>
|
||||
{n > 99 ? "99+" : n}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------ icon rail
|
||||
if (collapsed) {
|
||||
const railBtn = (app: WorkApp) => {
|
||||
@@ -79,7 +106,16 @@ export default function Nav({
|
||||
}
|
||||
>
|
||||
{active && <span className="absolute left-0 h-5 w-[3px] rounded-full bg-sky-500" />}
|
||||
<Favicon url={app.url} name={app.name} size={20} />
|
||||
<Favicon url={app.url} name={app.name} size={20} version={iconV} />
|
||||
{(unread[app.id] ?? 0) > 0 && (
|
||||
<span
|
||||
title={`${unread[app.id]} new since you last looked`}
|
||||
className="absolute -right-0.5 -top-0.5 min-w-[15px] rounded-full bg-sky-500
|
||||
px-1 text-center font-mono text-[9px] font-semibold text-white"
|
||||
>
|
||||
{unread[app.id] > 99 ? "99+" : unread[app.id]}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -87,13 +123,10 @@ export default function Nav({
|
||||
return (
|
||||
<aside className={`${shell} items-center`} style={{ width: RAIL }}>
|
||||
<div data-tauri-drag-region style={{ height: TITLEBAR }} className="w-full shrink-0" />
|
||||
{/* Only settings and the expander survive the rail. Back and forward
|
||||
are a two-finger swipe, reload is ⌘R, and five buttons across 72px
|
||||
is clutter standing in for a toolbar nobody asked for. */}
|
||||
{/* Only the expander survives the rail's header. Back and forward are
|
||||
a two-finger swipe and reload is ⌘R, so a toolbar here would be
|
||||
clutter standing in for something nobody asked for. */}
|
||||
<div className="flex w-full flex-col items-center border-b border-slate-200 pb-2 dark:border-slate-800">
|
||||
<button onClick={onOpenSettings} title="Settings" className={ICON_CHROME}>
|
||||
<Cog />
|
||||
</button>
|
||||
<button onClick={onToggleCollapse} title="Expand" className={ICON_CHROME}>
|
||||
<Collapse open={false} />
|
||||
</button>
|
||||
@@ -114,6 +147,9 @@ export default function Nav({
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<div className="flex w-full flex-col items-center border-t border-slate-200 py-2 dark:border-slate-800">
|
||||
{settingsButton}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -132,8 +168,9 @@ export default function Nav({
|
||||
title={app.url}
|
||||
className={`${row} ${app.id === activeId ? active : inactive}`}
|
||||
>
|
||||
<Favicon url={app.url} name={app.name} />
|
||||
<Favicon url={app.url} name={app.name} version={iconV} />
|
||||
<span className="truncate">{app.name}</span>
|
||||
{badge(app.id, app.id === activeId)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
@@ -161,7 +198,7 @@ export default function Nav({
|
||||
<section key={g.id} className="pt-3 first:pt-0">
|
||||
<button
|
||||
onClick={() => onToggleGroup(g)}
|
||||
className={`${HEADING} flex w-full cursor-pointer items-center gap-1 px-2 pb-1
|
||||
className={`${GROUP_LABEL} flex w-full cursor-pointer items-center gap-1 px-2 pb-1
|
||||
hover:text-slate-700 dark:hover:text-slate-200`}
|
||||
>
|
||||
<svg
|
||||
@@ -172,9 +209,7 @@ export default function Nav({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 6l6 6-6 6" />
|
||||
</svg>
|
||||
<span className="truncate">{g.name}</span>
|
||||
<span className="ml-auto font-mono text-[10px] tracking-normal opacity-60">
|
||||
{apps.length}
|
||||
</span>
|
||||
<span className="ml-auto font-mono text-[10px] opacity-60">{apps.length}</span>
|
||||
</button>
|
||||
{!g.collapsed && <ul className="space-y-0.5">{apps.map(appRow)}</ul>}
|
||||
</section>
|
||||
@@ -187,6 +222,10 @@ export default function Nav({
|
||||
</p>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<footer className="flex items-center gap-1 border-t border-slate-200 px-1.5 py-2 dark:border-slate-800">
|
||||
{settingsButton}
|
||||
</footer>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user