@echomem/mcp 1.4.26 → 1.4.28
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/dist/codex-session-files.js +20 -0
- package/dist/context-analysis/vendored-canonical.js +54 -11
- package/dist/forensics.js +24 -4
- package/dist/hud/cli.js +18 -1
- package/dist/hud/hooks.js +59 -0
- package/dist/index.js +111 -16
- package/dist/migrate.js +24 -11
- package/dist/package-metadata.js +5 -3
- package/dist/save-checkpoint-hook.js +111 -0
- package/dist/setup-page/client-core.js +87 -15
- package/dist/setup-page/client-extraction.js +508 -207
- package/dist/setup-page/client-lifecycle.js +10 -11
- package/dist/setup-page/client-report-audit.js +3 -2
- package/dist/setup-page/client-report-city.js +168 -38
- package/dist/setup-page/styles-extraction.js +37 -0
- package/dist/setup-page/styles-foundation.js +3 -1
- package/dist/setup-page/styles-mvp.js +751 -151
- package/dist/setup-page/styles-website-alignment.js +8 -3
- package/dist/setup-preview.js +46 -6
- package/dist/setup.js +402 -44
- package/dist/v1-contract.js +39 -6
- package/package.json +3 -2
- package/templates/codex-skills/echomem-save/SKILL.md +10 -0
- package/templates/echomem-recall.md +10 -2
|
@@ -8,22 +8,21 @@ export const SETUP_PAGE_CLIENT_LIFECYCLE = String.raw ` /* ---------- scan-
|
|
|
8
8
|
setHead("Local file access", "Consent");
|
|
9
9
|
app.className = "consentStage mvpConsentStage";
|
|
10
10
|
app.innerHTML =
|
|
11
|
+
setupJourney(1) +
|
|
11
12
|
'<section class="mvpConsent" aria-labelledby="mvpConsentTitle">' +
|
|
12
|
-
'<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'
|
|
13
|
+
'<div class="mvpConsentHero">' +
|
|
14
|
+
'<img class="mvpConsentEcho" src="/hud-assets/echo-face-cutout.png" alt="" />' +
|
|
15
|
+
'<div class="mvpConsentPitch">' +
|
|
16
|
+
'<h2 id="mvpConsentTitle">Start with the context you’ve already built.</h2>' +
|
|
17
|
+
'<p class="mvpConsentIntro">See what you keep re-explaining—and what’s worth carrying into future sessions.</p>' +
|
|
18
|
+
'</div>' +
|
|
19
|
+
'</div>' +
|
|
20
|
+
'<p class="mvpConsentTrustLine">Your history stays on your Mac until you choose what to import. ' + dataHandlingLink("Data handling") + '</p>' +
|
|
21
|
+
'<button type="button" class="primary mvpPrimary" id="allowLocalScan">Build my private report <span aria-hidden="true">→</span></button>' +
|
|
19
22
|
'<p class="consentFine" id="consentStatus" role="status" aria-live="polite"></p>' +
|
|
20
23
|
'</section>';
|
|
21
24
|
var allow = document.getElementById("allowLocalScan");
|
|
22
25
|
if (allow) allow.onclick = allowLocalScan;
|
|
23
|
-
var decline = document.getElementById("declineLocalScan");
|
|
24
|
-
if (decline) decline.onclick = function () {
|
|
25
|
-
setConsentStatus("Echo can't continue setup without permission to scan your local history.", "required");
|
|
26
|
-
};
|
|
27
26
|
}
|
|
28
27
|
function setConsentStatus(message, tone) {
|
|
29
28
|
var node = document.getElementById("consentStatus");
|
|
@@ -716,8 +716,9 @@ export const SETUP_PAGE_CLIENT_REPORT_AUDIT = String.raw ` /* ---------- fo
|
|
|
716
716
|
'</section>' +
|
|
717
717
|
'<div class="audit-list">' + groups.map(function (group, index) { return auditRowHtml(group, index, totalWaste, slices[index]); }).join('') + '</div>' +
|
|
718
718
|
'<footer class="audit-end">' +
|
|
719
|
-
'<h3>
|
|
720
|
-
'<
|
|
719
|
+
'<h3>Turn this lost context into memory.</h3>' +
|
|
720
|
+
'<p class="audit-memory-bridge">Your report found where context is being repeated. Next, Echo can turn selected coding sessions into memories your agents can recall.</p>' +
|
|
721
|
+
'<button class="rdo-cta" data-connect-echo>Build my first memories' +
|
|
721
722
|
'<img class="cta-face" src="/hud-assets/echo-face-cutout.png" alt="Echo" />' +
|
|
722
723
|
'<svg class="cta-arrow" viewBox="0 0 64 26" aria-hidden="true">' +
|
|
723
724
|
'<defs><filter id="ctaPencil" x="-20%" y="-40%" width="140%" height="180%"><feTurbulence type="fractalNoise" baseFrequency="0.05 0.12" numOctaves="3" seed="11"/><feDisplacementMap in="SourceGraphic" scale="2.6"/></filter></defs>' +
|
|
@@ -119,10 +119,11 @@ export const SETUP_PAGE_CLIENT_REPORT_CITY = String.raw ` /* ---------- ful
|
|
|
119
119
|
'Local Only' +
|
|
120
120
|
'</span>' +
|
|
121
121
|
'</nav>' +
|
|
122
|
-
'<button class="navConnect" data-connect-echo><span class="navDot"></span>
|
|
122
|
+
'<button class="navConnect" data-connect-echo><span class="navDot"></span>Build my first memories' +
|
|
123
123
|
'<svg viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M3.5 8.5 8.5 3.5M4.5 3.5h4v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
|
124
124
|
'</button>' +
|
|
125
125
|
'</header>' +
|
|
126
|
+
setupJourney(2) +
|
|
126
127
|
'<section class="cityHero"><div class="cityFigureRow">' +
|
|
127
128
|
'<figure class="cityFig">' +
|
|
128
129
|
'<div class="cityShell" id="city-shell">' +
|
|
@@ -130,7 +131,7 @@ export const SETUP_PAGE_CLIENT_REPORT_CITY = String.raw ` /* ---------- ful
|
|
|
130
131
|
'</div>' +
|
|
131
132
|
'<button class="cityFullBtn" id="city-full-btn" type="button">' +
|
|
132
133
|
'<svg viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M7.5 1.5h3v3M4.5 10.5h-3v-3M10.5 1.5 7 5M1.5 10.5 5 7" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
|
|
133
|
-
'
|
|
134
|
+
'Explore the memory city' +
|
|
134
135
|
'</button>' +
|
|
135
136
|
'</figure>' +
|
|
136
137
|
'<div class="cityNote" id="city-fieldnote"></div>' +
|
|
@@ -168,59 +169,188 @@ export const SETUP_PAGE_CLIENT_REPORT_CITY = String.raw ` /* ---------- ful
|
|
|
168
169
|
setScanMode(false);
|
|
169
170
|
setCityMode(false);
|
|
170
171
|
}
|
|
172
|
+
function formatReportEta(seconds) {
|
|
173
|
+
seconds = Math.max(1, Math.round(seconds));
|
|
174
|
+
if (seconds >= 3600) {
|
|
175
|
+
var hours = Math.floor(seconds / 3600);
|
|
176
|
+
var minutesAfterHours = Math.ceil((seconds % 3600) / 60);
|
|
177
|
+
return "About " + hours + "h" + (minutesAfterHours ? " " + minutesAfterHours + "m" : "") + " remaining";
|
|
178
|
+
}
|
|
179
|
+
if (seconds >= 60) {
|
|
180
|
+
var minutes = Math.floor(seconds / 60);
|
|
181
|
+
var secondsAfterMinutes = seconds % 60;
|
|
182
|
+
return "About " + minutes + "m " + secondsAfterMinutes + "s remaining";
|
|
183
|
+
}
|
|
184
|
+
return "About " + seconds + "s remaining";
|
|
185
|
+
}
|
|
186
|
+
function estimateReportRemaining(progress, overall, stage) {
|
|
187
|
+
void overall;
|
|
188
|
+
var now = Date.now();
|
|
189
|
+
var stageElapsed = Math.max(0, Number(progress.stageElapsedMs || 0) / 1000);
|
|
190
|
+
var stageDone = Math.max(0, Number(progress.stageDone || 0));
|
|
191
|
+
var stageTotal = Math.max(0, Number(progress.stageTotal || 0));
|
|
192
|
+
function tickExistingEstimate() {
|
|
193
|
+
if (reportEtaDisplaySeconds <= 0) {
|
|
194
|
+
return { seconds: 0, overdue: false, calibrating: true };
|
|
195
|
+
}
|
|
196
|
+
var sinceLast = reportEtaUpdatedAt ? Math.max(0, (now - reportEtaUpdatedAt) / 1000) : 0;
|
|
197
|
+
reportEtaDisplaySeconds = Math.max(0, reportEtaDisplaySeconds - sinceLast);
|
|
198
|
+
reportEtaUpdatedAt = now;
|
|
199
|
+
return {
|
|
200
|
+
seconds: reportEtaDisplaySeconds,
|
|
201
|
+
overdue: now >= reportEtaDeadline || reportEtaDisplaySeconds <= 0,
|
|
202
|
+
calibrating: false,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// The visual progress bands describe work completed, not wall-clock cost. In particular,
|
|
207
|
+
// reaching the 75% classification band does NOT mean only 25% of the time remains.
|
|
208
|
+
// Calibrate only from movement inside the expensive classification stage.
|
|
209
|
+
if (stage !== "classifying-repeated-context") {
|
|
210
|
+
return tickExistingEstimate();
|
|
211
|
+
}
|
|
212
|
+
if (reportEtaStage !== stage) {
|
|
213
|
+
reportEtaStage = stage;
|
|
214
|
+
reportEtaStageDone = stageDone;
|
|
215
|
+
reportEtaStageElapsed = stageElapsed;
|
|
216
|
+
reportEtaObservedRate = 0;
|
|
217
|
+
return { seconds: 0, overdue: false, calibrating: true };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The first canonical completions can be cache hits and arrive almost instantly. Treat the
|
|
221
|
+
// first observation as a baseline, then measure only later movement over real elapsed time.
|
|
222
|
+
if (stageDone > reportEtaStageDone && stageElapsed > reportEtaStageElapsed) {
|
|
223
|
+
var sampleElapsed = stageElapsed - reportEtaStageElapsed;
|
|
224
|
+
var sampleDone = stageDone - reportEtaStageDone;
|
|
225
|
+
if (sampleElapsed >= 2 && sampleDone > 0) {
|
|
226
|
+
var sampleRate = sampleDone / sampleElapsed;
|
|
227
|
+
reportEtaObservedRate = reportEtaObservedRate > 0
|
|
228
|
+
? reportEtaObservedRate * 0.65 + sampleRate * 0.35
|
|
229
|
+
: sampleRate;
|
|
230
|
+
reportEtaStageDone = stageDone;
|
|
231
|
+
reportEtaStageElapsed = stageElapsed;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
reportEtaObservedRate <= 0 ||
|
|
236
|
+
!isFinite(reportEtaObservedRate)
|
|
237
|
+
) {
|
|
238
|
+
return { seconds: 0, overdue: false, calibrating: true };
|
|
239
|
+
}
|
|
240
|
+
if (stageTotal <= stageDone) return tickExistingEstimate();
|
|
241
|
+
|
|
242
|
+
// Add a safety margin for the tail of slower sessions plus final report reconciliation.
|
|
243
|
+
// This intentionally prefers a slightly early completion over a fake three-second promise.
|
|
244
|
+
var observedRemaining = ((stageTotal - stageDone) / reportEtaObservedRate) * 1.35 + 5;
|
|
245
|
+
observedRemaining = Math.min(15 * 60, Math.max(8, observedRemaining));
|
|
246
|
+
|
|
247
|
+
if (!reportEtaDeadline) {
|
|
248
|
+
reportEtaDeadline = now + observedRemaining * 1000;
|
|
249
|
+
reportEtaDisplaySeconds = observedRemaining;
|
|
250
|
+
} else {
|
|
251
|
+
var sinceLast = reportEtaUpdatedAt ? Math.max(0, (now - reportEtaUpdatedAt) / 1000) : 0;
|
|
252
|
+
var clockRemaining = Math.max(0, reportEtaDisplaySeconds - sinceLast);
|
|
253
|
+
// A faster observed rate may shorten the estimate. A slower sample never pushes the
|
|
254
|
+
// countdown back upward; passing the first deadline changes the copy to "Finishing up".
|
|
255
|
+
reportEtaDisplaySeconds = Math.min(clockRemaining, observedRemaining);
|
|
256
|
+
}
|
|
257
|
+
reportEtaUpdatedAt = now;
|
|
258
|
+
return {
|
|
259
|
+
seconds: Math.max(0, reportEtaDisplaySeconds),
|
|
260
|
+
overdue: now >= reportEtaDeadline || reportEtaDisplaySeconds <= 0,
|
|
261
|
+
calibrating: false,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
171
264
|
function renderLoading() {
|
|
172
265
|
var progress = reportEnvelope && reportEnvelope.kind === "scanning" ? (reportEnvelope.progress || {}) : {};
|
|
173
|
-
var
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
266
|
+
var scanId = String(reportEnvelope && reportEnvelope.scanId || "");
|
|
267
|
+
if (scanId !== reportEtaScanId) {
|
|
268
|
+
reportEtaScanId = scanId;
|
|
269
|
+
reportEtaDeadline = 0;
|
|
270
|
+
reportEtaDisplaySeconds = 0;
|
|
271
|
+
reportEtaUpdatedAt = 0;
|
|
272
|
+
reportEtaStage = "";
|
|
273
|
+
reportEtaStageDone = 0;
|
|
274
|
+
reportEtaStageElapsed = 0;
|
|
275
|
+
reportEtaObservedRate = 0;
|
|
182
276
|
}
|
|
183
|
-
var
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
277
|
+
var scannedValue = Number(progress.scanned);
|
|
278
|
+
var totalValue = Number(progress.total);
|
|
279
|
+
var scanned = isFinite(scannedValue) ? Math.max(0, scannedValue) : 0;
|
|
280
|
+
var total = isFinite(totalValue) ? Math.max(0, totalValue) : 0;
|
|
281
|
+
var visibleScanned = total > 0 ? Math.min(scanned, total) : scanned;
|
|
282
|
+
// The bar tracks the whole scan (reading → summary → classifying → finalizing), which the
|
|
283
|
+
// file counters alone cannot express: they hit their maximum while analysis is still running.
|
|
284
|
+
var overallValue = Number(progress.overall);
|
|
285
|
+
var overall = isFinite(overallValue) ? Math.min(1, Math.max(0, overallValue)) : 0;
|
|
286
|
+
var pct = Math.round(overall * 100);
|
|
287
|
+
var stage = String(progress.stage || "starting");
|
|
288
|
+
var stageDone = Math.max(0, Number(progress.stageDone || 0));
|
|
289
|
+
var stageTotal = Math.max(0, Number(progress.stageTotal || 0));
|
|
290
|
+
var stageElapsed = Math.max(0, Number(progress.stageElapsedMs || 0));
|
|
291
|
+
var beat = Math.floor(stageElapsed / 4500);
|
|
292
|
+
var progressText = "";
|
|
293
|
+
if (total > 0 && stage === "reading-transcripts") {
|
|
294
|
+
progressText = "Reading " + number(visibleScanned) + " of " + number(total) + " sessions…";
|
|
295
|
+
} else if (stage === "building-summary") {
|
|
296
|
+
var summaryBeats = [
|
|
297
|
+
"Organizing what Echo found…",
|
|
298
|
+
"Building the report summary…"
|
|
299
|
+
];
|
|
300
|
+
progressText = summaryBeats[beat % summaryBeats.length];
|
|
301
|
+
} else if (stage === "classifying-repeated-context") {
|
|
302
|
+
var analysisCount = stageTotal > 0
|
|
303
|
+
? " · " + number(Math.min(stageDone, stageTotal)) + " of " + number(stageTotal)
|
|
304
|
+
: "";
|
|
305
|
+
var analysisBeats = [
|
|
306
|
+
"Comparing context across sessions" + analysisCount + "…",
|
|
307
|
+
"Finding repeated context" + analysisCount + "…",
|
|
308
|
+
"Separating useful context from noise" + analysisCount + "…"
|
|
309
|
+
];
|
|
310
|
+
progressText = analysisBeats[beat % analysisBeats.length];
|
|
311
|
+
} else if (stage === "finalizing-report") {
|
|
312
|
+
progressText = beat % 2 === 0 ? "Checking the report totals…" : "Preparing your report…";
|
|
192
313
|
}
|
|
314
|
+
var etaState = estimateReportRemaining(progress, overall, stage);
|
|
315
|
+
var etaText = etaState.overdue
|
|
316
|
+
? "Taking a little longer than estimated…"
|
|
317
|
+
: (etaState.seconds > 0 ? formatReportEta(etaState.seconds) : "Estimating time remaining…");
|
|
193
318
|
var fill = document.getElementById("scan-load-fill");
|
|
319
|
+
var caption = document.getElementById("scan-load-caption");
|
|
320
|
+
var eta = document.getElementById("scan-load-eta");
|
|
194
321
|
if (app.className === "scanLoading" && fill) {
|
|
195
322
|
fill.style.width = pct + "%";
|
|
196
323
|
fill.parentElement.setAttribute("aria-valuenow", String(pct));
|
|
197
|
-
|
|
198
|
-
if (
|
|
324
|
+
fill.parentElement.setAttribute("aria-valuetext", (progressText || "Preparing session count") + " " + etaText);
|
|
325
|
+
if (caption) {
|
|
326
|
+
caption.textContent = progressText;
|
|
327
|
+
caption.hidden = !progressText;
|
|
328
|
+
}
|
|
329
|
+
if (eta) eta.textContent = etaText;
|
|
199
330
|
return;
|
|
200
331
|
}
|
|
201
332
|
setScanMode(true);
|
|
202
|
-
setHead("
|
|
333
|
+
setHead("Building your report", "Local scan");
|
|
203
334
|
app.className = "scanLoading";
|
|
204
335
|
app.innerHTML =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
'</span>' +
|
|
336
|
+
setupJourney(1) +
|
|
337
|
+
'<section class="mvpReading" aria-labelledby="scanLoadingTitle">' +
|
|
338
|
+
'<span class="mvpReadingMascot scanMascotFrames" aria-hidden="true">' +
|
|
339
|
+
'<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-01-ready.png" alt="" />' +
|
|
340
|
+
'<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-02-open.png" alt="" />' +
|
|
341
|
+
'<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-03-pull.png" alt="" />' +
|
|
342
|
+
'<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-04-read.png" alt="" />' +
|
|
343
|
+
'<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-04-read.png" alt="" />' +
|
|
344
|
+
'<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-05-file.png" alt="" />' +
|
|
345
|
+
'<img class="scanMascotFrame" src="/hud-assets/scan-loop/frame-06-close.png" alt="" />' +
|
|
216
346
|
'</span>' +
|
|
217
|
-
'<h2>
|
|
218
|
-
'<p id="scan-load-caption"
|
|
219
|
-
'<
|
|
347
|
+
'<h2 id="scanLoadingTitle">Building your report…</h2>' +
|
|
348
|
+
'<p class="mvpReadingCount" id="scan-load-caption" aria-live="polite"' + (progressText ? "" : " hidden") + '>' + esc(progressText) + '</p>' +
|
|
349
|
+
'<p class="mvpReadingEta" id="scan-load-eta">' + esc(etaText) + '</p>' +
|
|
350
|
+
'<div class="scanProgress mvpReadingProgress" role="progressbar" aria-labelledby="scanLoadingTitle" aria-valuemin="0" aria-valuemax="100" aria-valuenow="' + pct + '" aria-valuetext="' + esc((progressText || "Preparing session count") + " " + etaText) + '">' +
|
|
220
351
|
'<i id="scan-load-fill" class="scanProgressFill" style="width:' + pct + '%"></i>' +
|
|
221
352
|
'</div>' +
|
|
222
353
|
'</section>';
|
|
223
|
-
if (!reportEstimateTimer) reportEstimateTimer = setInterval(updateEstimateCaption, 1000);
|
|
224
354
|
}
|
|
225
355
|
|
|
226
356
|
`;
|
|
@@ -1462,6 +1462,43 @@ export const SETUP_PAGE_STYLES_EXTRACTION = String.raw ` /* ---- dashboard (p
|
|
|
1462
1462
|
.savedHeading strong {
|
|
1463
1463
|
font-variant-numeric: tabular-nums;
|
|
1464
1464
|
}
|
|
1465
|
+
.firstRecallStage {
|
|
1466
|
+
display: flex;
|
|
1467
|
+
flex-direction: column;
|
|
1468
|
+
gap: clamp(22px, 3vw, 34px);
|
|
1469
|
+
}
|
|
1470
|
+
.firstRecallStage .resultTop h2 {
|
|
1471
|
+
max-width: none;
|
|
1472
|
+
margin: 0;
|
|
1473
|
+
color: var(--echo-ink-text);
|
|
1474
|
+
font-family: var(--echo-font-brand);
|
|
1475
|
+
font-size: clamp(42px, 6vw, 72px);
|
|
1476
|
+
font-weight: 700;
|
|
1477
|
+
line-height: 0.96;
|
|
1478
|
+
letter-spacing: -0.045em;
|
|
1479
|
+
}
|
|
1480
|
+
.firstRecallStage .savedHeading {
|
|
1481
|
+
display: block;
|
|
1482
|
+
}
|
|
1483
|
+
.firstRecallStage .savedHeading .savedMain {
|
|
1484
|
+
color: var(--echo-ink-text);
|
|
1485
|
+
}
|
|
1486
|
+
.firstRecallStage .savedHeading strong {
|
|
1487
|
+
color: var(--echo-ink-primary);
|
|
1488
|
+
font-weight: 900;
|
|
1489
|
+
}
|
|
1490
|
+
.firstRecallStage .tryCard {
|
|
1491
|
+
margin-top: 0;
|
|
1492
|
+
}
|
|
1493
|
+
.firstRecallStage .tryCard > .tryTitle {
|
|
1494
|
+
margin-bottom: 0;
|
|
1495
|
+
color: var(--echo-ink-primary);
|
|
1496
|
+
font-size: clamp(26px, 3vw, 38px);
|
|
1497
|
+
line-height: 1.04;
|
|
1498
|
+
}
|
|
1499
|
+
.firstRecallStage .recallPromptPrimary {
|
|
1500
|
+
margin-top: 18px;
|
|
1501
|
+
}
|
|
1465
1502
|
.viewEchoLink {
|
|
1466
1503
|
color: var(--echo-ink-mute);
|
|
1467
1504
|
font-style: normal;
|
|
@@ -165,7 +165,9 @@ export const SETUP_PAGE_STYLES_FOUNDATION = String.raw ` :root {
|
|
|
165
165
|
min-width: 10px;
|
|
166
166
|
border-radius: 0;
|
|
167
167
|
background: var(--echo-ink-primary);
|
|
168
|
-
|
|
168
|
+
/* Matched to the 700ms report poll so the fill glides between updates instead of
|
|
169
|
+
stepping and then sitting still for the rest of the interval. */
|
|
170
|
+
transition: width 700ms linear;
|
|
169
171
|
}
|
|
170
172
|
.scanCaption {
|
|
171
173
|
margin: 20px 0 0;
|