diff --git a/CHANGELOG.md b/CHANGELOG.md index c2157fd..c9d946f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,11 @@ Versions follow [SemVer](https://semver.org/) (`0.1.0-alpha.x` while the public - MCP / AGENTS docs no longer imply a silent `PRM_PASSWORD=workbench` default - Settings → AI: OpenAI-compatible gateways are a separate provider from Cursor Cloud Agents - Empty workspaces land in setup until finished or skipped; demo seed marks onboarding complete +<<<<<<< HEAD - **Cursor Cloud Agents** chat: stream status and assistant text live via SSE; empty-result errors include agent/run ids and a dashboard link +======= +- Primary CTA hierarchy: NextStep owns the solid primary action; PageHeader/Section duplicates and table-row moves use secondary/ghost so pages like Cycles no longer show the same green button 3–4 times +>>>>>>> bd04ca1 (Clarify primary CTAs across Cycles and other pages) ### Fixed diff --git a/apps/ui/src/pages/BackfillPage.tsx b/apps/ui/src/pages/BackfillPage.tsx index 781a698..eb2a10b 100644 --- a/apps/ui/src/pages/BackfillPage.tsx +++ b/apps/ui/src/pages/BackfillPage.tsx @@ -89,7 +89,7 @@ export function BackfillPage() { tone="ok" label="Dossiers look populated" detail="Every direct has at least two evidence items. Keep capturing year-round." - action={Back to command center} + action={Back to command center} /> )} diff --git a/apps/ui/src/pages/ConsistencyPage.tsx b/apps/ui/src/pages/ConsistencyPage.tsx index f64e1e5..4b872d1 100644 --- a/apps/ui/src/pages/ConsistencyPage.tsx +++ b/apps/ui/src/pages/ConsistencyPage.tsx @@ -56,9 +56,9 @@ export function ConsistencyPage() { + Open first writing desk ) : undefined @@ -114,7 +114,7 @@ export function ConsistencyPage() { action={ - ) : ( - - ) + secondary={ + empty ? ( + + Jump to create form + + ) : selectedCycle && subjectId ? ( + + Open writing desk + + ) : undefined } - secondary={selectedCycle && subjectId ? ( - Open writing desk - ) : undefined} /> - {cycles.length === 0 ? ( + {empty ? ( Create cycle with directs} + action={ + + } /> ) : ( - - Jump to import + + + Jump to import + } /> @@ -90,286 +100,339 @@ export function CyclesPage() { { id: "import-bundle", label: "Import" }, ]} > -
- Create cycle with directs - - } - > - {cycles.length === 0 ? ( -

No cycles yet.

- ) : ( - - - - {cycles.map((c) => ( - setSelectedCycle(c.id)} - > - - - - - - ))} - -
NameWindowStatusPeople
{c.name}{c.windowStart} → {c.windowEnd}{c.status}{c.participantCount}
- )} -
{ - e.preventDefault(); - await api("/api/cycles", { - method: "POST", - body: JSON.stringify({ - name, - windowStart, - windowEnd, - includeDirects: true, - ...(selfDue ? { selfDue } : {}), - ...(peerDue ? { peerDue } : {}), - ...(managerDue ? { managerDue } : {}), - }), - }); - setMessage("Cycle created — export self-review bundles next."); - await load(); - }} - > -
- setName(e.target.value)} aria-label="Cycle name" placeholder="Cycle name" /> -
- - setWindowStart(e.target.value)} required /> -
-
- - setWindowEnd(e.target.value)} required /> -
-
-
-
- - setSelfDue(e.target.value)} /> -
-
- - setPeerDue(e.target.value)} /> -
-
- - setManagerDue(e.target.value)} /> -
- -
-
-
- - {selected && (
- Update cycle window - - } + id="cycles-list" + title="Cycles" + when="Create a cycle when a review window starts; select a row anytime to export, import, or write for that cycle." + how="Fill name, window, and optional due dates, then create with your current directs — or click a cycle to select it." > -
- {PHASE_STATUSES.map((phase) => ( - - ))} -
+ {empty ? ( +

No cycles yet.

+ ) : ( + + + + + + + + + + + {cycles.map((c) => ( + setSelectedCycle(c.id)} + > + + + + + + ))} + +
NameWindowStatusPeople
{c.name} + {c.windowStart} → {c.windowEnd} + {c.status}{c.participantCount}
+ )}
{ e.preventDefault(); - const fd = new FormData(e.currentTarget); - await api(`/api/cycles/${selected.id}`, { - method: "PATCH", + await api("/api/cycles", { + method: "POST", body: JSON.stringify({ - windowStart: String(fd.get("windowStart") || selected.windowStart), - windowEnd: String(fd.get("windowEnd") || selected.windowEnd), + name, + windowStart, + windowEnd, + includeDirects: true, + ...(selfDue ? { selfDue } : {}), + ...(peerDue ? { peerDue } : {}), + ...(managerDue ? { managerDue } : {}), }), }); - setMessage("Cycle window updated."); + setMessage("Cycle created — export self-review bundles next."); await load(); }} > -
- - +
+ setName(e.target.value)} + aria-label="Cycle name" + placeholder="Cycle name" + /> +
+ + setWindowStart(e.target.value)} + required + /> +
+
+ + setWindowEnd(e.target.value)} + required + /> +
-
- - +
+
+ + setSelfDue(e.target.value)} /> +
+
+ + setPeerDue(e.target.value)} /> +
+
+ + setManagerDue(e.target.value)} + /> +
+
-
- )} -
-
- Download JSON bundle - - } - > -
{ - e.preventDefault(); - if (!selectedCycle || !subjectId) return; - const bundle = await api>("/api/bundles/export", { - method: "POST", - body: JSON.stringify({ - kind: bundleKind, - cycleId: selectedCycle, - subjectPersonId: subjectId, - peerAuthorName: peerName || undefined, - }), - }); - const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: "application/json" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `${bundleKind}-${(bundle.subjectName as string)?.replace(/\s+/g, "-") ?? "bundle"}.json`; - a.click(); - setMessage( - `Bundle downloaded — ${bundleKind} pipeline marked exported. Send outside the app, then import the filled JSON below.`, - ); - }} + {selected && ( +
-
- - -
-
- - +
+ {PHASE_STATUSES.map((phase) => { + const active = selected.status === phase.value; + return ( + + ); + })}
-
- - -
- {bundleKind === "peer" && ( -
- - setPeerName(e.target.value)} placeholder="Jordan Lee" /> + { + e.preventDefault(); + const fd = new FormData(e.currentTarget); + await api(`/api/cycles/${selected.id}`, { + method: "PATCH", + body: JSON.stringify({ + windowStart: String(fd.get("windowStart") || selected.windowStart), + windowEnd: String(fd.get("windowEnd") || selected.windowEnd), + }), + }); + setMessage("Cycle window updated."); + await load(); + }} + > +
+ +
- )} - -
+
+ + +
+ + +
+ )} -
{ - const parsed = JSON.parse(importText) as Record; - const fields = (parsed.fields ?? {}) as Record; - if (parsed.kind === "self" && !fields.top_outcomes) { - fields.top_outcomes = "Shipped reliability work; mentored a new joiner; unblocked checkout migration."; - fields.growth_areas = "Take more cross-team design leadership."; - fields.proposed_goals = "Own Q3 platform migration milestones."; - parsed.fields = fields; - } - if (parsed.kind === "peer" && !fields.strengths) { - fields.context = "Paired on API design for 6 weeks."; - fields.strengths = "Clear written RFCs; calm under incident pressure."; - fields.growth = "Could socialize designs earlier with partner teams."; - parsed.fields = fields; - parsed.peerAuthorName = parsed.peerAuthorName || "Peer Reviewer"; - } - await api("/api/bundles/import", { +
+
+
{ + e.preventDefault(); + if (!selectedCycle || !subjectId) return; + const bundle = await api>("/api/bundles/export", { method: "POST", - body: JSON.stringify({ bundle: parsed, sourceFilename: "paste.json" }), + body: JSON.stringify({ + kind: bundleKind, + cycleId: selectedCycle, + subjectPersonId: subjectId, + peerAuthorName: peerName || undefined, + }), }); - const sid = String(parsed.subjectPersonId ?? subjectId); - const cid = String(parsed.cycleId ?? selectedCycle); - setMessage("Imported. Next: open the writing desk and draft the manager review."); - setImportText(""); - setSubjectId(sid); - setSelectedCycle(cid); + const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${bundleKind}-${(bundle.subjectName as string)?.replace(/\s+/g, "-") ?? "bundle"}.json`; + a.click(); + setMessage( + `Bundle downloaded — ${bundleKind} pipeline marked exported. Send outside the app, then import the filled JSON below.`, + ); }} > - Import completed JSON - - } - > -
- -