reviewflow 3.21.0 → 3.22.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 (96) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/dashboard/index.html +258 -44
  3. package/dist/dashboard/modules/animations.d.ts +164 -0
  4. package/dist/dashboard/modules/animations.d.ts.map +1 -0
  5. package/dist/dashboard/modules/animations.js +405 -0
  6. package/dist/dashboard/modules/animations.js.map +1 -0
  7. package/dist/dashboard/modules/cardCounters.d.ts +16 -1
  8. package/dist/dashboard/modules/cardCounters.d.ts.map +1 -1
  9. package/dist/dashboard/modules/cardCounters.js +36 -2
  10. package/dist/dashboard/modules/cardCounters.js.map +1 -1
  11. package/dist/dashboard/modules/i18n.d.ts.map +1 -1
  12. package/dist/dashboard/modules/i18n.js +20 -2
  13. package/dist/dashboard/modules/i18n.js.map +1 -1
  14. package/dist/dashboard/modules/settingsModal.d.ts.map +1 -1
  15. package/dist/dashboard/modules/settingsModal.js +28 -8
  16. package/dist/dashboard/modules/settingsModal.js.map +1 -1
  17. package/dist/dashboard/styles.css +731 -372
  18. package/dist/main/server.d.ts.map +1 -1
  19. package/dist/main/server.js +22 -0
  20. package/dist/main/server.js.map +1 -1
  21. package/dist/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.d.ts +1 -0
  22. package/dist/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.d.ts.map +1 -1
  23. package/dist/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.js +26 -5
  24. package/dist/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.js.map +1 -1
  25. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.d.ts.map +1 -1
  26. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.js +1 -0
  27. package/dist/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.js.map +1 -1
  28. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/github.controller.d.ts.map +1 -1
  29. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/github.controller.js +3 -0
  30. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/github.controller.js.map +1 -1
  31. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.d.ts.map +1 -1
  32. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.js +3 -0
  33. package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.js.map +1 -1
  34. package/dist/modules/review-execution/entities/reviewContext/reviewContext.gateway.d.ts +1 -0
  35. package/dist/modules/review-execution/entities/reviewContext/reviewContext.gateway.d.ts.map +1 -1
  36. package/dist/modules/review-execution/entities/reviewContext/reviewContext.schema.d.ts +10 -2
  37. package/dist/modules/review-execution/entities/reviewContext/reviewContext.schema.d.ts.map +1 -1
  38. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.factory.d.ts +11 -0
  39. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.factory.d.ts.map +1 -0
  40. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.factory.js +22 -0
  41. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.factory.js.map +1 -0
  42. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.guard.d.ts +10 -0
  43. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.guard.d.ts.map +1 -1
  44. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.schema.d.ts +31 -1
  45. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.schema.d.ts.map +1 -1
  46. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.schema.js +11 -1
  47. package/dist/modules/review-execution/entities/reviewContext/reviewContextResult.schema.js.map +1 -1
  48. package/dist/modules/review-execution/interface-adapters/gateways/reviewContext.fileSystem.gateway.d.ts +1 -0
  49. package/dist/modules/review-execution/interface-adapters/gateways/reviewContext.fileSystem.gateway.d.ts.map +1 -1
  50. package/dist/modules/review-execution/interface-adapters/gateways/reviewContext.fileSystem.gateway.js +27 -1
  51. package/dist/modules/review-execution/interface-adapters/gateways/reviewContext.fileSystem.gateway.js.map +1 -1
  52. package/dist/modules/review-execution/services/reviewRecovery.service.d.ts +33 -0
  53. package/dist/modules/review-execution/services/reviewRecovery.service.d.ts.map +1 -0
  54. package/dist/modules/review-execution/services/reviewRecovery.service.js +80 -0
  55. package/dist/modules/review-execution/services/reviewRecovery.service.js.map +1 -0
  56. package/dist/tests/acceptance/91-dashboard-multi-project-overview.acceptance.test.js +2 -0
  57. package/dist/tests/acceptance/91-dashboard-multi-project-overview.acceptance.test.js.map +1 -1
  58. package/dist/tests/stubs/reviewContextGateway.stub.d.ts +1 -0
  59. package/dist/tests/stubs/reviewContextGateway.stub.d.ts.map +1 -1
  60. package/dist/tests/stubs/reviewContextGateway.stub.js +3 -0
  61. package/dist/tests/stubs/reviewContextGateway.stub.js.map +1 -1
  62. package/dist/tests/units/dashboard/modules/animations.test.d.ts +10 -0
  63. package/dist/tests/units/dashboard/modules/animations.test.d.ts.map +1 -0
  64. package/dist/tests/units/dashboard/modules/animations.test.js +223 -0
  65. package/dist/tests/units/dashboard/modules/animations.test.js.map +1 -0
  66. package/dist/tests/units/dashboard/modules/cardCounters.test.js +56 -0
  67. package/dist/tests/units/dashboard/modules/cardCounters.test.js.map +1 -1
  68. package/dist/tests/units/entities/reviewContext/reviewContext.schema.test.js +1 -0
  69. package/dist/tests/units/entities/reviewContext/reviewContext.schema.test.js.map +1 -1
  70. package/dist/tests/units/entities/reviewContext/reviewContextResult.factory.test.d.ts +2 -0
  71. package/dist/tests/units/entities/reviewContext/reviewContextResult.factory.test.d.ts.map +1 -0
  72. package/dist/tests/units/entities/reviewContext/reviewContextResult.factory.test.js +43 -0
  73. package/dist/tests/units/entities/reviewContext/reviewContextResult.factory.test.js.map +1 -0
  74. package/dist/tests/units/entities/reviewContext/reviewContextResult.guard.test.js +7 -3
  75. package/dist/tests/units/entities/reviewContext/reviewContextResult.guard.test.js.map +1 -1
  76. package/dist/tests/units/entities/reviewContext/reviewContextResult.schema.test.js +12 -1
  77. package/dist/tests/units/entities/reviewContext/reviewContextResult.schema.test.js.map +1 -1
  78. package/dist/tests/units/interface-adapters/controllers/webhook/github.controller.test.js +57 -0
  79. package/dist/tests/units/interface-adapters/controllers/webhook/github.controller.test.js.map +1 -1
  80. package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js +1 -0
  81. package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js.map +1 -1
  82. package/dist/tests/units/interface-adapters/gateways/reviewContext.fileSystem.gateway.test.js +64 -4
  83. package/dist/tests/units/interface-adapters/gateways/reviewContext.fileSystem.gateway.test.js.map +1 -1
  84. package/dist/tests/units/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.test.js +56 -0
  85. package/dist/tests/units/modules/claude-invocation/interface-adapters/gateways/reviewReport.fileSystem.gateway.test.js.map +1 -1
  86. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.test.js +1 -1
  87. package/dist/tests/units/modules/cli-configuration/interface-adapters/controllers/http/repositories.routes.test.js.map +1 -1
  88. package/dist/tests/units/services/reviewRecovery.service.test.d.ts +2 -0
  89. package/dist/tests/units/services/reviewRecovery.service.test.d.ts.map +1 -0
  90. package/dist/tests/units/services/reviewRecovery.service.test.js +49 -0
  91. package/dist/tests/units/services/reviewRecovery.service.test.js.map +1 -0
  92. package/dist/tests/units/services/runReviewRecovery.service.test.d.ts +2 -0
  93. package/dist/tests/units/services/runReviewRecovery.service.test.d.ts.map +1 -0
  94. package/dist/tests/units/services/runReviewRecovery.service.test.js +199 -0
  95. package/dist/tests/units/services/runReviewRecovery.service.test.js.map +1 -0
  96. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.22.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.21.0...reviewflow-v3.22.0) (2026-05-25)
9
+
10
+
11
+ ### Added
12
+
13
+ * **dashboard:** operator's console redesign + animations ([#204](https://github.com/DGouron/review-flow/issues/204)) ([8b33ebd](https://github.com/DGouron/review-flow/commit/8b33ebd28bc09a85280371c499f68d73927eb18e))
14
+
8
15
  ## [3.21.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.20.0...reviewflow-v3.21.0) (2026-05-25)
9
16
 
10
17
 
@@ -4,13 +4,25 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Reviewflow Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
7
10
  <link rel="stylesheet" href="styles.css">
8
11
  <script src="https://unpkg.com/lucide@latest"></script>
9
12
  </head>
10
13
  <body>
11
14
  <div class="container">
12
15
  <header>
13
- <div class="logo"><i data-lucide="bot"></i></div>
16
+ <div class="logo" aria-label="Reviewflow">
17
+ <svg viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
18
+ <rect class="logo-frame" x="2" y="2" width="40" height="40" rx="10"/>
19
+ <line class="logo-line" x1="10" y1="14" x2="26" y2="14"/>
20
+ <line class="logo-line logo-line--accent" x1="10" y1="22" x2="20" y2="22"/>
21
+ <line class="logo-line" x1="10" y1="30" x2="24" y2="30"/>
22
+ <path class="logo-check" d="M28 26 L31 29 L36 22"/>
23
+ <circle class="logo-pulse" cx="20" cy="22" r="1.7"/>
24
+ </svg>
25
+ </div>
14
26
  <h1>Reviewflow</h1>
15
27
  <div class="header-actions">
16
28
  <div class="version-update-wrapper">
@@ -37,6 +49,29 @@
37
49
  </button>
38
50
  <section id="manage-panel" class="manage-panel" aria-label="Manage projects" data-open="false"></section>
39
51
  <nav id="dashboard-tabs" class="dashboard-tab-bar-wrapper" aria-label="Project tabs"></nav>
52
+ <div class="context-chips" aria-label="Toolchain status">
53
+ <div class="toolchain-chip">
54
+ <span class="toolchain-chip-label" id="i18n-card-claude-cli"></span>
55
+ <div id="claude-status" class="card-claude checking">
56
+ <span class="status" id="i18n-claude-checking"></span>
57
+ <span class="version"></span>
58
+ </div>
59
+ </div>
60
+ <div class="toolchain-chip" id="git-cli-card">
61
+ <span class="toolchain-chip-label" id="git-cli-label"></span>
62
+ <div id="git-cli-status" class="card-claude checking">
63
+ <span class="status" id="i18n-git-load-project"></span>
64
+ <span class="version"></span>
65
+ </div>
66
+ </div>
67
+ <div class="toolchain-chip toolchain-chip-model">
68
+ <span class="toolchain-chip-label" id="i18n-card-model"></span>
69
+ <select id="model-select" class="model-select" onchange="changeModel(this.value)">
70
+ <option value="opus" id="i18n-model-opus"></option>
71
+ <option value="sonnet" id="i18n-model-sonnet"></option>
72
+ </select>
73
+ </div>
74
+ </div>
40
75
  </div>
41
76
 
42
77
  <div id="cards-scope-marker" class="cards-scope-marker" data-scope-kind="overview">
@@ -57,44 +92,21 @@
57
92
  <div class="card-label" id="i18n-card-completed"></div>
58
93
  <div id="completed-count" class="card-value">-</div>
59
94
  </div>
60
- <div class="card">
61
- <div class="card-label" id="i18n-card-claude-cli"></div>
62
- <div id="claude-status" class="card-claude checking">
63
- <span class="status" id="i18n-claude-checking"></span>
64
- <span class="version"></span>
65
- </div>
66
- </div>
67
- <div class="card" id="git-cli-card">
68
- <div class="card-label" id="git-cli-label"></div>
69
- <div id="git-cli-status" class="card-claude checking">
70
- <span class="status" id="i18n-git-load-project"></span>
71
- <span class="version"></span>
72
- </div>
73
- </div>
74
- <div class="card">
75
- <div class="card-label" id="i18n-card-model"></div>
76
- <div class="card-model">
77
- <select id="model-select" class="model-select" onchange="changeModel(this.value)">
78
- <option value="opus" id="i18n-model-opus"></option>
79
- <option value="sonnet" id="i18n-model-sonnet"></option>
80
- </select>
81
- </div>
82
- </div>
83
95
  </div>
84
96
 
85
97
  <div class="dashboard-layout">
86
98
  <aside class="dashboard-sidebar" aria-label="Project tools">
87
- <div class="sidebar-language">
88
- <label class="sidebar-language-label" for="language-select" id="i18n-card-language"></label>
89
- <select id="language-select" class="model-select" onchange="changeLanguage(this.value)">
90
- <option value="en">English</option>
91
- <option value="fr">Français</option>
92
- </select>
93
- </div>
99
+ <button type="button" id="open-settings-modal-btn" class="sidebar-settings-button" hidden>
100
+ <span class="sidebar-settings-button__prefix">// SETTINGS</span>
101
+ </button>
94
102
 
95
103
  <span id="config-status" class="config-status hidden"></span>
96
104
 
97
- <div class="focus-strip">
105
+ <section id="worktree-section" aria-label="Worktree pool"></section>
106
+ </aside>
107
+
108
+ <main class="dashboard-main">
109
+ <div class="focus-strip attention-strip" aria-label="Attention indicators">
98
110
  <div class="focus-chip focus-now">
99
111
  <div class="focus-copy">
100
112
  <span class="focus-label" id="i18n-strip-now"></span>
@@ -119,14 +131,6 @@
119
131
  <button id="focus-strip-toggle" class="focus-toggle-btn" onclick="toggleFocusStripMode()"></button>
120
132
  </div>
121
133
 
122
- <section id="worktree-section" aria-label="Worktree pool"></section>
123
-
124
- <button type="button" id="open-settings-modal-btn" class="sidebar-settings-button" hidden>
125
- <span class="sidebar-settings-button__prefix">// SETTINGS</span>
126
- </button>
127
- </aside>
128
-
129
- <main class="dashboard-main">
130
134
  <section id="overview-section" class="overview-section" aria-label="Multi-project overview"></section>
131
135
 
132
136
  <div id="data-loading-state" class="data-loading hidden" role="status" aria-live="polite">
@@ -154,7 +158,11 @@
154
158
  <span id="pending-reviews-count" class="badge-count hidden">0</span>
155
159
  </div>
156
160
  <div id="pending-reviews" class="section-content">
157
- <div class="empty-state" id="i18n-empty-pending-reviews"></div>
161
+ <div class="heartbeat-empty-state" id="pending-reviews-empty-state">
162
+ <span class="heartbeat-label">// STANDBY</span>
163
+ <span class="heartbeat-message" id="i18n-empty-pending-reviews">No reviews waiting for confirmation</span>
164
+ <div class="heartbeat-line-container" id="heartbeat-line" aria-hidden="true"></div>
165
+ </div>
158
166
  </div>
159
167
  </div>
160
168
 
@@ -323,9 +331,30 @@
323
331
 
324
332
  <dialog id="settings-modal" class="settings-modal" aria-labelledby="settings-modal-title"></dialog>
325
333
 
334
+ <!-- UI language select — kept in DOM for JS wiring; visual control lives in settings modal -->
335
+ <select id="language-select" style="display:none" aria-hidden="true" onchange="changeLanguage(this.value)">
336
+ <option value="en">English</option>
337
+ <option value="fr">Français</option>
338
+ </select>
339
+
326
340
  <script type="module">
327
341
  import { t, setLanguage, getLanguage } from './modules/i18n.js';
328
342
  import { formatTime, formatDuration, formatPhase, formatLogTime } from './modules/formatting.js';
343
+ import {
344
+ reducedMotion,
345
+ animateMount,
346
+ animateCounter as animateCounterValue,
347
+ slideTabUnderline,
348
+ heartbeat,
349
+ pulseLive,
350
+ springIn,
351
+ liftCard,
352
+ unliftCard,
353
+ pulseStatusDot,
354
+ breatheLogo,
355
+ crossFadeTab,
356
+ toggleHeight,
357
+ } from './modules/animations.js';
329
358
  import { escapeHtml, markdownToHtml, sanitizeHttpUrl } from './modules/html.js';
330
359
  import { getAgentIcon, icon, refreshIcons } from './modules/icons.js';
331
360
  import { MAX_RECONNECT_ATTEMPTS, RECONNECT_DELAY, STORAGE_KEY_CURRENT, STORAGE_KEY_FOCUS_STRIP_MODE, QUALITY_TARGET_SCORE } from './modules/constants.js';
@@ -334,7 +363,7 @@
334
363
  import { renderOverviewHtml } from './modules/overview.js';
335
364
  import { getDesktopNotificationPayload, shouldNotifyDesktop } from './modules/desktopNotifications.js';
336
365
  import { getLoadingPresentation, getQuietRefreshSectionIdentifiers } from './modules/loading.js';
337
- import { computeCardCounters } from './modules/cardCounters.js';
366
+ import { computeCardCounters, extractGithubSlug } from './modules/cardCounters.js';
338
367
  import { collectReviewNotifications, createReviewNotificationState } from './modules/notifications.js';
339
368
  import { resolveReviewAssigneeDisplay } from './modules/assignee.js';
340
369
  import { buildQueueLanesModel } from './modules/queueLanes.js';
@@ -804,6 +833,13 @@
804
833
  document.getElementById('focus-next-count').textContent = String(nextCount);
805
834
  document.getElementById('focus-blocked-count').textContent = String(blocked);
806
835
 
836
+ const chipNow = document.querySelector('.focus-chip.focus-now');
837
+ const chipNext = document.querySelector('.focus-chip.focus-next');
838
+ const chipBlocked = document.querySelector('.focus-chip.focus-blocked');
839
+ if (chipNow) chipNow.dataset.active = nowCount > 0 ? 'true' : 'false';
840
+ if (chipNext) chipNext.dataset.active = nextCount > 0 ? 'true' : 'false';
841
+ if (chipBlocked) chipBlocked.dataset.active = blocked > 0 ? 'critical' : 'false';
842
+
807
843
  const activeReviewsSection = document.getElementById('active-reviews-section');
808
844
  const activeReviewsEl = document.getElementById('active-reviews');
809
845
  const activeReviewsCount = document.getElementById('active-reviews-count');
@@ -2392,7 +2428,11 @@
2392
2428
  } else {
2393
2429
  const repository = availableRepositories.find((r) => r.localPath === activeTabId);
2394
2430
  const projectName = repository?.name ?? activeTabId.split('/').filter(Boolean).pop() ?? activeTabId;
2395
- scope = { kind: 'project', localPath: activeTabId, projectName };
2431
+ const aliases = [];
2432
+ const slug = extractGithubSlug(repository?.remoteUrl);
2433
+ if (slug) aliases.push(slug);
2434
+ if (repository?.name) aliases.push(repository.name);
2435
+ scope = { kind: 'project', localPath: activeTabId, projectName, aliases };
2396
2436
  }
2397
2437
  const counters = computeCardCounters({
2398
2438
  activeReviews: currentData.activeReviews,
@@ -2428,6 +2468,7 @@
2428
2468
  handleTabClick(tabId);
2429
2469
  });
2430
2470
  });
2471
+ setupTabUnderline();
2431
2472
  }
2432
2473
 
2433
2474
  function handleTabClick(tabId) {
@@ -2499,6 +2540,11 @@
2499
2540
  });
2500
2541
  dialog.innerHTML = renderSettingsModalHtml(viewModel);
2501
2542
  bindSettingsModalForm(dialog);
2543
+ const uiLangSelect = dialog.querySelector('#settings-modal-ui-language');
2544
+ if (uiLangSelect) {
2545
+ const hiddenSelect = document.getElementById('language-select');
2546
+ uiLangSelect.value = hiddenSelect ? hiddenSelect.value : getLanguage();
2547
+ }
2502
2548
  dialog.showModal();
2503
2549
  } catch (error) {
2504
2550
  showToast(t('toast.error') || 'Erreur', 'error');
@@ -2627,6 +2673,12 @@
2627
2673
  isManagePanelOpen = !isManagePanelOpen;
2628
2674
  toggle.setAttribute('aria-expanded', isManagePanelOpen ? 'true' : 'false');
2629
2675
  renderManagePanel();
2676
+ const panel = document.getElementById('manage-panel');
2677
+ if (panel) {
2678
+ loadAnimeApi().then((anime) => {
2679
+ if (anime) toggleHeight(panel, isManagePanelOpen, { animeApi: anime });
2680
+ }).catch(() => {});
2681
+ }
2630
2682
  });
2631
2683
  }
2632
2684
 
@@ -3390,7 +3442,169 @@
3390
3442
  }
3391
3443
 
3392
3444
  refreshWorktreeSection({ animate: true });
3393
- setInterval(() => refreshWorktreeSection({ animate: false }), 30000);
3445
+ setInterval(() => refreshWorktreeSection({ animate: false }), 5000);
3446
+
3447
+ // === Operator's Console boot animations ===
3448
+
3449
+ let heartbeatStop = null;
3450
+
3451
+ async function bootAnimations() {
3452
+ const anime = await loadAnimeApi();
3453
+ if (!anime) return;
3454
+
3455
+ // Logo breath — daemon-alive signal
3456
+ const logo = document.querySelector('.logo');
3457
+ if (logo) breatheLogo(logo, { animeApi: anime });
3458
+
3459
+ // Online status dot pulse
3460
+ const statusDot = document.querySelector('.status-dot');
3461
+ if (statusDot) pulseStatusDot(statusDot, { animeApi: anime });
3462
+
3463
+ // Metric cards stagger mount
3464
+ const cards = document.querySelectorAll('.cards > .card');
3465
+ if (cards.length > 0) animateMount(cards, { animeApi: anime });
3466
+
3467
+ // Sidebar slide-in
3468
+ const sidebar = document.querySelector('.dashboard-sidebar');
3469
+ if (sidebar && !reducedMotion()) {
3470
+ anime.animate(sidebar, {
3471
+ opacity: [{ from: 0, to: 1 }],
3472
+ translateX: [{ from: -8, to: 0 }],
3473
+ duration: 280,
3474
+ easing: 'easeOutCubic',
3475
+ });
3476
+ }
3477
+
3478
+ // Attention strip mount (focus-strip promoted to main)
3479
+ const focusChips = document.querySelectorAll('.focus-chip');
3480
+ if (focusChips.length > 0) animateMount(focusChips, { animeApi: anime, staggerMs: 40, yOffset: 8 });
3481
+
3482
+ // Heartbeat on pending-reviews empty state
3483
+ startHeartbeat(anime);
3484
+
3485
+ // Card hover lift — wire to existing cards
3486
+ wireCardHovers(anime);
3487
+
3488
+ // Settings modal spring-in
3489
+ wireSettingsModal(anime);
3490
+ }
3491
+
3492
+ function startHeartbeat(anime) {
3493
+ const container = document.getElementById('heartbeat-line');
3494
+ if (!container) return;
3495
+ if (heartbeatStop) heartbeatStop.stop();
3496
+ heartbeatStop = heartbeat(container, { animeApi: anime });
3497
+ }
3498
+
3499
+ function wireCardHovers(anime) {
3500
+ const cards = document.querySelectorAll('.cards > .card');
3501
+ for (const card of cards) {
3502
+ card.addEventListener('mouseenter', () => liftCard(card, { animeApi: anime }));
3503
+ card.addEventListener('mouseleave', () => unliftCard(card, { animeApi: anime }));
3504
+ }
3505
+ }
3506
+
3507
+ function wireSettingsModal(anime) {
3508
+ const modal = document.getElementById('settings-modal');
3509
+ if (!modal) return;
3510
+ modal.addEventListener('animationsjs-open', () => springIn(modal, { animeApi: anime }));
3511
+ }
3512
+
3513
+ // Tab underline glide — set up after tab bar renders
3514
+ let tabUnderlineEl = null;
3515
+
3516
+ function setupTabUnderline() {
3517
+ const tabBar = document.querySelector('.dashboard-tab-bar');
3518
+ if (!tabBar) return;
3519
+
3520
+ if (!tabUnderlineEl) {
3521
+ tabUnderlineEl = document.createElement('div');
3522
+ tabUnderlineEl.className = 'tab-underline-indicator';
3523
+ tabBar.style.position = 'relative';
3524
+ tabBar.appendChild(tabUnderlineEl);
3525
+ }
3526
+
3527
+ const activeTab = tabBar.querySelector('.is-active');
3528
+ if (activeTab) {
3529
+ loadAnimeApi().then((anime) => {
3530
+ if (anime && tabUnderlineEl) slideTabUnderline(tabUnderlineEl, activeTab, { animeApi: anime });
3531
+ }).catch(() => {});
3532
+ }
3533
+ }
3534
+
3535
+ // Counter animation — wrap renderCardCounters and focus strip to animate changes
3536
+ const originalCounterIds = ['running-count', 'queued-count', 'completed-count', 'focus-now-count', 'focus-next-count', 'focus-blocked-count'];
3537
+ const previousCounterValues = {};
3538
+
3539
+ function animateCounterChanges(anime) {
3540
+ for (const id of originalCounterIds) {
3541
+ const el = document.getElementById(id);
3542
+ if (!el) continue;
3543
+ if (el.dataset.animating === 'true') continue;
3544
+ const currentText = el.textContent;
3545
+ const currentVal = parseInt(currentText, 10);
3546
+ const prevVal = previousCounterValues[id];
3547
+ if (!Number.isNaN(currentVal) && prevVal !== undefined && prevVal !== currentVal) {
3548
+ el.dataset.animating = 'true';
3549
+ animateCounterValue(el, prevVal, currentVal, {
3550
+ animeApi: anime,
3551
+ onComplete: () => {
3552
+ el.dataset.animating = 'false';
3553
+ previousCounterValues[id] = currentVal;
3554
+ },
3555
+ });
3556
+ } else if (!Number.isNaN(currentVal)) {
3557
+ previousCounterValues[id] = currentVal;
3558
+ }
3559
+ }
3560
+ }
3561
+
3562
+ // Observe counter element changes
3563
+ async function observeCounters() {
3564
+ const anime = await loadAnimeApi();
3565
+ if (!anime) return;
3566
+
3567
+ // Snapshot initial values
3568
+ for (const id of originalCounterIds) {
3569
+ const el = document.getElementById(id);
3570
+ if (el) {
3571
+ const val = parseInt(el.textContent, 10);
3572
+ if (!Number.isNaN(val)) previousCounterValues[id] = val;
3573
+ }
3574
+ }
3575
+
3576
+ const observer = new MutationObserver(() => animateCounterChanges(anime));
3577
+ for (const id of originalCounterIds) {
3578
+ const el = document.getElementById(id);
3579
+ if (el) observer.observe(el, { childList: true, characterData: true, subtree: true });
3580
+ }
3581
+ }
3582
+
3583
+ // Visibility change — pause/resume heartbeat to save battery
3584
+ document.addEventListener('visibilitychange', () => {
3585
+ loadAnimeApi().then((anime) => {
3586
+ if (!anime) return;
3587
+ if (document.hidden) {
3588
+ if (heartbeatStop) heartbeatStop.stop();
3589
+ } else {
3590
+ startHeartbeat(anime);
3591
+ }
3592
+ }).catch(() => {});
3593
+ });
3594
+
3595
+ // Boot animations & observers exactly once, regardless of readyState timing
3596
+ let __animationsBooted = false;
3597
+ function bootOnce() {
3598
+ if (__animationsBooted) return;
3599
+ __animationsBooted = true;
3600
+ bootAnimations();
3601
+ observeCounters();
3602
+ }
3603
+ if (document.readyState !== 'loading') {
3604
+ bootOnce();
3605
+ } else {
3606
+ window.addEventListener('DOMContentLoaded', bootOnce);
3607
+ }
3394
3608
  </script>
3395
3609
  </body>
3396
3610
  </html>
@@ -0,0 +1,164 @@
1
+ /**
2
+ * animations.js — Anime.js v4 animation helpers for the Operator's Console dashboard.
3
+ *
4
+ * Pure functions: take DOM elements + options, delegate to anime.js, return nothing
5
+ * (or a stop handle for loops). No DOM access at module level, no global state.
6
+ *
7
+ * Every function guards on reducedMotion() and returns immediately (or applies end-state)
8
+ * when the user prefers reduced motion.
9
+ *
10
+ * @module animations
11
+ */
12
+ /**
13
+ * Returns true when the user has opted-in to reduced motion.
14
+ * @returns {boolean}
15
+ */
16
+ export function reducedMotion(): boolean;
17
+ /**
18
+ * Fade-up stagger mount animation for a NodeList or Array of elements.
19
+ * @param {Element[]|NodeList} elements
20
+ * @param {{ staggerMs?: number, durationMs?: number, yOffset?: number, animeApi: object }} options
21
+ */
22
+ export function animateMount(elements: Element[] | NodeList, options: {
23
+ staggerMs?: number;
24
+ durationMs?: number;
25
+ yOffset?: number;
26
+ animeApi: object;
27
+ }): void;
28
+ /**
29
+ * Animated number counter — morphs text content from oldValue to newValue.
30
+ * Adds a brief scale pulse on the element.
31
+ * @param {Element} element
32
+ * @param {number} from
33
+ * @param {number} to
34
+ * @param {{ animeApi: object, durationMs?: number, onComplete?: () => void }} options
35
+ */
36
+ export function animateCounter(element: Element, from: number, to: number, options: {
37
+ animeApi: object;
38
+ durationMs?: number;
39
+ onComplete?: () => void;
40
+ }): void;
41
+ /**
42
+ * Slide the shared tab underline element to match the target tab's
43
+ * position and width. Pass the underline element and the active tab element.
44
+ * @param {Element} underline
45
+ * @param {Element} targetTab
46
+ * @param {{ animeApi: object }} options
47
+ */
48
+ export function slideTabUnderline(underline: Element, targetTab: Element, options: {
49
+ animeApi: object;
50
+ }): void;
51
+ /**
52
+ * Heartbeat — a 1px amber line traversing left-to-right infinitely
53
+ * inside the given container element.
54
+ * @param {Element} container - The 240px-wide container element.
55
+ * @param {{ animeApi: object }} options
56
+ * @returns {{ stop: () => void }}
57
+ */
58
+ export function heartbeat(container: Element, options: {
59
+ animeApi: object;
60
+ }): {
61
+ stop: () => void;
62
+ };
63
+ /**
64
+ * Breathing amber glow pulse on a live/active review card.
65
+ * @param {Element} element
66
+ * @param {{ animeApi: object }} options
67
+ * @returns {{ stop: () => void }}
68
+ */
69
+ export function pulseLive(element: Element, options: {
70
+ animeApi: object;
71
+ }): {
72
+ stop: () => void;
73
+ };
74
+ /**
75
+ * Spring-in entry for the settings modal (scale + opacity).
76
+ * @param {Element} modal
77
+ * @param {{ animeApi: object }} options
78
+ */
79
+ export function springIn(modal: Element, options: {
80
+ animeApi: object;
81
+ }): void;
82
+ /**
83
+ * Card lift on hover — translateY -2px + scale 1.005.
84
+ * @param {Element} card
85
+ * @param {{ animeApi: object }} options
86
+ */
87
+ export function liftCard(card: Element, options: {
88
+ animeApi: object;
89
+ }): void;
90
+ /**
91
+ * Return card to resting position.
92
+ * @param {Element} card
93
+ * @param {{ animeApi: object }} options
94
+ */
95
+ export function unliftCard(card: Element, options: {
96
+ animeApi: object;
97
+ }): void;
98
+ /**
99
+ * Subtle breathing pulse for the online status dot.
100
+ * @param {Element} dot
101
+ * @param {{ animeApi: object }} options
102
+ * @returns {{ stop: () => void }}
103
+ */
104
+ export function pulseStatusDot(dot: Element, options: {
105
+ animeApi: object;
106
+ }): {
107
+ stop: () => void;
108
+ };
109
+ /**
110
+ * Very subtle logo breathing pulse (daemon-alive signal).
111
+ * @param {Element} logo
112
+ * @param {{ animeApi: object }} options
113
+ * @returns {{ stop: () => void }}
114
+ */
115
+ export function breatheLogo(logo: Element, options: {
116
+ animeApi: object;
117
+ }): {
118
+ stop: () => void;
119
+ };
120
+ /**
121
+ * Cross-fade between tab content panels (fade out old, fade in new).
122
+ * @param {Element} outgoing - Element being hidden.
123
+ * @param {Element} incoming - Element being shown.
124
+ * @param {{ animeApi: object }} options
125
+ */
126
+ export function crossFadeTab(outgoing: Element, incoming: Element, options: {
127
+ animeApi: object;
128
+ }): void;
129
+ /**
130
+ * Expand an element from height 0 to its natural height using anime.js.
131
+ * Uses a snapshot of the natural height before collapsing to 0.
132
+ * @param {Element} element
133
+ * @param {{ animeApi: object }} options
134
+ */
135
+ export function expandHeight(element: Element, options: {
136
+ animeApi: object;
137
+ }): void;
138
+ /**
139
+ * Collapse an element from its natural height to 0 using anime.js.
140
+ * @param {Element} element
141
+ * @param {{ animeApi: object }} options
142
+ */
143
+ export function collapseHeight(element: Element, options: {
144
+ animeApi: object;
145
+ }): void;
146
+ /**
147
+ * Toggle expand/collapse of an element and stagger-animate its children on open.
148
+ * @param {Element} element
149
+ * @param {boolean} isOpen - true to expand, false to collapse
150
+ * @param {{ animeApi: object, childSelector?: string }} options
151
+ */
152
+ export function toggleHeight(element: Element, isOpen: boolean, options: {
153
+ animeApi: object;
154
+ childSelector?: string;
155
+ }): void;
156
+ /**
157
+ * Review status change: amber border fades → green glow pulse → green border.
158
+ * @param {Element} element
159
+ * @param {{ animeApi: object }} options
160
+ */
161
+ export function reviewCompleted(element: Element, options: {
162
+ animeApi: object;
163
+ }): void;
164
+ //# sourceMappingURL=animations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animations.d.ts","sourceRoot":"","sources":["../../../src/dashboard/modules/animations.js"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,iCAFa,OAAO,CAInB;AAED;;;;GAIG;AACH,uCAHW,OAAO,EAAE,GAAC,QAAQ,WAClB;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,QAkBzF;AAED;;;;;;;GAOG;AACH,wCALW,OAAO,QACP,MAAM,MACN,MAAM,WACN;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,QA0B5E;AAED;;;;;;GAMG;AACH,6CAJW,OAAO,aACP,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,QAsB9B;AAED;;;;;;GAMG;AACH,qCAJW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CA6ChC;AAED;;;;;GAKG;AACH,mCAJW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAiBhC;AAED;;;;GAIG;AACH,gCAHW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,QAe9B;AAED;;;;GAIG;AACH,+BAHW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,QAW9B;AAED;;;;GAIG;AACH,iCAHW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,QAW9B;AAED;;;;;GAKG;AACH,oCAJW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAYhC;AAED;;;;;GAKG;AACH,kCAJW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAYhC;AAED;;;;;GAKG;AACH,uCAJW,OAAO,YACP,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,QAwB9B;AAED;;;;;GAKG;AACH,sCAHW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,QA0B9B;AAED;;;;GAIG;AACH,wCAHW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,QAoB9B;AAED;;;;;GAKG;AACH,sCAJW,OAAO,UACP,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,QAqBtD;AAED;;;;GAIG;AACH,yCAHW,OAAO,WACP;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,QAkB9B"}