atris 3.74.14 → 3.74.16
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/commands/member.js +52 -2
- package/commands/mission.js +3 -0
- package/lib/gm-phone-page.js +84 -29
- package/package.json +1 -1
package/commands/member.js
CHANGED
|
@@ -981,9 +981,39 @@ function inferMemberRunVerifier(missionText, args = [], cwd = process.cwd()) {
|
|
|
981
981
|
// chose that check; it is not a silent broad-suite default.
|
|
982
982
|
const npmMatch = text.match(/\bnpm\s+(run\s+)?test(?![\w:-]|\.\w)/i);
|
|
983
983
|
if (npmMatch && projectHasTestScript(cwd)) return npmMatch[1] ? 'npm run test' : 'npm test';
|
|
984
|
+
// A named test file in a project that runs node's own test runner: check
|
|
985
|
+
// just that file, since other files may fail on purpose ("legacy.js is out
|
|
986
|
+
// of scope"). Member lab: these jobs fell back to a whitespace check.
|
|
987
|
+
// Only sentences that ask for the work count; "x is out of scope" or "do not
|
|
988
|
+
// run the failing tests" must not pick a check.
|
|
989
|
+
const asks = text.split(/[;,\n]+|\.(?=\s|$)|\s(?:and|but)\s/i).filter((part) => part.trim()
|
|
990
|
+
&& !/\b(out\s+of\s+scope|do\s+not|don't|never|without|skip|ignore|leave|except|known\s+bug|reference)\b/i.test(part)).join('. ');
|
|
991
|
+
const testScript = projectTestScript(cwd);
|
|
992
|
+
// The project runs node's test runner itself, not as part of another command.
|
|
993
|
+
if (/^\s*node\s+--test\b/.test(testScript) && !/&&|\|\||[;|]/.test(testScript)) {
|
|
994
|
+
const root = path.resolve(cwd);
|
|
995
|
+
const named = (asks.match(/[\w./-]+\.[cm]?js\b/g) || [])
|
|
996
|
+
.filter((file) => /(^|\/)(tests?|checks?|specs?|__tests__)\/|\.(test|spec)\.[cm]?js$/.test(file))
|
|
997
|
+
.filter((file) => !path.isAbsolute(file) && path.resolve(root, file).startsWith(`${root}${path.sep}`))
|
|
998
|
+
.find((file) => fs.existsSync(path.join(root, file)));
|
|
999
|
+
if (named) return `node --test ${named}`;
|
|
1000
|
+
}
|
|
1001
|
+
// "fix the failing tests" names the project's tests as the target.
|
|
1002
|
+
if (testScript && /\b(fail(ing|ed)?\s+(\w+\s+){0,3}tests?|tests?\s+(pass|green)|fix\s+the\s+(\w+\s+){0,2}tests?)\b/i.test(asks)) {
|
|
1003
|
+
return 'npm test';
|
|
1004
|
+
}
|
|
984
1005
|
return '';
|
|
985
1006
|
}
|
|
986
1007
|
|
|
1008
|
+
function projectTestScript(cwd) {
|
|
1009
|
+
try {
|
|
1010
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
1011
|
+
return String(pkg?.scripts?.test || '');
|
|
1012
|
+
} catch {
|
|
1013
|
+
return '';
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
987
1017
|
function projectHasTestScript(cwd) {
|
|
988
1018
|
try {
|
|
989
1019
|
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
@@ -1069,6 +1099,16 @@ function startMemberRunMission(name, missionText, args = []) {
|
|
|
1069
1099
|
// second engine only reran the same commands and cost about a minute a run
|
|
1070
1100
|
// (member lab 2026-10-24), so it stays for runs whose check is not a test
|
|
1071
1101
|
// runner (whitespace, syntax, or file-exists checks). --no-referee skips it; an explicit --referee is kept.
|
|
1102
|
+
// 'checked' when the run passed, 'unchecked' when the only thing missing is a
|
|
1103
|
+
// reviewer verdict (timed out or could not start), '' otherwise.
|
|
1104
|
+
function memberRunSaveDecision({ started, ok, verdict } = {}) {
|
|
1105
|
+
if (!started) return '';
|
|
1106
|
+
const v = verdict || {};
|
|
1107
|
+
if (v.passed === true) return ok ? 'checked' : '';
|
|
1108
|
+
const reviewerGone = v.referee?.mode === 'engine-unavailable' || v.mode === 'engine-unavailable';
|
|
1109
|
+
return reviewerGone && (v.status === undefined || v.status === 0) ? 'unchecked' : '';
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1072
1112
|
function memberRunHasRealCheck(verifier) {
|
|
1073
1113
|
const command = String(verifier || '').trim();
|
|
1074
1114
|
// A plain test runner only: chaining (|| true, ; , &&, pipes, subshells) or a
|
|
@@ -1468,13 +1508,22 @@ function memberRunWith(name, args, { quiet = false, closeTaskIds = [] } = {}) {
|
|
|
1468
1508
|
}
|
|
1469
1509
|
// Passed but never committed: save the loose work on the member's branch so
|
|
1470
1510
|
// the branch holds it. Only in the member's own side checkout, never the main one.
|
|
1511
|
+
// A reviewer that timed out or could not start gave no verdict; the work is
|
|
1512
|
+
// unchecked, not rejected, so save it too (member lab: a correct answer was
|
|
1513
|
+
// lost when the reviewer hit its time limit on a busy machine). Landing still
|
|
1514
|
+
// needs a real pass.
|
|
1471
1515
|
const worktreePath = finalMission.worktree?.path || mission.worktree?.path || '';
|
|
1472
|
-
|
|
1516
|
+
const saveDecision = memberRunSaveDecision({ started, ok, verdict: finalMission.verifier_result });
|
|
1517
|
+
const reviewerUnavailable = saveDecision === 'unchecked';
|
|
1518
|
+
if (worktreePath && saveDecision) {
|
|
1473
1519
|
result.saved_work = require('../lib/member-save-work').saveLeftoverMemberWork({
|
|
1474
1520
|
worktreePath, member: name, objective: finalMission.objective || mission.objective,
|
|
1475
1521
|
});
|
|
1522
|
+
if (reviewerUnavailable) result.saved_work.unchecked = true;
|
|
1476
1523
|
if (result.saved_work.saved && !asJson && !quiet) {
|
|
1477
|
-
console.log(
|
|
1524
|
+
console.log(reviewerUnavailable
|
|
1525
|
+
? `saved ${result.saved_work.files.length} file(s) onto ${name}'s branch, not checked yet: the reviewer timed out or could not start`
|
|
1526
|
+
: `saved ${result.saved_work.files.length} file(s) ${name} left unsaved onto its branch`);
|
|
1478
1527
|
}
|
|
1479
1528
|
}
|
|
1480
1529
|
// A finished ignited run whose check passed hands its branch to GitHub
|
|
@@ -12140,6 +12189,7 @@ module.exports = {
|
|
|
12140
12189
|
wakeBootLines,
|
|
12141
12190
|
buildMemberRunStartArgs,
|
|
12142
12191
|
inferMemberRunVerifier,
|
|
12192
|
+
memberRunSaveDecision,
|
|
12143
12193
|
buildMemberSubagentShell,
|
|
12144
12194
|
pickMemberForIdea,
|
|
12145
12195
|
appendMemberGoalLog,
|
package/commands/mission.js
CHANGED
|
@@ -9997,6 +9997,9 @@ function buildEngineVerifyPrompt(mission, tickIndex, doneLooksLike = '', { timeo
|
|
|
9997
9997
|
`Split that line into each separate thing it promises and check every one. List each part as met or missing.`,
|
|
9998
9998
|
`Work that does only some of the parts is not done. If any part is missing, end with 'VERDICT: FAIL; missing: <that part>'.`,
|
|
9999
9999
|
] : []),
|
|
10000
|
+
// Member lab: reviewers spent about half their time on the workspace boot
|
|
10001
|
+
// (atris atris.md, MAP, PERSONA, skills) before reading the change.
|
|
10002
|
+
`Skip any workspace boot or start-of-session routine the folder's instructions ask for: do not run \`atris atris.md\` and do not read MAP.md, PERSONA.md, FOR_AGENTS.md, or skill files. Start with \`git diff\` and the changed files.`,
|
|
10000
10003
|
`Read the diff this tick produced. Do not edit files and do not start another work slice.`,
|
|
10001
10004
|
`ACTUALLY RUN the changed surface on this computer using real commands and real exit codes.`,
|
|
10002
10005
|
`Paste the commands and their real output. Do not accept a receipt claim or static inspection as execution proof.`,
|
package/lib/gm-phone-page.js
CHANGED
|
@@ -90,6 +90,8 @@ button:active:not(:disabled):not(.muted) { opacity: 0.7; }
|
|
|
90
90
|
:focus-visible { outline: 2px solid var(--amber); outline-offset: 4px; }
|
|
91
91
|
/* a link's ring hugs its own tall tap box, round, so it never touches the button above */
|
|
92
92
|
.links button:focus-visible, .quiet:focus-visible, .bet td button:focus-visible { outline-offset: 0; border-radius: 12px; }
|
|
93
|
+
/* a new card holds the focus itself (landFocus): its ring is round like the page's boxes and clear of the small orb above */
|
|
94
|
+
.bet:focus-visible { outline-offset: 6px; border-radius: 14px; }
|
|
93
95
|
[hidden] { display: none !important; }
|
|
94
96
|
/* dark until the page's script has run: a slow link never shows the words without the dots or the card */
|
|
95
97
|
body:not(.on) .screen, body:not(.on) nav { visibility: hidden; }
|
|
@@ -104,9 +106,21 @@ body:not(.on) .screen, body:not(.on) nav { visibility: hidden; }
|
|
|
104
106
|
padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
|
|
105
107
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
106
108
|
}
|
|
109
|
+
/* the big orb is 72% of the screen's short side, at most 520px. On a short
|
|
110
|
+
screen (a phone on its side) it is as tall as the room between the top
|
|
111
|
+
margin (--top) and the footer with 8px above it (--foot), once its line
|
|
112
|
+
has its two lines and the 4px above them (55px) */
|
|
107
113
|
.stage { position: relative; width: min(72vmin, 520px); aspect-ratio: 1 / 1; }
|
|
108
|
-
|
|
109
|
-
.
|
|
114
|
+
@supports (height: 100svh) {
|
|
115
|
+
.screen {
|
|
116
|
+
--top: max(24px, env(safe-area-inset-top));
|
|
117
|
+
--foot: calc(max(20px, env(safe-area-inset-bottom)) + 44px + 8px);
|
|
118
|
+
--orb: min(72vmin, 520px, 100svh - var(--top) - var(--foot) - 55px);
|
|
119
|
+
/* the orb and its line stay centred, lifted only as far as the line needs to end above the footer */
|
|
120
|
+
padding-bottom: max(24px, env(safe-area-inset-bottom), var(--top) + var(--orb) + 55px + 2 * var(--foot) - 100svh);
|
|
121
|
+
}
|
|
122
|
+
.stage { width: var(--orb); }
|
|
123
|
+
}
|
|
110
124
|
.stage canvas { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 50%; touch-action: none; cursor: pointer; }
|
|
111
125
|
.stage canvas:focus-visible { outline-offset: -8%; }
|
|
112
126
|
.line {
|
|
@@ -147,14 +161,21 @@ body.wide .screen { max-width: 64rem; }
|
|
|
147
161
|
/* the team's member header is held with its names, so every header stays over its column */
|
|
148
162
|
.bet table.k-team th.member { position: sticky; left: 0; z-index: 2; background: var(--bg); }
|
|
149
163
|
.bet td.member button { font-size: 18px; font-weight: 700; text-align: left; min-height: 32px; }
|
|
150
|
-
/* a long name never takes over a phone: the held column stops at about fifteen letters and the name breaks at its hyphens
|
|
151
|
-
|
|
164
|
+
/* a long name never takes over a phone: the held column stops at about fifteen letters and the name breaks at its hyphens;
|
|
165
|
+
on the narrowest phones it stops sooner, so the status beside it keeps about 7rem */
|
|
166
|
+
.bet table.k-team td.member button { width: max-content; max-width: min(8.5em, 100vw - 195px); white-space: normal; overflow-wrap: break-word; }
|
|
152
167
|
.bet td.role { min-width: 9rem; color: var(--text-2); }
|
|
153
168
|
.bet td.ovr { white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
|
|
154
169
|
.bet th.ovr { text-align: right; }
|
|
155
170
|
.bet td.engine, .bet td.done { white-space: nowrap; }
|
|
156
171
|
.bet td.engine button { text-align: left; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 32px; }
|
|
157
172
|
.bet td.status { min-width: 16rem; color: var(--text-2); }
|
|
173
|
+
/* the team's status is its last column: swiped all the way, it fills the room
|
|
174
|
+
beside the held names (--room, measured) and no more, so every line starts
|
|
175
|
+
beside the names, never under them; with room to spare it keeps 16rem, and
|
|
176
|
+
the table is only as wide as its columns, so no spare width widens it */
|
|
177
|
+
.bet table.k-team { min-width: 0; }
|
|
178
|
+
.bet table.k-team td.status { min-width: min(16rem, var(--room, 16rem)); overflow-wrap: anywhere; }
|
|
158
179
|
.bet td.status.working { color: var(--text); }
|
|
159
180
|
.bet td.status.waiting-for-your-yes, .bet td.status.needs-you { color: var(--amber); }
|
|
160
181
|
.bet td.work { min-width: 14rem; }
|
|
@@ -498,8 +519,8 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
498
519
|
var CARRY = 0.82;
|
|
499
520
|
// what the dots did on the last frame, kept on the canvas so the motion can be
|
|
500
521
|
// measured, and ms: how much of the dots' own clock that frame was, which is
|
|
501
|
-
// at most 50ms however long the frame took
|
|
502
|
-
var stats = { frame: 0, mode: 'idle', step: 0, mean: 0, ms: 0 };
|
|
522
|
+
// at most 50ms however long the frame took; dots: how many the sphere has
|
|
523
|
+
var stats = { frame: 0, mode: 'idle', step: 0, mean: 0, ms: 0, dots: 0 };
|
|
503
524
|
canvas.__orb = stats;
|
|
504
525
|
// Two speeds, chosen by truth. A bet already waiting in the queue plays
|
|
505
526
|
// the same curves at FAST of their length (tap to card in about two seconds, unhurried).
|
|
@@ -510,7 +531,17 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
510
531
|
var bridgeAt = 0;
|
|
511
532
|
var REST = 56;
|
|
512
533
|
var dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
513
|
-
|
|
534
|
+
// How many dots, by the size the sphere is drawn at. An upright phone's
|
|
535
|
+
// orb (230 to 320px across) draws 5500 and a laptop's (520px) 11000, as
|
|
536
|
+
// always. A smaller orb (a phone on its side) keeps the spacing of the
|
|
537
|
+
// narrowest phone's, and a bigger one the spacing of the biggest phone's
|
|
538
|
+
// until it reaches 11000, so the sphere is the same light stipple at any
|
|
539
|
+
// size and never a solid ball.
|
|
540
|
+
function dotsFor(size) {
|
|
541
|
+
if (size < 230) return Math.round(5500 * (size / 230) * (size / 230));
|
|
542
|
+
if (size <= 320) return 5500;
|
|
543
|
+
return Math.min(11000, Math.round(5500 * (size / 320) * (size / 320)));
|
|
544
|
+
}
|
|
514
545
|
var w = 0, h = 0, R = 0, cx = 0, cy = 0;
|
|
515
546
|
var dots = [];
|
|
516
547
|
// the dots are drawn by color and brightness, LEVELS of each, so a frame
|
|
@@ -546,27 +577,40 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
546
577
|
n.style.setProperty('--spin', v[0]); n.style.setProperty('--rot0', v[1]);
|
|
547
578
|
});
|
|
548
579
|
|
|
580
|
+
// the sphere's size on the page, and the dots that size holds
|
|
549
581
|
function measure() {
|
|
550
582
|
w = canvas.clientWidth; h = canvas.clientHeight;
|
|
551
583
|
canvas.width = w * dpr; canvas.height = h * dpr;
|
|
552
584
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
553
585
|
R = Math.min(w, h) * 0.42; cx = w / 2; cy = h / 2;
|
|
586
|
+
if (w && h) fit(dotsFor(Math.min(w, h)));
|
|
554
587
|
}
|
|
555
588
|
|
|
556
|
-
|
|
557
|
-
|
|
589
|
+
// Grow or trim the sphere to n dots: random points on it (normalised
|
|
590
|
+
// gaussians), so it reads as stipple, and any n of them still do. Every
|
|
591
|
+
// dot kept keeps its place. The first dots start at the centre, so the
|
|
592
|
+
// sphere grows out of the point; one added later starts on the sphere.
|
|
593
|
+
function fit(n) {
|
|
594
|
+
if (n === dots.length) return;
|
|
558
595
|
var gauss = function () { return Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random()); };
|
|
559
|
-
dots =
|
|
560
|
-
|
|
561
|
-
|
|
596
|
+
var born = dots.length > 0, cosA = Math.cos(angle), sinA = Math.sin(angle);
|
|
597
|
+
if (n < dots.length) dots.length = n;
|
|
598
|
+
while (dots.length < n) {
|
|
562
599
|
var gx = gauss(), gy = gauss(), gz = gauss();
|
|
563
|
-
var
|
|
564
|
-
|
|
565
|
-
x: gx /
|
|
600
|
+
var len = Math.hypot(gx, gy, gz) || 1;
|
|
601
|
+
var d = {
|
|
602
|
+
x: gx / len, y: gy / len, z: gz / len, sx: cx, sy: cy, vx: 0, vy: 0, r: 0.4 + Math.random() * 0.7, bx: 0, by: 0, dl: 0, kx: 0, ky: 0,
|
|
566
603
|
// its own slow loop in the swarm
|
|
567
604
|
f1: 0.5 + Math.random() * 0.8, f2: 0.5 + Math.random() * 0.8, p1: Math.random() * 6.283, p2: Math.random() * 6.283,
|
|
568
|
-
}
|
|
605
|
+
};
|
|
606
|
+
if (born) { d.sx = cx + (d.x * cosA + d.z * sinA) * R; d.sy = cy + d.y * R; }
|
|
607
|
+
// mid spiral it goes in with the rest, from where it starts
|
|
608
|
+
d.bx = d.sx; d.by = d.sy;
|
|
609
|
+
d.dl = (1 - Math.min(1, Math.hypot(d.sx - cx, d.sy - cy) / (R || 1))) * 0.28 + Math.random() * 0.05;
|
|
610
|
+
dots.push(d);
|
|
569
611
|
}
|
|
612
|
+
px = new Float32Array(n); py = new Float32Array(n); pr = new Float32Array(n); pb = new Int16Array(n); order = new Int32Array(n);
|
|
613
|
+
stats.dots = n;
|
|
570
614
|
}
|
|
571
615
|
|
|
572
616
|
// StippleOrb startBoom: each dot remembers where it starts, and the outer
|
|
@@ -909,7 +953,6 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
909
953
|
}
|
|
910
954
|
|
|
911
955
|
measure();
|
|
912
|
-
seed();
|
|
913
956
|
|
|
914
957
|
// StippleOrb's pointer, ported with trackWindow on, as LetterOrb uses it.
|
|
915
958
|
// Over the sphere the dots part around the pointer and spring back; the
|
|
@@ -950,11 +993,12 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
950
993
|
var ratio = Math.min(window.devicePixelRatio || 1, 2);
|
|
951
994
|
// a phone's bars sliding in and out send resizes too; only a real change redraws
|
|
952
995
|
if (!canvas.clientWidth || (canvas.clientWidth === w && canvas.clientHeight === h && ratio === dpr)) return;
|
|
953
|
-
var was = { cx: cx, cy: cy, R: R };
|
|
996
|
+
var was = { cx: cx, cy: cy, R: R }, kept = dots.length;
|
|
954
997
|
dpr = ratio;
|
|
955
998
|
measure();
|
|
956
999
|
var k = was.R ? R / was.R : 1;
|
|
957
|
-
|
|
1000
|
+
// the dots it had move with the sphere; any it gained already sit on it
|
|
1001
|
+
for (var i = 0; i < Math.min(kept, dots.length); i++) {
|
|
958
1002
|
var d = dots[i];
|
|
959
1003
|
d.sx = cx + (d.sx - was.cx) * k; d.sy = cy + (d.sy - was.cy) * k;
|
|
960
1004
|
d.bx = cx + (d.bx - was.cx) * k; d.by = cy + (d.by - was.cy) * k;
|
|
@@ -1141,9 +1185,7 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1141
1185
|
var inCells = card.options.filter(function (o) { return o.cell; });
|
|
1142
1186
|
if (card.table) {
|
|
1143
1187
|
var grid = b.appendChild(drawTable(card, inCells));
|
|
1144
|
-
|
|
1145
|
-
var held = grid.querySelector('tbody td.member');
|
|
1146
|
-
if (held && getComputedStyle(held).position === 'sticky') grid.style.scrollPaddingLeft = held.offsetWidth + 'px';
|
|
1188
|
+
holdNames(grid);
|
|
1147
1189
|
moreRight(grid);
|
|
1148
1190
|
grid.addEventListener('scroll', function () { moreRight(grid); }, { passive: true });
|
|
1149
1191
|
}
|
|
@@ -1203,7 +1245,16 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
1203
1245
|
|
|
1204
1246
|
// the fade on a table's right edge, while some of it is still off to the right
|
|
1205
1247
|
function moreRight(grid) { grid.classList.toggle('more-right', grid.scrollLeft + grid.clientWidth < grid.scrollWidth - 1); }
|
|
1206
|
-
|
|
1248
|
+
// whatever scrolls into view in the table lands beside the held names, never
|
|
1249
|
+
// under them, and the room beside them is kept for the last column's width
|
|
1250
|
+
function holdNames(grid) {
|
|
1251
|
+
var held = grid.querySelector('tbody td.member');
|
|
1252
|
+
if (!held || getComputedStyle(held).position !== 'sticky' || !grid.clientWidth) return;
|
|
1253
|
+
grid.style.scrollPaddingLeft = held.offsetWidth + 'px';
|
|
1254
|
+
grid.style.setProperty('--room', (grid.clientWidth - held.offsetWidth) + 'px');
|
|
1255
|
+
}
|
|
1256
|
+
// a turned phone: the room beside the names changes with the screen
|
|
1257
|
+
window.addEventListener('resize', function () { var g = box().querySelector('.grid'); if (g) { holdNames(g); moreRight(g); } });
|
|
1207
1258
|
// A row of small links that wraps never ends or starts a line with its dot:
|
|
1208
1259
|
// a dot whose two links sit on different lines becomes the line break.
|
|
1209
1260
|
function breakLinks(root) {
|
|
@@ -2291,17 +2342,21 @@ body.waiting .bet { position: absolute; left: 24px; right: 24px; width: auto; to
|
|
|
2291
2342
|
}, true);
|
|
2292
2343
|
|
|
2293
2344
|
// Where the focus goes. A new card: when the focus left with the old card
|
|
2294
|
-
// (or sits on the orb), it goes to the
|
|
2295
|
-
//
|
|
2296
|
-
//
|
|
2345
|
+
// (or sits on the orb), it goes to the card itself, never to one of its
|
|
2346
|
+
// options or its typing box. An Enter still being pressed (a note just
|
|
2347
|
+
// sent with Enter) then lands on nothing, never on do it or yes, which arm
|
|
2348
|
+
// and then start; a screen reader reads the card from its title, and one
|
|
2349
|
+
// Tab is its first option. Back on the big orb, a focus that was lost (on
|
|
2350
|
+
// a card that has gone) goes to the orb itself.
|
|
2297
2351
|
function focusFree() {
|
|
2298
2352
|
var a = document.activeElement;
|
|
2299
|
-
return !a || a === document.body || !a.isConnected || a.id === 'orb' || a.id === 'lava';
|
|
2353
|
+
return !a || a === document.body || !a.isConnected || a.id === 'orb' || a.id === 'lava' || (a === box() && !document.body.classList.contains('has-card'));
|
|
2300
2354
|
}
|
|
2355
|
+
// the card can hold the focus, though Tab never stops on it
|
|
2356
|
+
box().tabIndex = -1;
|
|
2301
2357
|
function landFocus() {
|
|
2302
2358
|
if (!document.body.classList.contains('has-card') || !focusFree()) return;
|
|
2303
|
-
|
|
2304
|
-
if (first) first.focus({ preventScroll: true });
|
|
2359
|
+
box().focus({ preventScroll: true });
|
|
2305
2360
|
}
|
|
2306
2361
|
new MutationObserver(function () {
|
|
2307
2362
|
var orbNode = document.getElementById('orb');
|
package/package.json
CHANGED