@hanzlaa/rcode 4.1.2 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/install.js +176 -13
- package/cli/lib/config.cjs +4 -2
- package/cli/lib/fsutil.cjs +13 -2
- package/cli/lib/homedir.cjs +21 -0
- package/cli/lib/schemas.cjs +6 -1
- package/cli/nuke.js +13 -8
- package/cli/postinstall.js +14 -4
- package/cli/rcode-slash-router.cjs +118 -0
- package/cli/uninstall.js +59 -1
- package/cli/update.js +10 -5
- package/dist/rcode.js +234 -230
- package/package.json +1 -1
- package/server/dashboard.js +26 -7
- package/server/lib/api.js +62 -4
- package/server/lib/html/client/agents-data.js +22 -18
- package/server/lib/html/client/app.js +3 -0
- package/server/lib/html/client/components/AgentCard.js +127 -0
- package/server/lib/html/client/components/App.js +104 -39
- package/server/lib/html/client/components/CommandPalette.js +133 -0
- package/server/lib/html/client/components/FileReader.js +116 -0
- package/server/lib/html/client/components/FilterChips.js +94 -0
- package/server/lib/html/client/components/NotifyCenter.js +117 -0
- package/server/lib/html/client/components/OrchPanel.js +80 -52
- package/server/lib/html/client/components/PhaseGraph.js +300 -0
- package/server/lib/html/client/components/RejectDialog.js +78 -0
- package/server/lib/html/client/components/RunnerPicker.js +190 -0
- package/server/lib/html/client/components/Sidebar.js +106 -61
- package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
- package/server/lib/html/client/components/TaskPipeline.js +83 -0
- package/server/lib/html/client/components/Topbar.js +86 -39
- package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
- package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
- package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
- package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
- package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
- package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
- package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
- package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
- package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
- package/server/lib/html/client/components/shared.js +47 -11
- package/server/lib/html/client/filter-state.js +72 -0
- package/server/lib/html/client/icons-client.js +7 -0
- package/server/lib/html/client/notify.js +75 -0
- package/server/lib/html/client/orchestrator.js +168 -41
- package/server/lib/html/client/preact.js +13 -8
- package/server/lib/html/client/store.js +70 -6
- package/server/lib/html/client/util.js +78 -0
- package/server/lib/html/client/vendor/htm.js +1 -0
- package/server/lib/html/client/vendor/preact-hooks.js +2 -0
- package/server/lib/html/client/vendor/preact.js +2 -0
- package/server/lib/html/client/views/AgentsView.js +144 -51
- package/server/lib/html/client/views/FilesView.js +20 -103
- package/server/lib/html/client/views/KanbanView.js +40 -21
- package/server/lib/html/client/views/MemoryView.js +26 -9
- package/server/lib/html/client/views/MilestonesView.js +4 -4
- package/server/lib/html/client/views/OrchestrationView.js +154 -19
- package/server/lib/html/client/views/OverviewView.js +47 -239
- package/server/lib/html/client/views/PhasesView.js +50 -6
- package/server/lib/html/client/views/RoadmapView.js +6 -3
- package/server/lib/html/client/views/SprintsView.js +50 -6
- package/server/lib/html/client/views/TasksView.js +4 -3
- package/server/lib/html/client.js +21 -4
- package/server/lib/html/css.js +2761 -8
- package/server/lib/html/icons.js +7 -0
- package/server/lib/html/shell.js +10 -3
- package/server/lib/scanner.js +376 -39
- package/server/orchestrator.js +329 -5
|
@@ -1,86 +1,131 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sidebar component —
|
|
2
|
+
* Sidebar component — redesigned chrome to match the mockup.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Public API is UNCHANGED — App.js still calls:
|
|
5
|
+
* <${Sidebar} activeView=${view} projectName=${storeState.projectName} />
|
|
6
|
+
*
|
|
7
|
+
* Layout (top → bottom):
|
|
8
|
+
* 1. rcode logo badge
|
|
9
|
+
* 2. project switcher (shows project.name)
|
|
10
|
+
* 3. vertical nav with per-item icons (Overview active by default)
|
|
11
|
+
* 4. Project Health mini-card (ProjectHealth.js)
|
|
12
|
+
* 5. user profile footer (avatar initials + name + email)
|
|
13
|
+
*
|
|
14
|
+
* Reads `project { name, user { name, email } }` from the store; falls back to
|
|
15
|
+
* the projectName prop. No sample data — when no user is configured the
|
|
16
|
+
* profile footer is hidden, and a missing project name shows "No project".
|
|
17
|
+
* No inline style= attributes — all styling via .sb-* classes in css.js.
|
|
6
18
|
*/
|
|
7
19
|
|
|
8
20
|
import { html } from '../preact.js';
|
|
9
21
|
import { Icon } from '../icons-client.js';
|
|
10
22
|
import { useStore } from '../store.js';
|
|
11
|
-
import {
|
|
12
|
-
import { AGENTS } from '../agents-data.js';
|
|
23
|
+
import { ProjectHealth } from './dashboard/ProjectHealth.js';
|
|
13
24
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ view: 'tasks', icon: 'checkSquare', label: 'Tasks' },
|
|
31
|
-
{ view: 'kanban', icon: 'kanban', label: 'Kanban' },
|
|
32
|
-
],
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
section: 'Workspace',
|
|
36
|
-
links: [
|
|
37
|
-
{ view: 'files', icon: 'file', label: 'Files' },
|
|
38
|
-
{ view: 'agents', icon: 'users', label: 'Agents' },
|
|
39
|
-
{ view: 'decisions', icon: 'scale', label: 'Decisions' },
|
|
40
|
-
{ view: 'memory', icon: 'database', label: 'Memory' },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
25
|
+
// Single flat nav — one entry per real Preact view (PREACT_VIEWS in App.js).
|
|
26
|
+
// Keep this list in sync with App.js: a view key absent there silently
|
|
27
|
+
// falls back to Overview, so never add a link without a matching view.
|
|
28
|
+
const NAV_LINKS = [
|
|
29
|
+
{ view: 'overview', icon: 'home', label: 'Overview' },
|
|
30
|
+
{ view: 'roadmap', icon: 'map', label: 'Roadmap' },
|
|
31
|
+
{ view: 'milestones', icon: 'flag', label: 'Milestones' },
|
|
32
|
+
{ view: 'phases', icon: 'layers', label: 'Phases' },
|
|
33
|
+
{ view: 'sprints', icon: 'zap', label: 'Sprints' },
|
|
34
|
+
{ view: 'tasks', icon: 'checkSquare', label: 'Tasks' },
|
|
35
|
+
{ view: 'kanban', icon: 'kanban', label: 'Kanban' },
|
|
36
|
+
{ view: 'decisions', icon: 'scale', label: 'Decisions' },
|
|
37
|
+
{ view: 'files', icon: 'file-text', label: 'Files' },
|
|
38
|
+
{ view: 'agents', icon: 'users', label: 'Agents' },
|
|
39
|
+
{ view: 'memory', icon: 'brain', label: 'Memory' },
|
|
40
|
+
{ view: 'orchestration', icon: 'terminal', label: 'Orchestration' },
|
|
43
41
|
];
|
|
44
42
|
|
|
43
|
+
/** Two-letter initials from a display name. */
|
|
44
|
+
function initials(name) {
|
|
45
|
+
const parts = String(name || '').trim().split(/\s+/).filter(Boolean);
|
|
46
|
+
if (!parts.length) return '?';
|
|
47
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
48
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
/**
|
|
46
52
|
* Sidebar component.
|
|
47
53
|
*
|
|
48
54
|
* Props:
|
|
49
|
-
* activeView {string} — currently active view key
|
|
50
|
-
* projectName {string} —
|
|
55
|
+
* activeView {string} — currently active view key (drives nav highlight)
|
|
56
|
+
* projectName {string} — fallback project name when store has no project slice
|
|
51
57
|
*/
|
|
52
58
|
export function Sidebar({ activeView, projectName }) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// Slice subscription — Sidebar only re-renders when the project slice
|
|
60
|
+
// changes, not on every refreshing/sessions tick.
|
|
61
|
+
const project = useStore(s => s.project) || {};
|
|
62
|
+
const name = project.name || projectName || 'No project';
|
|
63
|
+
const user = (project.user && project.user.name) ? project.user : null;
|
|
64
|
+
|
|
65
|
+
// Full store subscription for live health badge counts.
|
|
66
|
+
// Re-renders on every setState (sessions poll every 4 s, state refresh every 30 s).
|
|
67
|
+
const { activeSessions, blockers } = useStore();
|
|
68
|
+
const sessionCount = (activeSessions || []).filter(s => s.status === 'running').length;
|
|
69
|
+
const blockerCount = (blockers || []).length;
|
|
61
70
|
|
|
62
71
|
return html`
|
|
63
72
|
<aside class="sidebar" id="sidebar">
|
|
64
|
-
<div class="
|
|
65
|
-
<
|
|
66
|
-
<span
|
|
73
|
+
<div class="sb-logo">
|
|
74
|
+
<span class="sb-logo-badge">r</span>
|
|
75
|
+
<span class="sb-logo-word">rcode</span>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Visually inert: the server scans exactly one project, so there is
|
|
79
|
+
no switcher menu. Rendered as a plain label, no chevron/affordance. -->
|
|
80
|
+
<div class="sb-switcher sb-switcher--static" title=${name}>
|
|
81
|
+
<span class="sb-switcher-dot"></span>
|
|
82
|
+
<span class="sb-switcher-name">${name}</span>
|
|
67
83
|
</div>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
|
|
85
|
+
<div class="sidebar-health">
|
|
86
|
+
<span
|
|
87
|
+
class=${'health-badge' + (sessionCount === 0 ? ' health-badge--zero' : '')}
|
|
88
|
+
title=${sessionCount + ' active orchestration session' + (sessionCount === 1 ? '' : 's')}
|
|
89
|
+
>
|
|
90
|
+
<${Icon} name="activity" size=${12} />
|
|
91
|
+
${sessionCount} active
|
|
92
|
+
</span>
|
|
93
|
+
<span
|
|
94
|
+
class=${'health-badge' + (blockerCount > 0 ? ' health-badge--alert' : ' health-badge--zero')}
|
|
95
|
+
title=${blockerCount + ' blocker' + (blockerCount === 1 ? '' : 's')}
|
|
96
|
+
>
|
|
97
|
+
<${Icon} name="alert-triangle" size=${12} />
|
|
98
|
+
${blockerCount} blocked
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<nav class="sb-nav">
|
|
103
|
+
${NAV_LINKS.map(({ view, icon, label }) => html`
|
|
104
|
+
<button
|
|
105
|
+
class=${'sb-nav-link' + (activeView === view ? ' active' : '')}
|
|
106
|
+
data-view=${view}
|
|
107
|
+
aria-current=${activeView === view ? 'page' : undefined}
|
|
108
|
+
onClick=${() => { location.hash = view; }}
|
|
109
|
+
>
|
|
110
|
+
<span class="sb-nav-ic"><${Icon} name=${icon} size=${16} /></span>
|
|
111
|
+
<span class="sb-nav-label">${label}</span>
|
|
112
|
+
</button>
|
|
82
113
|
`)}
|
|
83
114
|
</nav>
|
|
115
|
+
|
|
116
|
+
<div class="sb-health">
|
|
117
|
+
<${ProjectHealth} />
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
${user ? html`
|
|
121
|
+
<div class="sb-profile">
|
|
122
|
+
<span class="sb-avatar">${initials(user.name)}</span>
|
|
123
|
+
<span class="sb-profile-meta">
|
|
124
|
+
<span class="sb-profile-name">${user.name}</span>
|
|
125
|
+
<span class="sb-profile-email">${user.email || ''}</span>
|
|
126
|
+
</span>
|
|
127
|
+
</div>
|
|
128
|
+
` : null}
|
|
84
129
|
</aside>
|
|
85
130
|
`;
|
|
86
131
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusSummaryBar — aggregate count chips for phases, sprints, and sessions
|
|
3
|
+
* grouped by status. Reads from the shared store; renders a flex row of
|
|
4
|
+
* labelled count chips.
|
|
5
|
+
*
|
|
6
|
+
* No props required — all data comes from useStore().
|
|
7
|
+
* Rendered above views in 34.2 once wired into App.js.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { html } from '../preact.js';
|
|
11
|
+
import { useStore } from '../store.js';
|
|
12
|
+
import { allSprints, chip } from '../util.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build a `{ [cls]: count }` map from an array of items by normalising each
|
|
16
|
+
* item's status through `chip()` and incrementing the corresponding cls bucket.
|
|
17
|
+
*
|
|
18
|
+
* @param {Array<{ status: string }>} items
|
|
19
|
+
* @returns {Object.<string, number>}
|
|
20
|
+
*/
|
|
21
|
+
function countByStatus(items) {
|
|
22
|
+
const map = {};
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
const { cls } = chip(item.status || '');
|
|
25
|
+
map[cls] = (map[cls] || 0) + 1;
|
|
26
|
+
}
|
|
27
|
+
return map;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render a single group of count chips.
|
|
32
|
+
*
|
|
33
|
+
* @param {{ label: string, counts: Object.<string, number> }} props
|
|
34
|
+
*/
|
|
35
|
+
function SummaryGroup({ label, counts }) {
|
|
36
|
+
const entries = Object.entries(counts).filter(([, n]) => n > 0);
|
|
37
|
+
if (entries.length === 0) return null;
|
|
38
|
+
return html`
|
|
39
|
+
<div class="summary-group">
|
|
40
|
+
<span class="summary-group-label">${label}</span>
|
|
41
|
+
${entries.map(([cls, count]) => html`
|
|
42
|
+
<span class=${'summary-count-chip ' + cls}>${count} ${cls}</span>
|
|
43
|
+
`)}
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* StatusSummaryBar — row of count chips for phases, sprints, and sessions.
|
|
50
|
+
* Suppresses a group entirely when its source array is empty.
|
|
51
|
+
*/
|
|
52
|
+
export function StatusSummaryBar() {
|
|
53
|
+
const S = useStore();
|
|
54
|
+
|
|
55
|
+
const phases = S.phases || [];
|
|
56
|
+
const sprints = allSprints(phases);
|
|
57
|
+
const sessions = S.activeSessions || [];
|
|
58
|
+
|
|
59
|
+
const phaseCounts = countByStatus(phases);
|
|
60
|
+
const sprintCounts = countByStatus(sprints);
|
|
61
|
+
const sessionCounts = countByStatus(sessions);
|
|
62
|
+
|
|
63
|
+
const hasPhases = phases.length > 0;
|
|
64
|
+
const hasSprints = sprints.length > 0;
|
|
65
|
+
const hasSessions = sessions.length > 0;
|
|
66
|
+
|
|
67
|
+
if (!hasPhases && !hasSprints && !hasSessions) return null;
|
|
68
|
+
|
|
69
|
+
return html`
|
|
70
|
+
<div class="summary-bar">
|
|
71
|
+
${hasPhases ? html`<${SummaryGroup} label="Phases" counts=${phaseCounts} />` : null}
|
|
72
|
+
${hasSprints ? html`<${SummaryGroup} label="Sprints" counts=${sprintCounts} />` : null}
|
|
73
|
+
${hasSessions ? html`<${SummaryGroup} label="Sessions" counts=${sessionCounts} />` : null}
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskPipeline — compact horizontal stage stepper for a single task.
|
|
3
|
+
*
|
|
4
|
+
* Stages derive from the rcode story lifecycle as seen by the scanner
|
|
5
|
+
* (todo/planned → in_progress/active → review/verify → done/completed):
|
|
6
|
+
* Planned → In Progress → Review → Done.
|
|
7
|
+
* "blocked" is not a stage — it pins the stepper at In Progress and adds
|
|
8
|
+
* a red "Blocked" badge (and a red ring on the current node).
|
|
9
|
+
*
|
|
10
|
+
* Tolerant of sparse data: a bare status string is enough; overview
|
|
11
|
+
* `tasks.inProgress` rows have only { title, pct }, so pct is used as a
|
|
12
|
+
* fallback. Unknown/missing status renders as Planned — never crashes.
|
|
13
|
+
*
|
|
14
|
+
* Props:
|
|
15
|
+
* task — { status?, pct?, title? } (anything else ignored)
|
|
16
|
+
* mini — smaller variant for overview card rows
|
|
17
|
+
* running — a live orchestrator session exists for this task; pins the
|
|
18
|
+
* stepper at In Progress (unless further along) and pulses the
|
|
19
|
+
* current node so live work is visible at a glance
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { html } from '../preact.js';
|
|
23
|
+
|
|
24
|
+
const STAGES = ['Planned', 'In Progress', 'Review', 'Done'];
|
|
25
|
+
|
|
26
|
+
/** Current stage index (0–3) for a task; 3 means fully done. */
|
|
27
|
+
export function taskStageIndex(task) {
|
|
28
|
+
const s = String((task && task.status) || '').toLowerCase();
|
|
29
|
+
if (/done|complete|shipped/.test(s)) return 3;
|
|
30
|
+
if (/review|verif|uat/.test(s)) return 2;
|
|
31
|
+
if (/active|progress|running|blocked/.test(s)) return 1;
|
|
32
|
+
if (!s && task && Number.isFinite(task.pct)) {
|
|
33
|
+
return task.pct >= 100 ? 3 : task.pct > 0 ? 1 : 0;
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function TaskPipeline({ task, mini, running }) {
|
|
39
|
+
const t = task || {};
|
|
40
|
+
// A live session means work is happening NOW — never show it as Planned,
|
|
41
|
+
// but don't demote a task already at Review/Done.
|
|
42
|
+
const cur = running ? Math.max(taskStageIndex(t), 1) : taskStageIndex(t);
|
|
43
|
+
const blocked = /blocked/i.test(String(t.status || ''));
|
|
44
|
+
// Number of fully-completed stages. When the task is done the Done node
|
|
45
|
+
// itself is filled, so all four count as complete.
|
|
46
|
+
const doneCount = cur === 3 ? 4 : cur;
|
|
47
|
+
|
|
48
|
+
const parts = [];
|
|
49
|
+
STAGES.forEach((label, i) => {
|
|
50
|
+
if (i > 0) {
|
|
51
|
+
const lineDone = i <= doneCount;
|
|
52
|
+
parts.push(html`
|
|
53
|
+
<span key=${'l' + i} class=${'tpipe-line' + (lineDone ? ' tpipe-line--done' : '')}></span>
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
const isDone = i < doneCount;
|
|
57
|
+
const isCurrent = !isDone && i === cur;
|
|
58
|
+
let cls = 'tpipe-node';
|
|
59
|
+
let state = 'upcoming';
|
|
60
|
+
if (isDone) { cls += ' tpipe-node--done'; state = 'complete'; }
|
|
61
|
+
else if (isCurrent) {
|
|
62
|
+
cls += blocked ? ' tpipe-node--current tpipe-node--blocked' : ' tpipe-node--current';
|
|
63
|
+
if (running && !blocked && cur < 3) cls += ' tpipe-node--live';
|
|
64
|
+
state = blocked ? 'blocked' : 'current';
|
|
65
|
+
}
|
|
66
|
+
parts.push(html`
|
|
67
|
+
<span key=${'n' + i} class=${cls} title=${label + ' · ' + state}>
|
|
68
|
+
${isDone ? '✓' : ''}
|
|
69
|
+
</span>
|
|
70
|
+
`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const summary = blocked
|
|
74
|
+
? 'Pipeline: blocked at ' + STAGES[cur]
|
|
75
|
+
: 'Pipeline: ' + (cur === 3 ? 'Done' : STAGES[cur]) + ' (stage ' + (Math.min(cur, 3) + 1) + ' of 4)';
|
|
76
|
+
|
|
77
|
+
return html`
|
|
78
|
+
<span class=${'tpipe' + (mini ? ' tpipe--mini' : '')} role="img" aria-label=${summary}>
|
|
79
|
+
${parts}
|
|
80
|
+
${blocked ? html`<span class="tpipe-blocked">Blocked</span>` : null}
|
|
81
|
+
</span>
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
@@ -1,55 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Topbar component —
|
|
2
|
+
* Topbar component — redesigned header chrome to match the mockup.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Public API is UNCHANGED — App.js still calls:
|
|
5
|
+
* <${Topbar} projectName updatedAgo refreshing onRefresh
|
|
6
|
+
* onToggleTheme onToggleSidebar themeLabel />
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Layout:
|
|
9
|
+
* - hamburger (mobile sidebar toggle)
|
|
10
|
+
* - greeting: "Welcome back, {user.name}! 👋" + subtitle with project name
|
|
11
|
+
* - right group: "Auto-synced {ago}" status dot (click = refresh),
|
|
12
|
+
* [Ask rcode] (primary, runs /rcode-next via orchestrator), [Share]
|
|
13
|
+
* (copy URL + toast), [...] (more / theme toggle)
|
|
14
|
+
*
|
|
15
|
+
* Reads `project { name, user { name } }` from the store; falls back to the
|
|
16
|
+
* projectName prop. No sample data — without a configured user the greeting
|
|
17
|
+
* is generic, and without a project name the subtitle stays generic too.
|
|
18
|
+
* No inline style= attributes — all styling via .tb-* classes in css.js.
|
|
13
19
|
*/
|
|
14
20
|
|
|
15
21
|
import { html } from '../preact.js';
|
|
16
22
|
import { Icon } from '../icons-client.js';
|
|
23
|
+
import { useStore } from '../store.js';
|
|
24
|
+
import { runCommandFromUI } from '../orchestrator.js';
|
|
25
|
+
import { showToast } from './shared.js';
|
|
26
|
+
import { BlockedBell } from './NotifyCenter.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ask rcode — reuse the existing orchestrator command runner (token-guarded
|
|
30
|
+
* POST /api/run via window.__ORCH_TOKEN__). "/rcode-next" asks rcode for the
|
|
31
|
+
* suggested next action and streams it into the terminal panel. No new endpoint.
|
|
32
|
+
*/
|
|
33
|
+
function askRcode() {
|
|
34
|
+
runCommandFromUI('/rcode-next');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Share — copy the dashboard URL to the clipboard and confirm via toast. */
|
|
38
|
+
function shareDashboard() {
|
|
39
|
+
const url = location.href;
|
|
40
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
41
|
+
navigator.clipboard.writeText(url)
|
|
42
|
+
.then(() => showToast('Dashboard link copied'))
|
|
43
|
+
.catch(() => showToast(url));
|
|
44
|
+
} else {
|
|
45
|
+
showToast(url);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
17
48
|
|
|
18
49
|
export function Topbar({ projectName, updatedAgo, refreshing, onRefresh, onToggleTheme, onToggleSidebar, themeLabel }) {
|
|
50
|
+
const S = useStore();
|
|
51
|
+
const project = (S && S.project) || {};
|
|
52
|
+
const name = project.name || projectName || '';
|
|
53
|
+
const firstName = (project.user && project.user.name) || '';
|
|
54
|
+
|
|
19
55
|
return html`
|
|
20
|
-
<header>
|
|
21
|
-
<
|
|
56
|
+
<header class="topbar">
|
|
57
|
+
<button
|
|
58
|
+
class="hamburger-btn"
|
|
59
|
+
id="hamburger-btn"
|
|
60
|
+
onClick=${onToggleSidebar}
|
|
61
|
+
aria-label="Toggle menu"
|
|
62
|
+
>
|
|
63
|
+
<span></span><span></span><span></span>
|
|
64
|
+
</button>
|
|
65
|
+
|
|
66
|
+
<div class="tb-greeting">
|
|
67
|
+
<h1 class="tb-welcome">${firstName ? 'Welcome back, ' + firstName + '!' : 'Welcome back!'} <span class="tb-wave" aria-hidden="true">👋</span></h1>
|
|
68
|
+
<p class="tb-sub">${name ? "Here's what's happening with " + name : "Here's what's happening with your project"}</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="tb-actions">
|
|
22
72
|
<button
|
|
23
|
-
class
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
aria-label="Toggle menu"
|
|
73
|
+
class=${'tb-synced' + (refreshing ? ' tb-synced--busy' : '')}
|
|
74
|
+
onClick=${onRefresh}
|
|
75
|
+
title="Click to refresh"
|
|
27
76
|
>
|
|
28
|
-
<span></span
|
|
77
|
+
<span class="tb-dot"></span>
|
|
78
|
+
${refreshing ? 'Syncing…' : 'Auto-synced ' + (updatedAgo || 'just now')}
|
|
79
|
+
</button>
|
|
80
|
+
|
|
81
|
+
<${BlockedBell} />
|
|
82
|
+
|
|
83
|
+
<button class="tb-btn tb-btn--primary" type="button" onClick=${askRcode} title="Ask rcode for the next action">
|
|
84
|
+
<${Icon} name="brain" size=${15} /> Ask rcode
|
|
85
|
+
</button>
|
|
86
|
+
|
|
87
|
+
<button class="tb-btn tb-btn--share" type="button" onClick=${shareDashboard} title="Copy dashboard link">
|
|
88
|
+
<${Icon} name="link" size=${15} /> Share
|
|
89
|
+
</button>
|
|
90
|
+
|
|
91
|
+
<button
|
|
92
|
+
class="tb-btn tb-btn--icon"
|
|
93
|
+
type="button"
|
|
94
|
+
onClick=${onToggleTheme}
|
|
95
|
+
title=${'More — switch to ' + (themeLabel === 'light' ? 'dark' : 'light') + ' theme'}
|
|
96
|
+
aria-label="More options"
|
|
97
|
+
>
|
|
98
|
+
<span class="tb-kebab">⋯</span>
|
|
29
99
|
</button>
|
|
30
|
-
<div class="brand">
|
|
31
|
-
<div class="icon"><${Icon} name="building" size=${16} cls="brand-icon"/></div>
|
|
32
|
-
<div>
|
|
33
|
-
<h1>Majlis — The Council</h1>
|
|
34
|
-
<div class="arabic">مجلس · ${projectName || ''}</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
<div class="header-actions">
|
|
39
|
-
<span class="live" id="live-dot" title="Live"
|
|
40
|
-
style=${refreshing ? 'animation-duration:0.7s;background:var(--accent-blue);' : ''}></span>
|
|
41
|
-
<span id="updated-ago" class="updated-ago">
|
|
42
|
-
${refreshing ? '⟳ syncing…' : (updatedAgo || 'just now')}
|
|
43
|
-
</span>
|
|
44
|
-
<button class="header-btn" id="refresh-btn" onClick=${onRefresh}>↺ Refresh</button>
|
|
45
|
-
<!-- icon shows TARGET state (not current): dark→sun means "click to go light"; light→moon means "click to go dark" -->
|
|
46
|
-
<button class="header-btn" id="theme-btn" onClick=${onToggleTheme} title="Toggle theme"><${Icon} name=${themeLabel === 'light' ? 'moon' : 'sun'} size=${14}/></button>
|
|
47
|
-
<button class="header-btn" onClick=${() => {
|
|
48
|
-
navigator.clipboard.writeText(location.href);
|
|
49
|
-
// Show a toast if available
|
|
50
|
-
const toast = document.getElementById('toast');
|
|
51
|
-
if (toast) { toast.textContent = 'URL copied!'; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2500); }
|
|
52
|
-
}} title="Copy URL">⎘ Link</button>
|
|
53
100
|
</div>
|
|
54
101
|
</header>
|
|
55
102
|
`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blockers — Overview redesign, Row 2 Card 3 (Blockers list by severity).
|
|
3
|
+
*
|
|
4
|
+
* Reads `blockers[{ title, desc, severity }]` from the store (severity one of
|
|
5
|
+
* high | medium | low). Each row: warning icon + bold title, a right-aligned
|
|
6
|
+
* severity pill (High = red, Medium = amber, Low = gray), and a muted one-line
|
|
7
|
+
* description below. "View all" sits top-right in the card header.
|
|
8
|
+
* An empty array is the real, good state — rendered as "No blockers 🎉",
|
|
9
|
+
* never substituted with sample data.
|
|
10
|
+
* See .planning/campaign/DATA-CONTRACT.md. Reads store only — no fetch.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { html } from '../../preact.js';
|
|
14
|
+
import { useStore } from '../../store.js';
|
|
15
|
+
import { rowLink } from '../../util.js';
|
|
16
|
+
|
|
17
|
+
// Severity → pill label (lowercase enum to human-facing label).
|
|
18
|
+
const SEV_LABEL = { high: 'High', medium: 'Medium', low: 'Low' };
|
|
19
|
+
|
|
20
|
+
export function Blockers() {
|
|
21
|
+
const S = useStore();
|
|
22
|
+
const blockers = Array.isArray(S.blockers) ? S.blockers : [];
|
|
23
|
+
|
|
24
|
+
return html`
|
|
25
|
+
<section class="dash-card">
|
|
26
|
+
<div class="bk-head">
|
|
27
|
+
<p class="dash-card-title">Blockers</p>
|
|
28
|
+
<button class="bk-viewall" type="button"
|
|
29
|
+
onClick=${() => { location.hash = 'tasks'; }}>View all</button>
|
|
30
|
+
</div>
|
|
31
|
+
${blockers.length === 0
|
|
32
|
+
? html`
|
|
33
|
+
<div class="dash-empty">
|
|
34
|
+
<span class="dash-empty-emoji" aria-hidden="true">🎉</span>
|
|
35
|
+
<span>No blockers</span>
|
|
36
|
+
</div>
|
|
37
|
+
`
|
|
38
|
+
: html`
|
|
39
|
+
<ul class="bk-list">
|
|
40
|
+
${blockers.map((b) => {
|
|
41
|
+
const sev = SEV_LABEL[b.severity] ? b.severity : 'low';
|
|
42
|
+
return html`
|
|
43
|
+
<li class="bk-row ovr-link" key=${b.title} ...${rowLink('tasks')}>
|
|
44
|
+
<span class=${'bk-icon bk-sev-' + sev} aria-hidden="true">⚠</span>
|
|
45
|
+
<div class="bk-body">
|
|
46
|
+
<p class="bk-title">${b.title}</p>
|
|
47
|
+
<p class="bk-desc">${b.desc}</p>
|
|
48
|
+
</div>
|
|
49
|
+
<span class=${'bk-pill bk-sev-' + sev}>${SEV_LABEL[sev]}</span>
|
|
50
|
+
</li>
|
|
51
|
+
`;
|
|
52
|
+
})}
|
|
53
|
+
</ul>
|
|
54
|
+
`}
|
|
55
|
+
</section>
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CompletedTasks — Overview redesign, Row 2 Card 1.
|
|
3
|
+
*
|
|
4
|
+
* "Completed Tasks" card with a "View all" link top-right and a list of rows,
|
|
5
|
+
* each = green check icon + task title + right-aligned date.
|
|
6
|
+
*
|
|
7
|
+
* Reads `tasks.completed[{ title, date }]` from the store. Pure — never fetches.
|
|
8
|
+
* An absent or empty slice renders an honest "No completed tasks yet" state —
|
|
9
|
+
* never sample data. See .planning/campaign/DATA-CONTRACT.md.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { html } from '../../preact.js';
|
|
13
|
+
import { useStore } from '../../store.js';
|
|
14
|
+
import { humanDate } from '../../util.js';
|
|
15
|
+
|
|
16
|
+
export function CompletedTasks() {
|
|
17
|
+
const S = useStore();
|
|
18
|
+
const items = (S.tasks && Array.isArray(S.tasks.completed)) ? S.tasks.completed : [];
|
|
19
|
+
|
|
20
|
+
return html`
|
|
21
|
+
<section class="dash-card ct-card">
|
|
22
|
+
<div class="ct-head">
|
|
23
|
+
<p class="dash-card-title">Completed Tasks</p>
|
|
24
|
+
<button class="ct-viewall" onClick=${() => { location.hash = 'tasks'; }}>
|
|
25
|
+
View all
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
${items.length === 0
|
|
29
|
+
? html`<p class="dash-card-sub">No completed tasks yet</p>`
|
|
30
|
+
: html`
|
|
31
|
+
<ul class="ct-list">
|
|
32
|
+
${items.map((t, i) => html`
|
|
33
|
+
<li class="ct-row" key=${t.title + i}>
|
|
34
|
+
<svg class="ct-check" width="16" height="16" viewBox="0 0 24 24"
|
|
35
|
+
fill="none" stroke="currentColor" stroke-width="2.5"
|
|
36
|
+
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
37
|
+
<polyline points="20 6 9 17 4 12"/>
|
|
38
|
+
</svg>
|
|
39
|
+
<span class="ct-title">${t.title}</span>
|
|
40
|
+
<span class="ct-date">${humanDate(t.date)}</span>
|
|
41
|
+
</li>
|
|
42
|
+
`)}
|
|
43
|
+
</ul>
|
|
44
|
+
`}
|
|
45
|
+
</section>
|
|
46
|
+
`;
|
|
47
|
+
}
|