Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1341,3 +1341,34 @@ after each step and stuttering the auto-scroll toward the target column; on drop
untouched** — `flex-1` fits every column there, so the row never overflows and
snap is inert. Verified in WebKit at 390 px: a partial nudge settled exactly on
a column edge (0 px delta), zero page overflow.

### PROG-81 — mobile audit: filters collapse + 44px touch targets

An audit of the rendered phone experience (WebKit at 360/390/430/768, touch
emulation, per the `mobile-first-audit` skill) found the app broadly solid —
correct `viewport-fit=cover` meta, zoom enabled, 19 px body, `pb-24` clearing
the bottom tab bar, no page-level horizontal overflow — but three things worth
fixing:

1. **Board filters buried the board.** The six filter dropdowns + New-issue +
two toggles filled the entire first viewport; the cards (the point of the
page) only appeared after scrolling past all of it. Fixed by collapsing the
filter row behind a **"Filters"** disclosure on phones (`< sm`), collapsed by
default with an active-count badge; at `sm`+ the row stays inline and desktop
is byte-identical. *Hard call:* a phone user now taps once to filter, but
gets three cards above the fold instead of zero — mobile is primary, so the
board wins the default view.
2. **Sub-44 px touch targets.** The header New button (~34), avatar (32) and its
dropdown rows (~31), the board's New-issue and Show-backlog/sub-issues
buttons (~31), and the issue page's field-edit links — Move…/Change…/Edit…/
Copy… — at ~19 px (on a phone the *only* way to fire those, since the
keyboard shortcuts don't exist there). All bumped to a **≥44 px** row on
mobile via `min-h-11` (and a `flex` row for the issue links so they keep
their own line), reverting to the compact sizing at `sm`+.

*Deliberately deferred* (noted, not fixed, to keep the PR focused): tablet-range
(640–767 px) filter `<select>`s render at 14.4 px and can trigger iOS
zoom-on-focus — the global 16 px rule stops at 639 px; the bottom-tab-bar's
translucency lets card content bleed through faintly on scroll. Both are low
severity. Verified before/after in WebKit: board mobile small-targets 6→1, issue
12→5, desktop unchanged (Filters toggle hidden, six selects inline).
6 changes: 5 additions & 1 deletion docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,11 @@ canonical key — entirely client-side from the loaded workspace (D22).
Agenda filters sort the same way. The current filter selection is also
mirrored to `localStorage` (`progress:board-filters`) and re-applied when the
board is reopened with a bare URL, so a choice sticks across navigation;
"Clear filters" clears the memory too (PROG-58). Drag-and-drop reorders cards
"Clear filters" clears the memory too (PROG-58). On phones (`< sm`) the whole
filter row collapses behind a **"Filters"** disclosure — collapsed by default,
with a badge counting the active filters/toggles — so the board itself sits
above the fold instead of a screenful of dropdowns; at `sm` and up the row is
always inline (PROG-81). Drag-and-drop reorders cards
vertically
within a column (a manual work order) and moves them between columns to set
status; both persist as one optimistic write via the card's `rank` (D44).
Expand Down
12 changes: 6 additions & 6 deletions src/client/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ export default function Header() {
<div className="relative">
<button
onClick={() => setMenuOpen((o) => !o)}
className="rounded bg-adobe px-3 py-1 text-sm text-white hover:bg-adobe-deep"
className="inline-flex min-h-11 items-center rounded bg-adobe px-3 py-1 text-sm text-white hover:bg-adobe-deep sm:min-h-0"
>
New <span className="text-white/70">▾</span>
New <span className="ml-1 text-white/70">▾</span>
</button>
{menuOpen && (
<>
Expand All @@ -82,7 +82,7 @@ export default function Header() {
setMenuOpen(false);
item.run();
}}
className="block w-full px-3 py-1.5 text-left text-sm text-ink-soft hover:bg-line"
className="flex min-h-11 w-full items-center px-3 py-1.5 text-left text-sm text-ink-soft hover:bg-line sm:min-h-0"
>
New {item.label.toLowerCase()}
</button>
Expand All @@ -98,7 +98,7 @@ export default function Header() {
<button
onClick={() => setAcctOpen((o) => !o)}
title={me.email}
className="flex h-8 w-8 items-center justify-center rounded-full bg-adobe-wash/60 text-sm font-medium text-adobe-deep hover:bg-adobe-wash"
className="flex h-11 w-11 items-center justify-center rounded-full bg-adobe-wash/60 text-sm font-medium text-adobe-deep hover:bg-adobe-wash sm:h-8 sm:w-8"
>
{me.name.slice(0, 1).toUpperCase()}
</button>
Expand All @@ -116,14 +116,14 @@ export default function Header() {
<Link
href="/admin"
onClick={() => setAcctOpen(false)}
className="block w-full px-3 py-1.5 text-left text-sm text-ink-soft hover:bg-line"
className="flex min-h-11 w-full items-center px-3 py-1.5 text-left text-sm text-ink-soft hover:bg-line sm:min-h-0"
>
Admin
</Link>
)}
<button
onClick={signOut}
className="block w-full px-3 py-1.5 text-left text-sm text-ink-soft hover:bg-line"
className="flex min-h-11 w-full items-center px-3 py-1.5 text-left text-sm text-ink-soft hover:bg-line sm:min-h-0"
>
Sign out
</button>
Expand Down
41 changes: 36 additions & 5 deletions src/client/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ export default function Home({ workspace }: { workspace: WorkspacePayload }) {

const [columns, setColumns] = useState<ColumnMap>(sourceColumns);
const [activeId, setActiveId] = useState<string | null>(null);
// Mobile only: the filter dropdowns collapse behind a "Filters" disclosure so
// the board itself sits above the fold instead of a screenful of chrome
// (PROG-81). Desktop ignores this — the block is always `sm:flex`.
const [filtersOpen, setFiltersOpen] = useState(false);
// Mirror of activeId for effects/handlers that must read it without depending
// on it (a ref updates synchronously and doesn't re-trigger effects).
const activeIdRef = useRef<string | null>(null);
Expand Down Expand Up @@ -367,6 +371,12 @@ export default function Home({ workspace }: { workspace: WorkspacePayload }) {
);
const shownCount = visibleColumns.reduce((n, s) => n + columns[s].length, 0);
const filtersActive = FILTER_KEYS.some((k) => filters[k]);
// Badge on the mobile "Filters" toggle: how many filters/toggles are narrowing
// the board right now, so a collapsed panel still signals it's doing something.
const activeFilterCount =
FILTER_KEYS.filter((k) => filters[k]).length +
(filters.backlog ? 1 : 0) +
(filters.subissues ? 1 : 0);

return (
<>
Expand All @@ -379,13 +389,34 @@ export default function Home({ workspace }: { workspace: WorkspacePayload }) {
</p>
<button
onClick={() => openCreateIssue()}
className="ml-auto rounded bg-adobe px-3 py-1 text-sm text-white hover:bg-adobe-deep"
className="ml-auto inline-flex min-h-11 items-center rounded bg-adobe px-3 py-1 text-sm text-white hover:bg-adobe-deep sm:min-h-0"
>
New issue <span className="text-white/70">(C)</span>
New issue <span className="ml-1 text-white/70">(C)</span>
</button>
</header>

<div className="mt-4 flex flex-wrap items-center gap-2 text-sm">
{/* Mobile: a "Filters" disclosure keeps the six dropdowns + two toggles
out of the default view so the board is above the fold (PROG-81). The
badge shows how many are active while collapsed. Hidden at sm+, where
the filter row is always inline. */}
<button
type="button"
onClick={() => setFiltersOpen((o) => !o)}
aria-expanded={filtersOpen}
className="mt-4 flex min-h-11 w-full items-center gap-2 rounded border border-line bg-card px-3 text-sm text-ink-soft hover:border-ink-faint sm:hidden"
>
<span className="font-medium">Filters</span>
{activeFilterCount > 0 && (
<span className="rounded-full bg-adobe px-1.5 py-0.5 text-xs font-medium text-white">
{activeFilterCount}
</span>
)}
<span className={`ml-auto transition-transform ${filtersOpen ? "rotate-180" : ""}`}>▾</span>
</button>

<div
className={`${filtersOpen ? "flex" : "hidden"} mt-3 flex-wrap items-center gap-2 text-sm sm:mt-4 sm:flex`}
>
{/* Archived containers stay out of the dropdowns (D26); their issues
still render, so nothing silently disappears from the board. */}
<FilterSelect
Expand Down Expand Up @@ -454,7 +485,7 @@ export default function Home({ workspace }: { workspace: WorkspacePayload }) {
/>
<button
onClick={() => setParam("backlog", filters.backlog ? null : "1")}
className={`rounded border px-2 py-1 text-xs ${
className={`inline-flex min-h-11 items-center rounded border px-3 py-1 text-xs sm:min-h-0 sm:px-2 ${
filters.backlog
? "border-ink-faint bg-line text-ink-soft"
: "border-line bg-card text-ink-faint hover:border-ink-faint"
Expand All @@ -464,7 +495,7 @@ export default function Home({ workspace }: { workspace: WorkspacePayload }) {
</button>
<button
onClick={() => setParam("subissues", filters.subissues ? null : "1")}
className={`rounded border px-2 py-1 text-xs ${
className={`inline-flex min-h-11 items-center rounded border px-3 py-1 text-xs sm:min-h-0 sm:px-2 ${
filters.subissues
? "border-ink-faint bg-line text-ink-soft"
: "border-line bg-card text-ink-faint hover:border-ink-faint"
Expand Down
26 changes: 17 additions & 9 deletions src/client/pages/IssuePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ import { toastAction } from "../toast";
const fmtTime = (iso: string) =>
new Date(iso).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });

// The field-edit triggers in the aside (Move… / Change… / Edit… / Copy…). They
// carry keyboard shortcuts on desktop, but on a phone tapping the link is the
// ONLY way to fire them — so give each a 44px-tall touch row on mobile while
// keeping the compact one-line-each layout on desktop (PROG-81). `flex` is
// block-level, so each still sits on its own line as `block` did.
const FIELD_ACTION_CLS =
"flex min-h-11 items-center text-xs text-adobe hover:underline sm:block sm:min-h-0";

export default function IssuePage({
workspace,
keyParam,
Expand Down Expand Up @@ -196,9 +204,9 @@ export default function IssuePage({
</p>
<button
onClick={() => openPalette({ kind: "move", issueId: issue.id })}
className="mt-0.5 text-xs text-adobe hover:underline"
className={`mt-0.5 ${FIELD_ACTION_CLS}`}
>
Move… <span className="text-ink-faint">(M)</span>
Move… <span className="ml-1 text-ink-faint">(M)</span>
</button>
</Field>
<Field label="Arc">
Expand All @@ -211,9 +219,9 @@ export default function IssuePage({
)}
<button
onClick={() => openPalette({ kind: "arc", issueId: issue.id })}
className="mt-0.5 block text-xs text-adobe hover:underline"
className={`mt-0.5 ${FIELD_ACTION_CLS}`}
>
Change… <span className="text-ink-faint">(A)</span>
Change… <span className="ml-1 text-ink-faint">(A)</span>
</button>
</Field>
<Field label="Tags">
Expand All @@ -234,21 +242,21 @@ export default function IssuePage({
)}
<button
onClick={() => openPalette({ kind: "tag", issueId: issue.id })}
className="mt-0.5 block text-xs text-adobe hover:underline"
className={`mt-0.5 ${FIELD_ACTION_CLS}`}
>
Edit… <span className="text-ink-faint">(T)</span>
Edit… <span className="ml-1 text-ink-faint">(T)</span>
</button>
</Field>
<Field label="Work on this">
<button
onClick={() => void copyBundleAsPrompt(issueKeyOf(workspace, issue))}
className="block text-xs text-adobe hover:underline"
className={FIELD_ACTION_CLS}
>
Copy as prompt <span className="text-ink-faint">(W)</span>
Copy as prompt <span className="ml-1 text-ink-faint">(W)</span>
</button>
<button
onClick={() => copyWorkCommand(issueKeyOf(workspace, issue))}
className="mt-0.5 block text-xs text-adobe hover:underline"
className={FIELD_ACTION_CLS}
>
Copy CLI command
</button>
Expand Down