@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.
Files changed (67) hide show
  1. package/cli/install.js +176 -13
  2. package/cli/lib/config.cjs +4 -2
  3. package/cli/lib/fsutil.cjs +13 -2
  4. package/cli/lib/homedir.cjs +21 -0
  5. package/cli/lib/schemas.cjs +6 -1
  6. package/cli/nuke.js +13 -8
  7. package/cli/postinstall.js +14 -4
  8. package/cli/rcode-slash-router.cjs +118 -0
  9. package/cli/uninstall.js +59 -1
  10. package/cli/update.js +10 -5
  11. package/dist/rcode.js +234 -230
  12. package/package.json +1 -1
  13. package/server/dashboard.js +26 -7
  14. package/server/lib/api.js +62 -4
  15. package/server/lib/html/client/agents-data.js +22 -18
  16. package/server/lib/html/client/app.js +3 -0
  17. package/server/lib/html/client/components/AgentCard.js +127 -0
  18. package/server/lib/html/client/components/App.js +104 -39
  19. package/server/lib/html/client/components/CommandPalette.js +133 -0
  20. package/server/lib/html/client/components/FileReader.js +116 -0
  21. package/server/lib/html/client/components/FilterChips.js +94 -0
  22. package/server/lib/html/client/components/NotifyCenter.js +117 -0
  23. package/server/lib/html/client/components/OrchPanel.js +80 -52
  24. package/server/lib/html/client/components/PhaseGraph.js +300 -0
  25. package/server/lib/html/client/components/RejectDialog.js +78 -0
  26. package/server/lib/html/client/components/RunnerPicker.js +190 -0
  27. package/server/lib/html/client/components/Sidebar.js +106 -61
  28. package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
  29. package/server/lib/html/client/components/TaskPipeline.js +83 -0
  30. package/server/lib/html/client/components/Topbar.js +86 -39
  31. package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
  32. package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
  33. package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
  34. package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
  35. package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
  36. package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
  37. package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
  38. package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
  39. package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
  40. package/server/lib/html/client/components/shared.js +47 -11
  41. package/server/lib/html/client/filter-state.js +72 -0
  42. package/server/lib/html/client/icons-client.js +7 -0
  43. package/server/lib/html/client/notify.js +75 -0
  44. package/server/lib/html/client/orchestrator.js +168 -41
  45. package/server/lib/html/client/preact.js +13 -8
  46. package/server/lib/html/client/store.js +70 -6
  47. package/server/lib/html/client/util.js +78 -0
  48. package/server/lib/html/client/vendor/htm.js +1 -0
  49. package/server/lib/html/client/vendor/preact-hooks.js +2 -0
  50. package/server/lib/html/client/vendor/preact.js +2 -0
  51. package/server/lib/html/client/views/AgentsView.js +144 -51
  52. package/server/lib/html/client/views/FilesView.js +20 -103
  53. package/server/lib/html/client/views/KanbanView.js +40 -21
  54. package/server/lib/html/client/views/MemoryView.js +26 -9
  55. package/server/lib/html/client/views/MilestonesView.js +4 -4
  56. package/server/lib/html/client/views/OrchestrationView.js +154 -19
  57. package/server/lib/html/client/views/OverviewView.js +47 -239
  58. package/server/lib/html/client/views/PhasesView.js +50 -6
  59. package/server/lib/html/client/views/RoadmapView.js +6 -3
  60. package/server/lib/html/client/views/SprintsView.js +50 -6
  61. package/server/lib/html/client/views/TasksView.js +4 -3
  62. package/server/lib/html/client.js +21 -4
  63. package/server/lib/html/css.js +2761 -8
  64. package/server/lib/html/icons.js +7 -0
  65. package/server/lib/html/shell.js +10 -3
  66. package/server/lib/scanner.js +376 -39
  67. package/server/orchestrator.js +329 -5
@@ -1,86 +1,131 @@
1
1
  /**
2
- * Sidebar component — project label, nav sections, 12 nav-link buttons.
2
+ * Sidebar component — redesigned chrome to match the mockup.
3
3
  *
4
- * Reuses existing CSS classes from css.js: sidebar, nav-section, nav-link,
5
- * data-view, active. Emoji replaced with SVG icons from icons-client.js.
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 { allSprints, allTasks } from '../util.js';
12
- import { AGENTS } from '../agents-data.js';
23
+ import { ProjectHealth } from './dashboard/ProjectHealth.js';
13
24
 
14
- // Nav structure: [ { section, links: [ { view, icon, label } ] } ]
15
- const NAV_SECTIONS = [
16
- {
17
- section: 'Overview',
18
- links: [
19
- { view: 'overview', icon: 'home', label: 'Overview' },
20
- { view: 'orchestration', icon: 'activity', label: 'Orchestration' },
21
- { view: 'roadmap', icon: 'map', label: 'Roadmap' },
22
- ],
23
- },
24
- {
25
- section: 'Planning',
26
- links: [
27
- { view: 'milestones', icon: 'target', label: 'Milestones' },
28
- { view: 'phases', icon: 'layers', label: 'Phases' },
29
- { view: 'sprints', icon: 'zap', label: 'Sprints' },
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} — displayed under the "rcode" label
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
- const S = useStore();
54
- const counts = {
55
- phases: (S.phases || []).length,
56
- sprints: allSprints(S.phases).length,
57
- tasks: allTasks(S.phases).length,
58
- decisions: (S.decisions || []).length,
59
- agents: AGENTS.length,
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="sidebar-project">
65
- <div class="project-label">rcode</div>
66
- <span>${projectName || ''}</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
- <nav>
69
- ${NAV_SECTIONS.map(({ section, links }) => html`
70
- <div class="nav-section">${section}</div>
71
- ${links.map(({ view, icon, label }) => html`
72
- <button
73
- class=${'nav-link' + (activeView === view ? ' active' : '')}
74
- data-view=${view}
75
- onClick=${() => { location.hash = view; }}
76
- >
77
- <${Icon} name=${icon} size=${14} />
78
- ${' ' + label}
79
- ${counts[view] ? html`<span class="nav-count">${counts[view]}</span>` : null}
80
- </button>
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 — brand, live dot, updated-ago, action buttons.
2
+ * Topbar component — redesigned header chrome to match the mockup.
3
3
  *
4
- * Reuses existing CSS classes: header-actions, header-btn, live, hamburger-btn.
4
+ * Public API is UNCHANGED App.js still calls:
5
+ * <${Topbar} projectName updatedAgo refreshing onRefresh
6
+ * onToggleTheme onToggleSidebar themeLabel />
5
7
  *
6
- * Props:
7
- * projectName {string} — shown in the brand subtitle
8
- * updatedAgo {string} text for the "updated N ago" span
9
- * onRefresh {function} called when Refresh button is clicked
10
- * onToggleTheme {function} called when theme button is clicked
11
- * onToggleSidebar {function} called when hamburger is clicked
12
- * themeLabel {string} — 'light' or 'dark' — controls which icon the theme button shows
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
- <div class="topbar-start-group">
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="hamburger-btn"
24
- id="hamburger-btn"
25
- onClick=${onToggleSidebar}
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><span></span><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
+ }