From b6d87294386d2838885d4b4569f6bcfd6851179b Mon Sep 17 00:00:00 2001 From: bkennedy Date: Thu, 16 Jul 2026 03:37:50 +0000 Subject: [PATCH 1/3] feat(action-page): PROG-123 merge Focus + Arc into one Location field with a tree picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner review of the first pass pushed further than parent-name hints: two separate fields hid a real coupling — setting the focus limits the arcs, and a focus change silently clears the arc. The action page now shows one **Location** field (focus line with gitUrl, arc nested beneath with an indent) opening a single palette picker that renders the Workspace → Focus → Arc tree in outline rank order. Workspaces are greyed, inert headers that keyboard selection skips; picking a focus means 'this focus, no arc' (the pinned 'No arc' row is gone); picking an arc lands focus + arc in one step (same-focus picks update arcId, cross-focus picks ride moveAction's PROG-118 arcId). Filtering is tree-aware: a query matches a row or any ancestor, and matches keep their ancestors visible as context. The shortcut is L (Location); M and A retire. The palette Item type gains header/indent so selection walks only actionable rows. Decision: docs/decisions/PROG-123.md (PROG-123b). REFERENCE picker, action-page, and keyboard-map sections updated. --- docs/REFERENCE.md | 27 ++-- docs/decisions/PROG-123.md | 32 +++++ src/client/commands/CommandPalette.tsx | 173 ++++++++++++++----------- src/client/commands/controller.ts | 2 +- src/client/commands/useGlobalKeys.ts | 6 +- src/client/pages/ActionPage.tsx | 65 ++++------ 6 files changed, 175 insertions(+), 130 deletions(-) diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index dfda858..4b96de5 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -466,8 +466,8 @@ so old bookmarks keep working. list is deterministic too** (PROG-83): pickers and selects (create dialogs, palette tag picker, palette container quick-jump, filter dropdowns per PROG-66) list alphabetically by name — except the palette - **move/arc pickers**, which follow this same outline order and hint each row - with its parent container (PROG-123) — tag chips on action + **location picker**, which renders the Workspace → Focus → Arc tree in this + same outline order (PROG-123) — tag chips on action pages and board/Agenda cards sort alphabetically (shared `tagsByAction`, `src/client/tags.ts`), and Archive groups sort by name. Board **actions** keep pure `rank` order — never alphabetized. The scope @@ -637,8 +637,10 @@ so old bookmarks keep working. (`actionAncestors`, `store.ts`) handles unbounded nesting and truncates on a missing or cyclic parent (PROG-106). Also: inline-editable title and description, sidebar fields - (status/due-date/priority/estimate in that order, then focus (its optional - gitUrl links out), arc, and tags with picker buttons; a **Work on this** + (status/due-date/priority/estimate in that order, then **Location** — one + field for the Focus → Arc position, the arc nested under its focus, opening + the location picker (PROG-123b; its optional gitUrl links out) — and tags + with picker buttons; a **Work on this** field — D35), a Git section (linked PRs with state badges, commits with short shas, linking out to GitHub), and comments + activity interleaved into one timeline. Each @@ -655,10 +657,17 @@ so old bookmarks keep working. actions by key (retired alias keys included) or title and containers by name, and lists commands (create action/workspace/focus/arc, pickers for the current action). Picker modes are filterable lists; tag - toggles keep the palette open for multi-edit. The move picker lists focuses - grouped by workspace in outline rank order with the parent workspace as the - row hint (a typed query matches it too); the arc picker lists the focus's - arcs in outline rank order, hinting the parent focus (PROG-123). + toggles keep the palette open for multi-edit. The **location picker** + (PROG-123) owns the action's whole outline position in one surface: it + renders the Workspace → Focus → Arc tree in outline rank order — workspaces + as greyed, inert headers that keyboard selection skips, focuses and arcs + indented beneath. Picking a focus means "this focus, no arc" (there is no + separate "No arc" row); picking an arc sets focus + arc in one step + (same-focus picks are a plain arc update, cross-focus picks ride + `moveAction`, which accepts the landing arc per PROG-118). The current + location hints "current". A typed query matches a row or any ancestor — an + ancestor match keeps its whole subtree visible, and ancestors of a match + stay as context. - **Create dialogs** — action and container creation; parents/containers default from the current view (open container page, viewed action's container, or active board filters). New actions default to **Todo** so @@ -674,7 +683,7 @@ so old bookmarks keep working. | `/` | Search modal (PROG-130) — separate from the palette; title/description hits paint instantly, comment hits stream in | | `C` | Create action | | `S` / `P` / `E` | Status / priority / estimate picker for the current action | -| `M` / `A` / `T` | Move / arc / tag picker for the current action | +| `L` / `T` | Location / tag picker for the current action (`L` replaced the pre-PROG-123b `M` move + `A` arc pair) | | `D` | Due-date picker for the current action (relative quick-picks or a typed `YYYY-MM-DD`; clear) | | `W` | Work on this — copy the bundle as a prompt or the `progress work` CLI line (D35) | | `↑↓`, `Enter`, `Esc`, `Backspace` | Navigate / run / close / back-to-root inside the palette | diff --git a/docs/decisions/PROG-123.md b/docs/decisions/PROG-123.md index 7d800fd..703cd23 100644 --- a/docs/decisions/PROG-123.md +++ b/docs/decisions/PROG-123.md @@ -22,3 +22,35 @@ Superseding PROG-83 **for these two pickers only**: Every other picker (tags, create dialogs, filter dropdowns, quick-jump) keeps PROG-83's alphabetical order — nothing else displays rank, so alpha remains the scannable choice there. + +### PROG-123b — one Location field replaces the Focus + Arc pair + +Owner review of PROG-123 (above) pushed further: flat lists with a +parent-name hint were still inconsistent with the app's nested-tree surfaces, +and two separate fields hid a real coupling — setting the focus limits the +available arcs, and a focus change silently clears the arc. Settled: + +- **The field is "Location"** (owner-picked over Place/Nest/Context): the + action's whole outline position, Workspace → Focus → Arc. The action page + shows one Location field — focus line (gitUrl beneath), arc nested with an + indent — replacing the separate Focus and Arc fields, so the clearing + coupling is explicit instead of a surprise. +- **One palette picker renders the whole tree** in outline rank order: + workspace rows are greyed, **inert headers** (keyboard selection skips + them; supersedes PROG-123's hint-on-the-right presentation), focuses indent + beneath, arcs beneath those. Picking a focus = "this focus, no arc" — the + old pinned "No arc" row is gone — and picking an arc lands focus + arc in + one step: same-focus picks are a plain `arcId` update, cross-focus picks + ride `moveAction`, which already carries a landing `arcId` (PROG-118). The + current location hints "current" and stays offered (moving within the + focus, or up to "just the focus", is legitimate). +- **Filtering is tree-aware**: a query matches a row or any ancestor; an + ancestor match keeps its subtree, and a match keeps its ancestors visible + as context. +- **The shortcut is `L`** (owner-picked): M (move) and A (arc) are retired + outright rather than aliased — one field, one key; D25's keyboard map + amended in REFERENCE. + +Scope: action page + palette only. The create-action dialog keeps its +separate focus/arc selects (and board filters their separate dropdowns — they +filter rather than set) for a follow-up under the consistency arc. diff --git a/src/client/commands/CommandPalette.tsx b/src/client/commands/CommandPalette.tsx index 0f9e672..94641e0 100644 --- a/src/client/commands/CommandPalette.tsx +++ b/src/client/commands/CommandPalette.tsx @@ -1,6 +1,6 @@ // The command palette (SPEC §4): ⌘K jumps to anything (actions by key — // including retired alias keys — or title, containers by name) and exposes -// commands. The single-key actions (s/p/e/m) open the same palette directly +// commands. The single-key actions (s/p/e/l) open the same palette directly // in a picker mode scoped to the current action, so there's exactly one // keyboard-driven surface to learn. @@ -29,16 +29,23 @@ import { } from "./controller"; // run() returning "keep" leaves the palette open (used by commands that -// switch it into a picker mode, and by the tag toggles). -type Item = { id: string; label: string; hint?: string; run: () => void | "keep" }; +// switch it into a picker mode, and by the tag toggles). `header` rows are +// inert group labels (the workspace level of the location tree, PROG-123b): +// greyed out, skipped by keyboard selection, no run. `indent` nests tree +// levels visually (1 = focus, 2 = arc). +type Item = { + id: string; + label: string; + hint?: string; + indent?: 1 | 2; +} & ({ header: true; run?: undefined } | { header?: undefined; run: () => void | "keep" }); const MODE_TITLES: Record, string> = { status: "Change status", priority: "Set priority", estimate: "Set estimate", - move: "Move to", + location: "Set location", tag: "Tags", - arc: "Set arc", due: "Set due date", workon: "Work on this", }; @@ -64,7 +71,10 @@ export default function CommandPalette({ snapshot }: { snapshot: SnapshotPayload [snapshot, mode, query, navigate], ); - const sel = Math.min(selected, Math.max(items.length - 1, 0)); + // Keyboard selection walks only the actionable rows — inert group headers + // (PROG-123b) render in place but can't be landed on. + const selectables = useMemo(() => items.filter((it) => !it.header), [items]); + const sel = Math.min(selected, Math.max(selectables.length - 1, 0)); useEffect(() => { listRef.current?.querySelector("[data-selected]")?.scrollIntoView({ block: "nearest" }); @@ -74,7 +84,7 @@ export default function CommandPalette({ snapshot }: { snapshot: SnapshotPayload const close = () => setMode(null); const execute = (item: Item) => { - if (item.run() !== "keep") close(); + if (item.run && item.run() !== "keep") close(); }; const onKeyDown = (e: React.KeyboardEvent) => { @@ -83,13 +93,13 @@ export default function CommandPalette({ snapshot }: { snapshot: SnapshotPayload close(); } else if (e.key === "ArrowDown") { e.preventDefault(); - setSelected(Math.min(sel + 1, items.length - 1)); + setSelected(Math.min(sel + 1, selectables.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setSelected(Math.max(sel - 1, 0)); } else if (e.key === "Enter") { e.preventDefault(); - const item = items[sel]; + const item = selectables[sel]; if (item) execute(item); } else if (e.key === "Backspace" && query === "" && mode.kind !== "root") { e.preventDefault(); @@ -125,21 +135,31 @@ export default function CommandPalette({ snapshot }: { snapshot: SnapshotPayload className="w-full border-b border-line px-4 py-3 text-sm focus:outline-none" /> @@ -216,56 +236,58 @@ function buildItems( } return items; } - case "arc": { - // Outline order, not alphabetical (PROG-123 supersedes PROG-83 here): - // arcs list in the manual rank set on the outline/structure pages, and - // each row's hint names the parent focus one level up. "No arc" stays - // pinned first. - const parentFocus = ws.focuses.find((p) => p.id === action.focusId); - const focusArcs = ws.arcs - .filter((a) => a.focusId === action.focusId && !a.archivedAt) - .sort(byRankThenName); - // "No arc" filters like any option, so a typed query can't leave it - // sitting first and steal the Enter. - return [ - { - id: "arc:none", - label: "No arc", - hint: action.arcId === null ? "current" : undefined, - run: () => void updateAction(action.id, { arcId: null }), - }, - ...focusArcs.map((a) => ({ - id: a.id, - label: a.name, - hint: a.id === action.arcId ? "current" : parentFocus?.name, - run: () => void updateAction(action.id, { arcId: a.id }), - })), - ].filter((item) => matches(item.label)); - } - case "move": { - // A move now only changes the focus (PROG-102). Archived focuses aren't - // valid destinations (D26); the action's current focus is excluded. - // Structure order, not alphabetical (PROG-123 supersedes PROG-83 here): - // focuses group under their workspace in the outline's manual rank, and - // the hint names the parent workspace. A typed query also matches the - // workspace name, since it's on screen. - const workspaceOrder = new Map(sortContainers(ws.workspaces).map((w, i) => [w.id, i])); - const workspaceName = (focus: { workspaceId: string }) => - ws.workspaces.find((w) => w.id === focus.workspaceId)?.name; - return ws.focuses - .filter((focus) => !focus.archivedAt && focus.id !== action.focusId) - .sort( - (a, b) => - (workspaceOrder.get(a.workspaceId) ?? 0) - (workspaceOrder.get(b.workspaceId) ?? 0) || - byRankThenName(a, b), - ) - .filter((focus) => matches(focus.name) || matches(workspaceName(focus) ?? "")) - .map((focus) => ({ - id: focus.id, - label: focus.name, - hint: workspaceName(focus) ?? "Focus", - run: () => moveAction(action.id, { focusId: focus.id }), - })); + case "location": { + // One picker owns the whole outline position (PROG-123b, replacing the + // separate move + arc modes): the tree renders Workspace (inert, greyed + // header) → Focus → Arc in the manual rank order set on the + // outline/structure pages (supersedes PROG-83's alphabetical rule + // here). Picking a focus row means "this focus, no arc" — there's no + // separate "No arc" row — and picking an arc lands focus + arc in one + // step (moveAction already carries an arcId, PROG-118). Same-focus + // picks are a plain field update; the current location hints "current". + // Archived containers aren't destinations (D26). A query matches a row + // or any ancestor (an ancestor match keeps its whole subtree), and + // ancestors of a match stay visible as context. + const items: Item[] = []; + for (const workspace of sortContainers(ws.workspaces)) { + const wsMatch = matches(workspace.name); + const group: Item[] = []; + for (const focus of ws.focuses + .filter((p) => p.workspaceId === workspace.id && !p.archivedAt) + .sort(byRankThenName)) { + const focusMatch = wsMatch || matches(focus.name); + const visibleArcs = ws.arcs + .filter((a) => a.focusId === focus.id && !a.archivedAt) + .sort(byRankThenName) + .filter((a) => focusMatch || matches(a.name)); + if (!focusMatch && visibleArcs.length === 0) continue; + group.push({ + id: focus.id, + label: focus.name, + indent: 1, + hint: focus.id === action.focusId && action.arcId === null ? "current" : undefined, + run: () => + focus.id === action.focusId + ? void updateAction(action.id, { arcId: null }) + : moveAction(action.id, { focusId: focus.id }), + }); + group.push( + ...visibleArcs.map((a): Item => ({ + id: a.id, + label: a.name, + indent: 2, + hint: a.id === action.arcId ? "current" : undefined, + run: () => + focus.id === action.focusId + ? void updateAction(action.id, { arcId: a.id }) + : moveAction(action.id, { focusId: focus.id, arcId: a.id }), + })), + ); + } + if (group.length > 0) + items.push({ id: workspace.id, label: workspace.name, header: true }, ...group); + } + return items; } case "due": { const today = todayISO(); @@ -371,9 +393,8 @@ function rootItems( picker("status", "S"), picker("priority", "P"), picker("estimate", "E"), - picker("move", "M"), + picker("location", "L"), picker("tag", "T"), - picker("arc", "A"), picker("due", "D"), picker("workon", "W"), ); diff --git a/src/client/commands/controller.ts b/src/client/commands/controller.ts index 70df3d9..49e04d1 100644 --- a/src/client/commands/controller.ts +++ b/src/client/commands/controller.ts @@ -5,7 +5,7 @@ export type PaletteMode = | { kind: "root"; actionId: string | null } | { - kind: "status" | "priority" | "estimate" | "move" | "tag" | "arc" | "due" | "workon"; + kind: "status" | "priority" | "estimate" | "location" | "tag" | "due" | "workon"; actionId: string; }; diff --git a/src/client/commands/useGlobalKeys.ts b/src/client/commands/useGlobalKeys.ts index 64b92af..270a65d 100644 --- a/src/client/commands/useGlobalKeys.ts +++ b/src/client/commands/useGlobalKeys.ts @@ -2,7 +2,8 @@ // ⌘K / Ctrl+K command palette // / search (PROG-130 — title/description instantly, comments stream) // C create action (container defaults from the current view) -// S / P / E / M / T / A status / priority / estimate / move / tag / arc +// S / P / E / L / T status / priority / estimate / location / tag +// (L replaced the pre-PROG-123b M move + A arc pair) // D due date (quick relative options or a typed YYYY-MM-DD) // W work on this (copy bundle as prompt / CLI one-liner) // picker for the current action (action page, or the @@ -17,9 +18,8 @@ const PICKER_KEYS = { s: "status", p: "priority", e: "estimate", - m: "move", + l: "location", t: "tag", - a: "arc", d: "due", w: "workon", } as const; diff --git a/src/client/pages/ActionPage.tsx b/src/client/pages/ActionPage.tsx index 13f73e2..3ecdded 100644 --- a/src/client/pages/ActionPage.tsx +++ b/src/client/pages/ActionPage.tsx @@ -221,17 +221,19 @@ export default function ActionPage({ {/* Field order + the icon gutter (PROG-101, reworked PROG-104): every field carries a glyph on the left and its value in the same - text column. After the status panel, Focus + Arc — the container - switchers — then due/priority/estimate, and Tags last. The - Focus/Arc glyphs are buttons that open the move/arc palette - (S/P/E/M/A shortcuts still fire regardless). */} - + text column. After the status panel, Location — the action's + whole outline position, Focus → Arc, one field since setting the + focus is what limits the arcs (PROG-123b replaced the separate + Focus + Arc switchers) — then due/priority/estimate, and Tags + last. The Focus glyph is a button that opens the location + palette (S/P/E/L shortcuts still fire regardless). */} + openPalette({ kind: "move", actionId: action.id })} + aria-label="Change location (L)" + onClick={() => openPalette({ kind: "location", actionId: action.id })} className={`${GLYPH_BUTTON_CLS} text-ink-faint hover:text-ink-soft`} > @@ -261,48 +263,29 @@ export default function ActionPage({ {focus.gitUrl.replace(/^https?:\/\//, "")} )} - {/* Explicit "change" affordance (PROG-105): the gutter glyph - also opens this palette, but a modal-backed field needs a - visible trigger — the name itself links to the focus page. - Mirrors the Tags field's "Edit… (T)". */} - - - - - - openPalette({ kind: "arc", actionId: action.id })} - className={`${GLYPH_BUTTON_CLS} text-ink-faint hover:text-ink-soft`} - > - - - } - > -
- {arc ? ( + {/* The arc nests under its focus like the location picker and + outline render it — the field reads as the Focus → Arc + path. No arc → the focus line alone is the location. */} + {arc && ( - {arc.name} + + + + {arc.name} - ) : ( - )} + {/* Explicit "change" affordance (PROG-105): the gutter glyph + also opens this palette, but a modal-backed field needs a + visible trigger — the names themselves link to the focus + and arc pages. Mirrors the Tags field's "Edit… (T)". */}
From 9deda285956f33dc631188efc33a1b91dbe7bfb4 Mon Sep 17 00:00:00 2001 From: bkennedy Date: Thu, 16 Jul 2026 13:49:00 +0000 Subject: [PATCH 2/3] feat(palette): PROG-123 lead location-picker rows with container glyphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner follow-up on the Location picker: each tree row now carries its level's glyph — a new 2×2 portfolio-grid mark for workspaces, the existing focus target and arc marks for the rows beneath — so the modal shares the sidebar field's iconography. Focus/Arc glyphs move out of ActionPage into a shared src/client/glyphs.tsx (with the new WorkspaceGlyph); the palette Item type gains an optional leading icon. --- docs/REFERENCE.md | 4 ++- src/client/commands/CommandPalette.tsx | 24 ++++++++++--- src/client/glyphs.tsx | 47 ++++++++++++++++++++++++++ src/client/pages/ActionPage.tsx | 30 +--------------- 4 files changed, 70 insertions(+), 35 deletions(-) create mode 100644 src/client/glyphs.tsx diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 4b96de5..04a4684 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -661,7 +661,9 @@ so old bookmarks keep working. (PROG-123) owns the action's whole outline position in one surface: it renders the Workspace → Focus → Arc tree in outline rank order — workspaces as greyed, inert headers that keyboard selection skips, focuses and arcs - indented beneath. Picking a focus means "this focus, no arc" (there is no + indented beneath, each row led by its level's glyph (the shared + workspace-grid/focus-target/arc marks, `src/client/glyphs.tsx` — the same + iconography as the sidebar Location field). Picking a focus means "this focus, no arc" (there is no separate "No arc" row); picking an arc sets focus + arc in one step (same-focus picks are a plain arc update, cross-focus picks ride `moveAction`, which accepts the landing arc per PROG-118). The current diff --git a/src/client/commands/CommandPalette.tsx b/src/client/commands/CommandPalette.tsx index 94641e0..f935020 100644 --- a/src/client/commands/CommandPalette.tsx +++ b/src/client/commands/CommandPalette.tsx @@ -4,12 +4,13 @@ // in a picker mode scoped to the current action, so there's exactly one // keyboard-driven surface to learn. -import { useEffect, useMemo, useRef, useState } from "react"; +import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { useLocation } from "wouter"; import { ACTION_ESTIMATES, ACTION_PRIORITIES, ACTION_STATUSES } from "../../shared/constants"; import type { WireAction, SnapshotPayload } from "../../shared/types"; import { sortByName } from "../boardFilters"; import { byRankThenName, sortContainers } from "../containerReorder"; +import { ArcGlyph, FocusGlyph, WorkspaceGlyph } from "../glyphs"; import { addDays, formatDueDate, relativeDue, todayISO } from "../dates"; import { PRIORITY_LABELS, STATUS_LABELS } from "../labels"; import { @@ -32,11 +33,13 @@ import { // switch it into a picker mode, and by the tag toggles). `header` rows are // inert group labels (the workspace level of the location tree, PROG-123b): // greyed out, skipped by keyboard selection, no run. `indent` nests tree -// levels visually (1 = focus, 2 = arc). +// levels visually (1 = focus, 2 = arc); `icon` leads the label (the location +// tree carries the shared container glyphs, matching the sidebar field). type Item = { id: string; label: string; hint?: string; + icon?: ReactNode; indent?: 1 | 2; } & ({ header: true; run?: undefined } | { header?: undefined; run: () => void | "keep" }); @@ -138,7 +141,10 @@ export default function CommandPalette({ snapshot }: { snapshot: SnapshotPayload {items.map((item) => item.header ? (
  • -
    {item.label}
    +
    + {item.icon} + {item.label} +
  • ) : (
  • @@ -151,7 +157,10 @@ export default function CommandPalette({ snapshot }: { snapshot: SnapshotPayload item.indent === 2 ? "pl-11" : item.indent === 1 ? "pl-7" : "pl-3" }`} > - {item.label} + + {item.icon && {item.icon}} + {item.label} + {item.hint && ( {item.hint} )} @@ -264,6 +273,7 @@ function buildItems( group.push({ id: focus.id, label: focus.name, + icon: , indent: 1, hint: focus.id === action.focusId && action.arcId === null ? "current" : undefined, run: () => @@ -275,6 +285,7 @@ function buildItems( ...visibleArcs.map((a): Item => ({ id: a.id, label: a.name, + icon: , indent: 2, hint: a.id === action.arcId ? "current" : undefined, run: () => @@ -285,7 +296,10 @@ function buildItems( ); } if (group.length > 0) - items.push({ id: workspace.id, label: workspace.name, header: true }, ...group); + items.push( + { id: workspace.id, label: workspace.name, icon: , header: true }, + ...group, + ); } return items; } diff --git a/src/client/glyphs.tsx b/src/client/glyphs.tsx new file mode 100644 index 0000000..a3bcaef --- /dev/null +++ b/src/client/glyphs.tsx @@ -0,0 +1,47 @@ +// Container-level glyphs, shared by the action page's Location field and the +// palette's location picker so the two surfaces carry one iconography +// (PROG-123). Focus and Arc began life as ActionPage gutter glyphs +// (PROG-104); all three share the 16×16 box and currentColor, so callers set +// size context via text color only. + +// Workspace glyph: a 2×2 portfolio grid — the workspace holds focuses side by +// side rather than pointing at any one of them. +export function WorkspaceGlyph() { + return ( + + + + + + + ); +} + +// Focus glyph (PROG-104): a target/crosshair — the focus is the thing the +// action is "focused" on. +export function FocusGlyph() { + return ( + + + + + ); +} + +// Arc glyph (PROG-104): a rainbow-like arc between two endpoints — the +// milestone trajectory an arc groups actions along. +export function ArcGlyph() { + return ( + + + + + + ); +} diff --git a/src/client/pages/ActionPage.tsx b/src/client/pages/ActionPage.tsx index 3ecdded..bc37f5d 100644 --- a/src/client/pages/ActionPage.tsx +++ b/src/client/pages/ActionPage.tsx @@ -26,6 +26,7 @@ import { useRegisterPageAction } from "../commands/currentAction"; import EditableMarkdown from "../EditableMarkdown"; import InlineEdit from "../InlineEdit"; import EstimateIndicator from "../EstimateIndicator"; +import { ArcGlyph, FocusGlyph } from "../glyphs"; import { PRIORITY_LABELS, STATUS_LABELS } from "../labels"; import PriorityIndicator from "../PriorityIndicator"; import StatusIndicator from "../StatusIndicator"; @@ -487,35 +488,6 @@ function CalendarGlyph() { ); } -// Focus gutter glyph (PROG-104): a target/crosshair — the focus is the thing -// the action is "focused" on. Same 16×16 box as the other gutter glyphs. -function FocusGlyph() { - return ( - - - - - ); -} - -// Arc gutter glyph (PROG-104): a rainbow-like arc between two endpoints — the -// milestone trajectory an arc groups actions along. -function ArcGlyph() { - return ( - - - - - - ); -} - // The "Work on this" forward arrow (PROG-104): evokes jumping forward in // Progress. Accepts a class so the button can nudge it on hover. function ArrowGlyph({ className = "" }: { className?: string }) { From a52dbfb0d03e19d78cb532b896b06f337ebe03c4 Mon Sep 17 00:00:00 2001 From: bkennedy Date: Thu, 16 Jul 2026 14:09:35 +0000 Subject: [PATCH 3/3] feat(action-page): PROG-123 render the Location field as a glyphed mini-tree Owner follow-up: the sidebar Location field now shows all three hierarchy levels with the same glyphs the picker rows carry. A workspace line (linked, like focus/arc) tops the tree; the workspace grid glyph doubles as the field's gutter button, start-aligned so it sits on the workspace line (IconRow gains an align prop); the focus line carries the target glyph inline and the arc keeps its mark one indent deeper. gitUrl aligns under the focus label. --- docs/REFERENCE.md | 6 ++-- src/client/pages/ActionPage.tsx | 56 ++++++++++++++++++++++++--------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 04a4684..b3f385b 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -638,8 +638,10 @@ so old bookmarks keep working. missing or cyclic parent (PROG-106). Also: inline-editable title and description, sidebar fields (status/due-date/priority/estimate in that order, then **Location** — one - field for the Focus → Arc position, the arc nested under its focus, opening - the location picker (PROG-123b; its optional gitUrl links out) — and tags + field showing the Workspace → Focus → Arc position as a mini-tree, each + line led by its level's glyph (the workspace's doubles as the gutter + button) and linking to its page, opening the location picker (PROG-123b; + the focus's optional gitUrl links out) — and tags with picker buttons; a **Work on this** field — D35), a Git section (linked PRs with state badges, commits with short shas, linking out to diff --git a/src/client/pages/ActionPage.tsx b/src/client/pages/ActionPage.tsx index bc37f5d..bf6e01a 100644 --- a/src/client/pages/ActionPage.tsx +++ b/src/client/pages/ActionPage.tsx @@ -26,7 +26,7 @@ import { useRegisterPageAction } from "../commands/currentAction"; import EditableMarkdown from "../EditableMarkdown"; import InlineEdit from "../InlineEdit"; import EstimateIndicator from "../EstimateIndicator"; -import { ArcGlyph, FocusGlyph } from "../glyphs"; +import { ArcGlyph, FocusGlyph, WorkspaceGlyph } from "../glyphs"; import { PRIORITY_LABELS, STATUS_LABELS } from "../labels"; import PriorityIndicator from "../PriorityIndicator"; import StatusIndicator from "../StatusIndicator"; @@ -223,13 +223,17 @@ export default function ActionPage({ {/* Field order + the icon gutter (PROG-101, reworked PROG-104): every field carries a glyph on the left and its value in the same text column. After the status panel, Location — the action's - whole outline position, Focus → Arc, one field since setting the - focus is what limits the arcs (PROG-123b replaced the separate - Focus + Arc switchers) — then due/priority/estimate, and Tags - last. The Focus glyph is a button that opens the location - palette (S/P/E/L shortcuts still fire regardless). */} + whole outline position as a Workspace → Focus → Arc mini-tree, + one field since setting the focus is what limits the arcs + (PROG-123b replaced the separate Focus + Arc switchers) — then + due/priority/estimate, and Tags last. Each tree line leads with + its level's glyph, matching the location picker; the workspace's + doubles as the field's gutter button (start-aligned so it sits + on the workspace line), opening the location palette (S/P/E/L + shortcuts still fire regardless). */} openPalette({ kind: "location", actionId: action.id })} className={`${GLYPH_BUTTON_CLS} text-ink-faint hover:text-ink-soft`} > - + } > {/* pl-2 aligns the value text with the select/input fields below, whose text sits inside a border + px-2 gutter (PROG-104). */}
    + {workspace && ( + + {workspace.name} + + )} {focus ? ( - {focus.name} + + + + {focus.name} ) : ( ? @@ -259,14 +274,14 @@ export default function ActionPage({ href={focus.gitUrl} target="_blank" rel="noreferrer" - className="block truncate font-mono text-xs text-ink-faint hover:text-ink-soft hover:underline" + className="block truncate pl-5 font-mono text-xs text-ink-faint hover:text-ink-soft hover:underline" > {focus.gitUrl.replace(/^https?:\/\//, "")} )} {/* The arc nests under its focus like the location picker and - outline render it — the field reads as the Focus → Arc - path. No arc → the focus line alone is the location. */} + outline render it — the field reads as the Workspace → + Focus → Arc path. No arc → the focus line ends the path. */} {arc && ( - {icon} +
    + {/* start: the glyph anchors to the first content line instead of the + stack's middle (the Location tree, PROG-123) — the 3px nudge centers + the 14px glyph on the 20px text-sm line box. */} + {align === "start" ?
    {icon}
    : icon}
    {children}
    );