generated from coulomb/repo-seed
feat(goals): add domain/repo goal tracking and update_workstream MCP tool
- Migration c5d6e7f8a9b0: domain_goals and repo_goals tables, repo_goal_id FK on workstreams - DomainGoal: one active per domain (partial unique index), status active/archived/superseded - RepoGoal: integer priority, status active/paused/completed/archived, optional domain_goal_id link - WorkstreamUpdate schema and router extended with repo_goal_id and repo_goal_id filter - 6 new MCP goal tools: create_domain_goal, get_domain_goals, activate_domain_goal, create_repo_goal, get_repo_goals, update_repo_goal - update_workstream MCP tool: patch any subset of workstream fields (title, description, owner, due_date, repo_goal_id, status) - get_domain_summary extended with goal_guidance (needs_workplan, alignment_warnings) signals - Dashboard goals.md page and docs/goals.md reference page - CLAUDE.md template updated to act on goal_guidance signals at session start - CUST-WP-0010 workplan for this feature Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
136
dashboard/src/docs/goals.md
Normal file
136
dashboard/src/docs/goals.md
Normal file
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: Goals — Reference
|
||||
---
|
||||
|
||||
# Goals — Reference
|
||||
|
||||
The Goals page shows strategic intent at two levels — **domain** and **repository** — and how they relate. It provides context for why workstreams exist and what they collectively deliver.
|
||||
|
||||
---
|
||||
|
||||
## The two goal levels
|
||||
|
||||
### Domain Goal
|
||||
|
||||
A domain goal captures the **high-level strategic intent** for an entire domain (e.g., railiance, markitect). It answers the question: *"What are we ultimately trying to achieve here?"*
|
||||
|
||||
Key properties:
|
||||
- Only **one domain goal can be active** per domain at any time.
|
||||
- When a new goal is activated, the previous one is automatically marked **superseded** — it is retained as history, not deleted.
|
||||
- Goals can also be manually set to **archived** to park them permanently.
|
||||
|
||||
### Repository Goal
|
||||
|
||||
A repository goal refines a domain goal into **actionable scope** for a specific repository. It answers: *"What does this repo need to deliver to advance the domain goal?"*
|
||||
|
||||
Key properties:
|
||||
- A repo can have **multiple active goals** with different **priorities**.
|
||||
- Priority is an integer — lower number means higher priority (e.g., `10` takes precedence over `100`).
|
||||
- Each repo goal should be linked to its parent domain goal via `domain_goal_id`, though unlinked goals are also supported.
|
||||
- Goals can shift between `active`, `paused`, `completed`, and `archived` as work evolves.
|
||||
|
||||
---
|
||||
|
||||
## Status lifecycle
|
||||
|
||||
### Domain goals
|
||||
|
||||
```
|
||||
active ──→ superseded (auto, when a newer goal is activated)
|
||||
active ──→ archived (manual, via PATCH /domain-goals/{id})
|
||||
```
|
||||
|
||||
Only one goal per domain can hold `active` status. Activating a goal via `POST /domain-goals/{id}/activate` supersedes whatever was active before.
|
||||
|
||||
### Repository goals
|
||||
|
||||
```
|
||||
active ──→ paused (work deprioritised, not abandoned)
|
||||
active ──→ completed (the goal was achieved)
|
||||
active ──→ archived (scope dropped or obsolete)
|
||||
paused ──→ active (work resumed)
|
||||
```
|
||||
|
||||
Completed and archived goals are preserved as history. They appear in the secondary accordion on the Goals page under the domain goal they were linked to.
|
||||
|
||||
---
|
||||
|
||||
## Page layout
|
||||
|
||||
The Goals page groups everything by domain:
|
||||
|
||||
```
|
||||
┌─ railiance ──────────────────────────────────────────────────────┐
|
||||
│ ┌─ Domain Goal ──────────────────────────── [active] ─────────┐ │
|
||||
│ │ Three-Phoenix Secure Kubernetes Infrastructure │ │
|
||||
│ │ Improve the railiance repositories so that I can … │ │
|
||||
│ │ ───────────────────────────────────────────────────────── │ │
|
||||
│ │ Repository Goals │ │
|
||||
│ │ ┌─ #10 railiance-bootstrap ─────────── [active] ────────┐ │ │
|
||||
│ │ │ Secure Single-Server Bootstrap at HostEurope │ │ │
|
||||
│ │ │ Bootstrap a new server securely at hosteurope … │ │ │
|
||||
│ │ └────────────────────────────────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ▶ 0 secondary goals │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- The **active domain goal** is the primary card with a green left border.
|
||||
- **Repository goals** are nested inside it, sorted by priority (lowest number first).
|
||||
- **Secondary goals** (superseded and archived domain goals, with their repo goals) are collapsed into an accordion below the active goal. Click to expand.
|
||||
- Domains that have no active goal are shown at the bottom and highlighted amber in the KPI box.
|
||||
- **Unlinked repository goals** — active repo goals not associated with any domain goal — appear in a separate section at the bottom of the page.
|
||||
|
||||
---
|
||||
|
||||
## KPI sidebar
|
||||
|
||||
| Metric | Meaning |
|
||||
|---|---|
|
||||
| **domains with active goal** | How many of your active domains have a current strategic intent set |
|
||||
| **active repo goals** | Total active repo goals across all repos |
|
||||
| **no active goal** | Domains missing a goal — shown in amber when non-zero |
|
||||
|
||||
---
|
||||
|
||||
## MCP tools
|
||||
|
||||
| Tool | Description |
|
||||
|---|---|
|
||||
| `create_domain_goal(domain_slug, title, description)` | Create a new active goal for a domain (supersedes previous active) |
|
||||
| `get_domain_goals(domain_slug, status?)` | List domain goals, optionally filtered by status |
|
||||
| `activate_domain_goal(goal_id)` | Make an existing goal active, superseding the current one |
|
||||
| `create_repo_goal(repo_slug, title, description, domain_goal_id?, priority?)` | Create a repo goal, optionally linked to a domain goal |
|
||||
| `get_repo_goals(repo_slug, status?)` | List repo goals for a repository |
|
||||
| `update_repo_goal(goal_id, ...)` | Update title, description, priority, status, or domain link |
|
||||
|
||||
---
|
||||
|
||||
## REST endpoints
|
||||
|
||||
| Method | Path | Effect |
|
||||
|---|---|---|
|
||||
| `GET` | `/domain-goals/` | List domain goals (filter: `domain_slug`, `status`) |
|
||||
| `POST` | `/domain-goals/` | Create a domain goal |
|
||||
| `GET` | `/domain-goals/{id}` | Get a single domain goal |
|
||||
| `PATCH` | `/domain-goals/{id}` | Update title, description, or status |
|
||||
| `POST` | `/domain-goals/{id}/activate` | Activate a goal (supersedes current active) |
|
||||
| `GET` | `/repo-goals/` | List repo goals (filter: `repo_slug`, `domain_goal_id`, `status`) |
|
||||
| `POST` | `/repo-goals/` | Create a repo goal |
|
||||
| `GET` | `/repo-goals/{id}` | Get a single repo goal |
|
||||
| `PATCH` | `/repo-goals/{id}` | Update any field |
|
||||
|
||||
---
|
||||
|
||||
## Linking workstreams to repo goals
|
||||
|
||||
Workstreams carry an optional `repo_goal_id` field. Setting it traces *why* a workstream exists — which specific repo goal it contributes to. This connection is currently recorded in the DB but is not yet visualised in the Workstreams page.
|
||||
|
||||
To set the link when creating a workstream via `create_workstream`, pass `repo_goal_id`. To update an existing one, use `PATCH /workstreams/{id}/` with `{"repo_goal_id": "<uuid>"}`.
|
||||
|
||||
---
|
||||
|
||||
## Design rationale
|
||||
|
||||
Goals are intentionally separate from workstreams. A workstream is a unit of *deliverable work*; a goal is a statement of *strategic intent*. Goals are stable and long-lived; workstreams are created, completed, and replaced as work advances. The goal hierarchy (domain → repo → workstream) provides the context needed to understand why any given piece of work exists.
|
||||
325
dashboard/src/goals.md
Normal file
325
dashboard/src/goals.md
Normal file
@@ -0,0 +1,325 @@
|
||||
---
|
||||
title: Goals
|
||||
---
|
||||
|
||||
```js
|
||||
const API = "http://127.0.0.1:8000";
|
||||
const POLL = 20_000;
|
||||
```
|
||||
|
||||
```js
|
||||
const goalsState = (async function*() {
|
||||
while (true) {
|
||||
let domains = [], domainGoals = [], repoGoals = [], repos = [], ok = false;
|
||||
try {
|
||||
const [rd, rdg, rrg, rr] = await Promise.all([
|
||||
fetch(`${API}/domains/?status=active`),
|
||||
fetch(`${API}/domain-goals/`),
|
||||
fetch(`${API}/repo-goals/`),
|
||||
fetch(`${API}/repos/`),
|
||||
]);
|
||||
ok = rd.ok && rdg.ok && rrg.ok && rr.ok;
|
||||
if (ok) {
|
||||
[domains, domainGoals, repoGoals, repos] = await Promise.all([
|
||||
rd.json(), rdg.json(), rrg.json(), rr.json(),
|
||||
]);
|
||||
}
|
||||
} catch {}
|
||||
yield {domains, domainGoals, repoGoals, repos, ok, ts: new Date()};
|
||||
await new Promise(res => setTimeout(res, POLL));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```js
|
||||
const domains = goalsState.domains ?? [];
|
||||
const domainGoals = goalsState.domainGoals ?? [];
|
||||
const repoGoals = goalsState.repoGoals ?? [];
|
||||
const repos = goalsState.repos ?? [];
|
||||
const _ok = goalsState.ok ?? false;
|
||||
const _ts = goalsState.ts;
|
||||
```
|
||||
|
||||
```js
|
||||
// ── Indexes ────────────────────────────────────────────────────────────────────
|
||||
const repoById = Object.fromEntries(repos.map(r => [r.id, r]));
|
||||
const domainById = Object.fromEntries(domains.map(d => [d.id, d]));
|
||||
|
||||
// Domain goals keyed by domain_id; active first, then superseded, then archived
|
||||
const goalsByDomain = {};
|
||||
for (const g of domainGoals) {
|
||||
if (!goalsByDomain[g.domain_id]) goalsByDomain[g.domain_id] = [];
|
||||
goalsByDomain[g.domain_id].push(g);
|
||||
}
|
||||
const STATUS_ORDER = {active: 0, superseded: 1, archived: 2};
|
||||
for (const id of Object.keys(goalsByDomain)) {
|
||||
goalsByDomain[id].sort((a, b) =>
|
||||
(STATUS_ORDER[a.status] ?? 9) - (STATUS_ORDER[b.status] ?? 9)
|
||||
);
|
||||
}
|
||||
|
||||
// Repo goals keyed by domain_goal_id (primary) and repo_id (for unlinked)
|
||||
const repoGoalsByDomainGoal = {};
|
||||
const unlinkedRepoGoals = []; // active repo goals with no domain_goal_id
|
||||
for (const rg of repoGoals) {
|
||||
if (rg.domain_goal_id) {
|
||||
if (!repoGoalsByDomainGoal[rg.domain_goal_id]) repoGoalsByDomainGoal[rg.domain_goal_id] = [];
|
||||
repoGoalsByDomainGoal[rg.domain_goal_id].push(rg);
|
||||
} else if (rg.status === "active") {
|
||||
unlinkedRepoGoals.push(rg);
|
||||
}
|
||||
}
|
||||
// Sort repo goals within each domain goal by priority asc
|
||||
for (const id of Object.keys(repoGoalsByDomainGoal)) {
|
||||
repoGoalsByDomainGoal[id].sort((a, b) => a.priority - b.priority);
|
||||
}
|
||||
|
||||
// KPI
|
||||
const domainsWithActiveGoal = domains.filter(d => (goalsByDomain[d.id] ?? []).some(g => g.status === "active"));
|
||||
const domainsWithoutGoal = domains.filter(d => !(goalsByDomain[d.id] ?? []).some(g => g.status === "active"));
|
||||
const totalActiveRepoGoals = repoGoals.filter(g => g.status === "active").length;
|
||||
```
|
||||
|
||||
# Goals
|
||||
|
||||
```js
|
||||
import {injectTocTop} from "./components/toc-sidebar.js";
|
||||
import {withDocHelp} from "./components/doc-overlay.js";
|
||||
|
||||
// ── Live indicator ─────────────────────────────────────────────────────────────
|
||||
const _liveEl = html`<div class="live-indicator">
|
||||
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
|
||||
${_ok
|
||||
? `Live · updated ${_ts?.toLocaleTimeString()}`
|
||||
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
|
||||
</div>`;
|
||||
withDocHelp(_liveEl, "/docs/live-data");
|
||||
|
||||
// ── KPI sidebar card ────────────────────────────────────────────────────────────
|
||||
const _kpiBox = html`<div class="kpi-infobox">
|
||||
<div class="kpi-infobox-title">Goals</div>
|
||||
<div class="kpi-row">
|
||||
<span class="kpi-row-label">domains with active goal</span>
|
||||
<div class="kpi-row-right">
|
||||
<div class="kpi-row-value">${domainsWithActiveGoal.length} / ${domains.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-row">
|
||||
<span class="kpi-row-label">active repo goals</span>
|
||||
<div class="kpi-row-right">
|
||||
<div class="kpi-row-value">${totalActiveRepoGoals}</div>
|
||||
</div>
|
||||
</div>
|
||||
${domainsWithoutGoal.length > 0 ? html`
|
||||
<div class="kpi-block" style="border-top:1px solid var(--theme-foreground-faint,#eee);padding-top:0.4rem;margin-top:0.1rem">
|
||||
<div class="kpi-row-label" style="color:#b45309;margin-bottom:0.25rem">no active goal</div>
|
||||
<div class="kpi-slug-list">${domainsWithoutGoal.map(d => html`<div class="kpi-slug-item">${d.slug}</div>`)}</div>
|
||||
</div>` : ""}
|
||||
</div>`;
|
||||
|
||||
injectTocTop("goals-kpi-box", _kpiBox);
|
||||
injectTocTop("live-indicator", _liveEl);
|
||||
|
||||
const _h1 = document.querySelector("#observablehq-main h1");
|
||||
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/goals"); }
|
||||
```
|
||||
|
||||
Strategic intent, organised by domain. Each domain has one **active** goal at a time; prior goals are retained as history. Repository goals inherit from a domain goal and refine it into actionable scope for a specific repo.
|
||||
|
||||
---
|
||||
|
||||
```js
|
||||
// ── Repo goal card renderer ────────────────────────────────────────────────────
|
||||
function renderRepoGoalCard(rg) {
|
||||
const repo = repoById[rg.repo_id];
|
||||
const STATUS_COLORS = {
|
||||
active: {border: "#3b82f6", badge_bg: "#dbeafe", badge_fg: "#1e40af"},
|
||||
paused: {border: "#f59e0b", badge_bg: "#fef3c7", badge_fg: "#92400e"},
|
||||
completed: {border: "#22c55e", badge_bg: "#dcfce7", badge_fg: "#166534"},
|
||||
archived: {border: "#94a3b8", badge_bg: "#f1f5f9", badge_fg: "#475569"},
|
||||
};
|
||||
const c = STATUS_COLORS[rg.status] ?? STATUS_COLORS.archived;
|
||||
return html`<div class="repo-goal-card" style="border-left-color:${c.border}">
|
||||
<div class="rg-header">
|
||||
<span class="rg-priority" title="Priority (lower = higher priority)">#${rg.priority}</span>
|
||||
<span class="rg-repo">${repo?.slug ?? rg.repo_id.slice(0,8)}</span>
|
||||
<span class="status-badge" style="background:${c.badge_bg};color:${c.badge_fg}">${rg.status}</span>
|
||||
</div>
|
||||
<div class="rg-title">${rg.title}</div>
|
||||
<div class="rg-desc">${rg.description}</div>
|
||||
<div class="rg-meta">goal id: <code>${rg.id.slice(0,8)}…</code></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Domain section renderer ────────────────────────────────────────────────────
|
||||
function renderDomainSection(domain) {
|
||||
const goals = goalsByDomain[domain.id] ?? [];
|
||||
const activeGoal = goals.find(g => g.status === "active");
|
||||
const secondaryGoals = goals.filter(g => g.status !== "active");
|
||||
|
||||
return html`<section class="domain-section">
|
||||
<div class="domain-header">
|
||||
<span class="domain-chip">${domain.slug}</span>
|
||||
<span class="domain-name">${domain.name}</span>
|
||||
</div>
|
||||
|
||||
${activeGoal ? html`
|
||||
<!-- ── Active domain goal ─────────────────────────────────────────── -->
|
||||
<div class="domain-goal-card dg-active">
|
||||
<div class="dg-header">
|
||||
<span class="dg-level-label">Domain Goal</span>
|
||||
<span class="status-badge badge-active">active</span>
|
||||
</div>
|
||||
<div class="dg-title">${activeGoal.title}</div>
|
||||
<div class="dg-desc">${activeGoal.description}</div>
|
||||
<div class="dg-meta">
|
||||
goal id: <code>${activeGoal.id.slice(0,8)}…</code> ·
|
||||
set ${new Date(activeGoal.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
|
||||
${(repoGoalsByDomainGoal[activeGoal.id] ?? []).length > 0 ? html`
|
||||
<div class="rg-section">
|
||||
<div class="rg-section-label">Repository Goals</div>
|
||||
${(repoGoalsByDomainGoal[activeGoal.id] ?? []).map(renderRepoGoalCard)}
|
||||
</div>` : html`
|
||||
<div class="rg-section rg-empty">No repository goals linked to this domain goal yet.</div>`}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="dg-empty">
|
||||
<span class="dg-no-goal-label">No active goal set for this domain.</span>
|
||||
</div>`}
|
||||
|
||||
${secondaryGoals.length > 0 ? html`
|
||||
<!-- ── Secondary goals (superseded / archived) ────────────────────── -->
|
||||
<details class="secondary-goals-details">
|
||||
<summary class="secondary-goals-summary">
|
||||
${secondaryGoals.length} secondary goal${secondaryGoals.length === 1 ? "" : "s"}
|
||||
(${[...new Set(secondaryGoals.map(g => g.status))].join(", ")})
|
||||
</summary>
|
||||
<div class="secondary-goals-list">
|
||||
${secondaryGoals.map(g => html`
|
||||
<div class="domain-goal-card dg-secondary">
|
||||
<div class="dg-header">
|
||||
<span class="dg-level-label">Domain Goal</span>
|
||||
<span class="status-badge badge-${g.status}">${g.status}</span>
|
||||
</div>
|
||||
<div class="dg-title">${g.title}</div>
|
||||
<div class="dg-desc">${g.description}</div>
|
||||
<div class="dg-meta">
|
||||
goal id: <code>${g.id.slice(0,8)}…</code> ·
|
||||
set ${new Date(g.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
${(repoGoalsByDomainGoal[g.id] ?? []).length > 0 ? html`
|
||||
<div class="rg-section rg-section-secondary">
|
||||
<div class="rg-section-label">Repository Goals</div>
|
||||
${(repoGoalsByDomainGoal[g.id] ?? []).map(renderRepoGoalCard)}
|
||||
</div>` : ""}
|
||||
</div>`)}
|
||||
</div>
|
||||
</details>` : ""}
|
||||
|
||||
</section>`;
|
||||
}
|
||||
|
||||
// ── Main render ────────────────────────────────────────────────────────────────
|
||||
if (!_ok) {
|
||||
display(html`<p class="dim">API offline — run <code>make api</code> from state-hub/.</p>`);
|
||||
} else if (domains.length === 0) {
|
||||
display(html`<p class="dim">No active domains found.</p>`);
|
||||
} else {
|
||||
// Domains with active goal first, then those without
|
||||
const sorted = [
|
||||
...domainsWithActiveGoal.sort((a, b) => a.slug.localeCompare(b.slug)),
|
||||
...domainsWithoutGoal.sort((a, b) => a.slug.localeCompare(b.slug)),
|
||||
];
|
||||
display(html`<div class="goals-root">${sorted.map(renderDomainSection)}</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// ── Unlinked active repo goals ─────────────────────────────────────────────────
|
||||
if (unlinkedRepoGoals.length > 0) {
|
||||
display(html`
|
||||
<hr/>
|
||||
<h2>Unlinked Repository Goals</h2>
|
||||
<p class="dim" style="margin-bottom:1rem">Active repo goals not yet associated with a domain goal.</p>
|
||||
<div class="rg-list-unlinked">${unlinkedRepoGoals.map(renderRepoGoalCard)}</div>
|
||||
`);
|
||||
}
|
||||
```
|
||||
|
||||
<style>
|
||||
/* ── Live indicator ───────────────────────────────────────────────────────── */
|
||||
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
|
||||
|
||||
/* ── KPI infobox ──────────────────────────────────────────────────────────── */
|
||||
.kpi-infobox { background: var(--theme-background-alt, #f9f9f9); border: 1px solid var(--theme-foreground-faint, #e0e0e0); border-radius: 10px; padding: 0.75rem 1rem; position: relative; box-shadow: 0 1px 6px rgba(0,0,0,0.07); margin-bottom: 1.25rem; }
|
||||
.kpi-infobox-title { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--theme-foreground-muted, #888); margin-bottom: 0.55rem; }
|
||||
.kpi-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.3rem 0; }
|
||||
.kpi-row + .kpi-row { border-top: 1px solid var(--theme-foreground-faint, #eee); }
|
||||
.kpi-row-label { font-size: 0.8rem; color: var(--theme-foreground-muted, #666); white-space: nowrap; }
|
||||
.kpi-row-right { text-align: right; }
|
||||
.kpi-row-value { font-size: 1.25rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1.1; }
|
||||
.kpi-block { }
|
||||
.kpi-slug-list { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.kpi-slug-item { font-family: monospace; font-size: 0.78rem; color: #b45309; }
|
||||
|
||||
/* ── Layout ───────────────────────────────────────────────────────────────── */
|
||||
.goals-root { display: flex; flex-direction: column; gap: 2rem; }
|
||||
|
||||
/* ── Domain section ───────────────────────────────────────────────────────── */
|
||||
.domain-section { border: 1px solid var(--theme-foreground-faint, #e0e0e0); border-radius: 12px; overflow: hidden; }
|
||||
.domain-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.65rem 1.1rem; background: var(--theme-background-alt); border-bottom: 1px solid var(--theme-foreground-faint, #e0e0e0); }
|
||||
.domain-chip { font-family: monospace; font-size: 0.8rem; background: var(--theme-background); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 4px; padding: 0.1rem 0.5rem; color: var(--theme-foreground-muted); }
|
||||
.domain-name { font-size: 0.95rem; font-weight: 700; }
|
||||
|
||||
/* ── Domain goal card ─────────────────────────────────────────────────────── */
|
||||
.domain-goal-card { padding: 1rem 1.2rem; }
|
||||
.dg-active { border-left: 4px solid #22c55e; background: var(--theme-background); }
|
||||
.dg-secondary { border-left: 4px solid #94a3b8; background: var(--theme-background-alt); opacity: 0.85; }
|
||||
.dg-empty { padding: 0.8rem 1.2rem; color: var(--theme-foreground-muted); font-style: italic; font-size: 0.85rem; }
|
||||
.dg-no-goal-label { }
|
||||
|
||||
.dg-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.4rem; }
|
||||
.dg-level-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--theme-foreground-muted); background: var(--theme-background-alt); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 4px; padding: 0.1rem 0.4rem; }
|
||||
.dg-title { font-size: 1.05rem; font-weight: 700; margin-bottom: 0.35rem; line-height: 1.3; }
|
||||
.dg-desc { font-size: 0.85rem; color: var(--theme-foreground-muted); line-height: 1.5; margin-bottom: 0.5rem; }
|
||||
.dg-meta { font-size: 0.72rem; color: var(--theme-foreground-faint); }
|
||||
|
||||
/* ── Status badges ────────────────────────────────────────────────────────── */
|
||||
.status-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 8px; font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.badge-active { background: #dcfce7; color: #166534; }
|
||||
.badge-superseded { background: #f3e8ff; color: #6b21a8; }
|
||||
.badge-archived { background: #f1f5f9; color: #475569; }
|
||||
.badge-paused { background: #fef3c7; color: #92400e; }
|
||||
.badge-completed { background: #dcfce7; color: #166534; }
|
||||
|
||||
/* ── Repo goal section ────────────────────────────────────────────────────── */
|
||||
.rg-section { border-top: 1px dashed var(--theme-foreground-faint, #ddd); margin-top: 0.8rem; padding-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.rg-section-secondary { opacity: 0.85; }
|
||||
.rg-section-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--theme-foreground-muted); margin-bottom: 0.35rem; }
|
||||
.rg-empty { font-size: 0.8rem; font-style: italic; color: var(--theme-foreground-faint); }
|
||||
|
||||
/* ── Repo goal card ───────────────────────────────────────────────────────── */
|
||||
.repo-goal-card { border-left: 3px solid #3b82f6; border-radius: 0 6px 6px 0; background: var(--theme-background-alt); padding: 0.55rem 0.9rem; }
|
||||
.rg-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.3rem; font-size: 0.75rem; }
|
||||
.rg-priority { font-weight: 700; color: var(--theme-foreground-muted); font-family: monospace; min-width: 2rem; }
|
||||
.rg-repo { font-family: monospace; font-size: 0.75rem; background: var(--theme-background); border: 1px solid var(--theme-foreground-faint, #ddd); border-radius: 3px; padding: 0.05rem 0.35rem; color: var(--theme-foreground-muted); }
|
||||
.rg-title { font-size: 0.9rem; font-weight: 700; margin-bottom: 0.25rem; }
|
||||
.rg-desc { font-size: 0.8rem; color: var(--theme-foreground-muted); line-height: 1.45; margin-bottom: 0.3rem; }
|
||||
.rg-meta { font-size: 0.7rem; color: var(--theme-foreground-faint); }
|
||||
|
||||
/* ── Secondary goals accordion ───────────────────────────────────────────── */
|
||||
.secondary-goals-details { border-top: 1px solid var(--theme-foreground-faint, #eee); }
|
||||
.secondary-goals-summary { padding: 0.6rem 1.2rem; font-size: 0.8rem; color: var(--theme-foreground-muted); cursor: pointer; list-style: none; user-select: none; }
|
||||
.secondary-goals-summary::-webkit-details-marker { display: none; }
|
||||
.secondary-goals-summary::before { content: "▶ "; font-size: 0.65rem; }
|
||||
details[open] .secondary-goals-summary::before { content: "▼ "; }
|
||||
.secondary-goals-list { padding: 0 0 0.5rem 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
|
||||
/* ── Unlinked repo goals ──────────────────────────────────────────────────── */
|
||||
.rg-list-unlinked { display: flex; flex-direction: column; gap: 0.5rem; max-width: 720px; }
|
||||
|
||||
/* ── Utility ─────────────────────────────────────────────────────────────── */
|
||||
.dim { color: gray; font-style: italic; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user