Quieten the settings lists; gather the nav buttons onto one bar

Three things, all about how much line-work the eye has to read past.

A group's app count is gone from the nav and from its row in Settings.
It was decoration: the apps are right there to be counted, and in the
nav it sat where the eye lands looking for a name.

The Settings lists framed every row and then framed all three fields
inside it — four outlines a row, forty down the Apps list, and in light
mode they carried nearly the same weight as the text. Each list is now
framed once with hairline rules between rows, and the fields inside draw
no border until you hover or focus one. Reads as a table rather than a
stack of boxes.

Back, forward, reload and collapse now share a single tinted bar instead
of floating loose in the header, and the one under the pointer lifts out
of it. They are one instrument and now look like one.

The focus tell on a bare field is the border alone. A filled background
would have been the same white — slate-900 in dark — as the panel behind
it, which says nothing.
This commit is contained in:
2026-09-03 22:27:12 +02:00
parent 9aac28c0cf
commit 2065d82d27
3 changed files with 55 additions and 36 deletions
+15 -14
View File
@@ -1,7 +1,7 @@
import type { Config, Download, Group, WorkApp } from "../types";
import { Back, Cog, Collapse, Forward, Reload } from "./icons";
import Downloads from "./Downloads";
import { Favicon, GROUP_LABEL, ICON_CHROME } from "./ui";
import { BUTTON_BAR, Favicon, GROUP_LABEL, ICON_CHROME, ICON_ON_BAR } from "./ui";
interface Props {
config: Config;
@@ -50,13 +50,13 @@ export default function Nav({
you want gone. */
const controls = (
<>
<button onClick={onBack} disabled={disabled} title="Back (or swipe left)" className={ICON_CHROME}>
<button onClick={onBack} disabled={disabled} title="Back (or swipe left)" className={ICON_ON_BAR}>
<Back />
</button>
<button onClick={onForward} disabled={disabled} title="Forward (or swipe right)" className={ICON_CHROME}>
<button onClick={onForward} disabled={disabled} title="Forward (or swipe right)" className={ICON_ON_BAR}>
<Forward />
</button>
<button onClick={onReload} disabled={disabled} title="Reload" className={ICON_CHROME}>
<button onClick={onReload} disabled={disabled} title="Reload" className={ICON_ON_BAR}>
<Reload />
</button>
</>
@@ -205,16 +205,18 @@ export default function Nav({
<div data-tauri-drag-region className="shrink-0" style={{ height: chrome }} />
<header
data-tauri-drag-region
className="flex items-center gap-0.5 border-b border-slate-200 px-1.5 py-2.5 dark:border-slate-800"
className="border-b border-slate-200 px-2 py-2 dark:border-slate-800"
>
{controls}
<button
onClick={onToggleCollapse}
title="Collapse"
className={`${ICON_CHROME} ml-auto`}
>
<Collapse open />
</button>
<div className={BUTTON_BAR}>
{controls}
<button
onClick={onToggleCollapse}
title="Collapse"
className={`${ICON_ON_BAR} ml-auto`}
>
<Collapse open />
</button>
</div>
</header>
<nav data-tauri-drag-region className="min-h-0 flex-1 overflow-y-auto px-2 py-3">
@@ -237,7 +239,6 @@ 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] opacity-60">{apps.length}</span>
</button>
{!g.collapsed && <ul data-tauri-drag-region className="space-y-0.5">{apps.map(appRow)}</ul>}
</section>
+15 -22
View File
@@ -16,7 +16,10 @@ import {
HELP,
ICON_CHROME,
INPUT,
INPUT_BARE,
LABEL,
ROW,
ROW_LIST,
SUBPANEL,
SectionHeading,
Segmented,
@@ -113,29 +116,26 @@ export default function Settings({
{/* ----------------------------------------------------- apps */}
<section className="space-y-2">
<SectionHeading>Apps</SectionHeading>
<div className="space-y-1.5">
{config.apps.length === 0 && <p className={HELP}>Nothing yet. Add the first one below.</p>}
{config.apps.length === 0 && <p className={HELP}>Nothing yet. Add the first one below.</p>}
<div className={ROW_LIST}>
{orderedApps.map((app) => (
<div
key={app.id}
className="flex items-center gap-2 rounded-lg border border-slate-200 px-2 py-1.5 dark:border-slate-800"
>
<div key={app.id} className={ROW}>
<Favicon icon={app.icon} name={app.name} />
<input
value={app.name}
onChange={(e) => patch(app, { name: e.target.value })}
className={`${INPUT} h-[26px] w-full flex-1`}
className={`${INPUT_BARE} h-[26px] w-full flex-1`}
/>
<input
value={app.url}
onChange={(e) => patch(app, { url: e.target.value })}
title="Changing this rebuilds the app's view"
className={`${INPUT} h-[26px] w-full flex-[1.4] font-mono text-[11px]`}
className={`${INPUT_BARE} h-[26px] w-full flex-[1.4] font-mono text-[11px]`}
/>
<select
value={app.groupId ?? ""}
onChange={(e) => patch(app, { groupId: e.target.value || null })}
className={`${INPUT} h-[26px] w-[110px] shrink-0 cursor-pointer`}
className={`${INPUT_BARE} h-[26px] w-[110px] shrink-0 cursor-pointer`}
>
<option value="">No group</option>
{groups.map((g) => (
@@ -186,19 +186,15 @@ export default function Settings({
{/* ---------------------------------------------------- groups */}
<section className="space-y-2">
<SectionHeading>Groups</SectionHeading>
<div className="space-y-1.5">
{groups.length === 0 && <p className={HELP}>No groups yet.</p>}
{groups.length === 0 && <p className={HELP}>No groups yet.</p>}
<div className={ROW_LIST}>
{groups.map((g: Group) => (
<div key={g.id}
className="flex items-center gap-2 rounded-lg border border-slate-200 px-2 py-1.5 dark:border-slate-800">
<div key={g.id} className={ROW}>
<input
value={g.name}
onChange={(e) => run(() => api.updateGroup({ ...g, name: e.target.value }))}
className={`${INPUT} h-[26px] w-full flex-1`}
className={`${INPUT_BARE} h-[26px] w-full flex-1`}
/>
<span className="shrink-0 font-mono text-[10px] text-slate-400">
{config.apps.filter((a) => a.groupId === g.id).length} apps
</span>
<button
onClick={() => run(() => api.deleteGroup(g.id))}
title={`Delete ${g.name} — its apps stay, ungrouped`}
@@ -239,12 +235,9 @@ export default function Settings({
{/* ------------------------------------------------------ zoom */}
<section className="space-y-2">
<SectionHeading>Zoom</SectionHeading>
<div className="space-y-1.5">
<div className={ROW_LIST}>
{orderedApps.map((app) => (
<div
key={app.id}
className="flex items-center gap-3 rounded-lg border border-slate-200 px-2 py-1.5 dark:border-slate-800"
>
<div key={app.id} className={`${ROW} gap-3`}>
<Favicon icon={app.icon} name={app.name} />
<span className="w-28 shrink-0 truncate text-[12px]">{app.name}</span>
<input
+25
View File
@@ -29,6 +29,23 @@ export const INPUT =
"placeholder:text-slate-400 dark:border-slate-800 dark:bg-slate-800 dark:placeholder:text-slate-500";
export const SUBPANEL = "rounded-lg bg-slate-50 p-3 dark:bg-slate-800/50";
/* A list of editable rows, framed once. Giving every row a border and every
field inside it another one stacks four outlines per row, and ten rows of
that is a thicket you have to read past to find the thing you came for. */
export const ROW_LIST =
"divide-y divide-slate-200 overflow-hidden rounded-lg border border-slate-200 " +
"dark:divide-slate-800 dark:border-slate-800";
export const ROW = "flex items-center gap-2 px-2 py-1.5";
/** A field inside a ROW: the row frames it, so it draws no outline until used.
The focus tell is the border alone — a filled background would be the same
white (or slate-900) as the panel behind it, which says nothing. */
export const INPUT_BARE =
`${CONTROL_H} min-w-0 rounded-md border border-transparent bg-transparent px-2 text-[12px] outline-none ` +
"hover:border-slate-200 focus:border-sky-500 " +
"dark:hover:border-slate-700 dark:focus:border-sky-500 " +
"placeholder:text-slate-400 dark:placeholder:text-slate-500";
const BTN_BASE =
`inline-flex ${CONTROL_H} items-center justify-center rounded-lg text-[12px] ` +
"disabled:cursor-not-allowed cursor-pointer";
@@ -54,6 +71,14 @@ export const BTN_QUIET =
"text-[11px] text-slate-500 underline underline-offset-2 cursor-pointer " +
"hover:text-sky-600 dark:text-slate-400 dark:hover:text-sky-400";
/* A row of icon buttons sharing one tinted surface, so they read as a single
instrument rather than three loose targets. The hovered one lifts out of it. */
export const BUTTON_BAR =
"flex items-center gap-0.5 rounded-lg bg-slate-100/80 p-0.5 dark:bg-slate-800/60";
export const ICON_ON_BAR =
`${ICON_BTN} text-slate-500 hover:bg-white hover:text-slate-900 hover:shadow-sm ` +
"dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-white";
/** Icon-button skin used throughout the chrome. */
export const ICON_CHROME =
`${ICON_BTN} text-slate-500 hover:bg-slate-100 hover:text-slate-900 ` +