atris 3.74.4 → 3.74.6
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/FOR_AGENTS.md +12 -12
- package/atris/AGENTS.md +7 -7
- package/atris/CLAUDE.md +10 -10
- package/atris/policies/atris-backend.md +1 -1
- package/atris/policies/atris-design.md +71 -71
- package/atris/policies/design-seed.md +28 -28
- package/atris/policies/writing.md +1 -1
- package/atris/skills/apps/SKILL.md +3 -3
- package/atris/skills/atris-feedback/SKILL.md +2 -2
- package/atris/skills/autopilot/SKILL.md +10 -10
- package/atris/skills/blocks/SKILL.md +16 -16
- package/atris/skills/calendar/SKILL.md +1 -1
- package/atris/skills/clawhub/atris/SKILL.md +4 -4
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +7 -7
- package/atris/skills/create-app/SKILL.md +18 -18
- package/atris/skills/create-member/SKILL.md +11 -11
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/drive/SKILL.md +25 -25
- package/atris/skills/email-agent/SKILL.md +11 -11
- package/atris/skills/endgame/SKILL.md +33 -33
- package/atris/skills/magic-inbox/SKILL.md +15 -15
- package/atris/skills/notion/SKILL.md +1 -1
- package/atris/skills/ramp/SKILL.md +7 -7
- package/atris/skills/research-search/SKILL.md +7 -7
- package/atris/skills/slides/SKILL.md +7 -7
- package/atris/skills/tidy/SKILL.md +11 -11
- package/atris/skills/wake/SKILL.md +11 -11
- package/atris/skills/writing/SKILL.md +5 -5
- package/atris/team/_template/SOUL.md +3 -3
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/launcher/MEMBER.md +2 -2
- package/atris/team/mission-lead/MISSION.md +7 -7
- package/atris/team/opus-overnight/MISSION.md +16 -16
- package/atris/team/opus-overnight/STEERING.md +5 -5
- package/atris.md +9 -0
- package/bin/atris.js +16 -3
- package/commands/campaign.js +17 -2
- package/commands/card.js +19 -12
- package/commands/engine.js +92 -17
- package/commands/gm.js +30 -0
- package/commands/idea.js +263 -0
- package/commands/init.js +11 -2
- package/commands/land.js +39 -18
- package/commands/mission.js +4 -3
- package/commands/ninety-nine.js +288 -0
- package/commands/one-lap.js +46 -27
- package/commands/reel.js +12 -7
- package/commands/sync.js +10 -2
- package/commands/task.js +4 -2
- package/commands/xp.js +68 -15
- package/commands/zen.js +10 -3
- package/lib/campaign-keep.js +23 -8
- package/lib/campaign-loop.js +24 -5
- package/lib/career-xp-lock.js +61 -0
- package/lib/career-xp-root.js +61 -0
- package/lib/claude-boot-block.js +7 -4
- package/lib/engine-registry.js +139 -1
- package/lib/fleet.js +11 -2
- package/lib/gm-bet.js +734 -42
- package/lib/gm-card.js +431 -0
- package/lib/gm-phone-page.js +486 -717
- package/lib/gm-phone.js +33 -34
- package/lib/gm-season.js +14 -0
- package/lib/headless-chrome.js +39 -0
- package/lib/idea-gate.js +33 -0
- package/lib/idea-inbox.js +220 -0
- package/lib/idea-ledger.js +196 -0
- package/lib/idea-tick.js +384 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +1 -0
- package/lib/ninety-nine-sync.js +156 -0
- package/lib/ninety-nine.js +247 -0
- package/lib/opening-lines.js +94 -0
- package/lib/roster-markdown.js +39 -1
- package/lib/roster-models.js +1 -1
- package/lib/roster-swap.js +353 -0
- package/lib/spawn-env.js +29 -2
- package/lib/task-db.js +8 -4
- package/lib/wish-audit.js +14 -0
- package/lib/yes-lock.js +365 -256
- package/lib/zen-apply.js +6 -3
- package/lib/zen.js +28 -1
- package/package.json +1 -1
- package/templates/business-starter/CLAUDE.md +7 -7
- package/templates/business-starter/MAP.md +1 -1
- package/templates/business-starter/TODO.md +1 -1
- package/templates/business-starter/atris.md +2 -2
- package/templates/business-starter/context/README.md +6 -6
- package/templates/business-starter/context/live-workspace.md +1 -1
- package/templates/business-starter/goals.md +1 -1
- package/templates/business-starter/instructions.md +1 -1
- package/templates/business-starter/memory.md +4 -4
- package/templates/business-starter/persona.md +3 -3
- package/templates/business-starter/policies/LESSONS.md +1 -1
- package/templates/business-starter/reports/README.md +1 -1
- package/templates/business-starter/skills/README.md +1 -1
- package/templates/business-starter/team/README.md +1 -1
- package/templates/business-starter/team/START_HERE.md +1 -1
- package/templates/business-starter/team/_template/SOUL.md +3 -3
- package/templates/business-starter/team/comms/SOUL.md +2 -2
- package/templates/business-starter/team/ops/SOUL.md +4 -4
- package/templates/business-starter/team/research/SOUL.md +3 -3
- package/templates/business-starter/wiki/index.md +1 -1
- package/templates/research-canonical/CLAUDE.md +7 -7
- package/templates/research-canonical/MAP.md +1 -1
- package/templates/research-canonical/TODO.md +1 -1
- package/templates/research-canonical/atris.md +2 -2
- package/templates/research-canonical/context/README.md +8 -8
- package/templates/research-canonical/context/live-workspace.md +1 -1
- package/templates/research-canonical/goals.md +2 -2
- package/templates/research-canonical/instructions.md +1 -1
- package/templates/research-canonical/memory.md +4 -4
- package/templates/research-canonical/persona.md +1 -1
- package/templates/research-canonical/policies/LESSONS.md +1 -1
- package/templates/research-canonical/reports/README.md +1 -1
- package/templates/research-canonical/skills/README.md +1 -1
- package/templates/research-canonical/team/README.md +1 -1
- package/templates/research-canonical/team/eval/SOUL.md +1 -1
- package/templates/research-canonical/team/experiment/SOUL.md +2 -2
- package/templates/research-canonical/team/hypothesis/SOUL.md +3 -3
- package/templates/research-canonical/team/literature/SOUL.md +2 -2
- package/templates/research-canonical/wiki/briefs/research-program.md +1 -1
- package/templates/research-canonical/wiki/index.md +1 -1
- package/utils/auth.js +1 -0
package/lib/gm-phone-page.js
CHANGED
|
@@ -71,19 +71,38 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
|
71
71
|
.line.out { opacity: 0; }
|
|
72
72
|
.line.bad { color: var(--text-2); }
|
|
73
73
|
.foot { position: absolute; left: 0; right: 0; bottom: max(20px, env(safe-area-inset-bottom)); text-align: center; }
|
|
74
|
+
/* the yeses still waiting: a small line beside the team, never a card on its own */
|
|
75
|
+
#needs { margin-right: 20px; color: var(--amber); }
|
|
74
76
|
.quiet { color: var(--text-2); font-size: 17px; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 44px; }
|
|
75
|
-
/*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
.
|
|
80
|
-
.
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
.
|
|
77
|
+
/* a card with a table (the team, one job) is wider, and at phone width the
|
|
78
|
+
table scrolls sideways with the name column held in place; a line between
|
|
79
|
+
every column and every row, and no column narrower than its words */
|
|
80
|
+
body.wide .screen { max-width: 64rem; }
|
|
81
|
+
.grid { margin: 14px 0 0; overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; border: 1px solid var(--line); border-radius: 14px; }
|
|
82
|
+
.bet table { width: 100%; min-width: 44rem; border-collapse: separate; border-spacing: 0; font-size: 17px; line-height: 1.35; }
|
|
83
|
+
.bet table.k-deck { min-width: 56rem; }
|
|
84
|
+
.bet th, .bet td { padding: 10px 14px; text-align: left; vertical-align: top; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
85
|
+
.bet th:last-child, .bet td:last-child { border-right: 0; }
|
|
86
|
+
.bet tbody tr:last-child td { border-bottom: 0; }
|
|
87
|
+
.bet th { color: var(--text-2); font-weight: 400; white-space: nowrap; }
|
|
88
|
+
.bet tr.row { cursor: pointer; }
|
|
89
|
+
@media (hover: hover) { .bet tr.row:hover td { background: #1c1816; } }
|
|
90
|
+
.bet td.member { position: sticky; left: 0; z-index: 1; background: var(--bg); white-space: nowrap; }
|
|
91
|
+
.bet td.member button { font-size: 18px; font-weight: 700; text-align: left; min-height: 32px; }
|
|
92
|
+
.bet td.role { min-width: 9rem; color: var(--text-2); }
|
|
93
|
+
.bet td.ovr { white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
|
|
94
|
+
.bet td.engine, .bet td.done { white-space: nowrap; }
|
|
95
|
+
.bet td.engine button { text-align: left; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 32px; }
|
|
96
|
+
.bet td.status { min-width: 16rem; color: var(--text-2); }
|
|
97
|
+
.bet td.status.working { color: var(--text); }
|
|
98
|
+
.bet td.status.waiting-for-your-yes, .bet td.status.needs-you { color: var(--amber); }
|
|
99
|
+
.bet td.work { min-width: 14rem; }
|
|
100
|
+
/* a job deck card: its place in the deck and its status above the title */
|
|
101
|
+
.bet .count, .bet .label, .bet .time { display: block; font-size: 17px; color: var(--text-2); }
|
|
102
|
+
.bet .label.needs-you { color: var(--amber); }
|
|
103
|
+
.bet .time { margin: 10px 0 0; }
|
|
104
|
+
.jobline { margin: 10px 0 0; color: var(--text-2); font-size: 17px; }
|
|
105
|
+
.thread { margin: 12px 0 0; }
|
|
87
106
|
.sayline { margin: 10px 0 0; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
88
107
|
.sayline .thread, .sayline .reply { width: 100%; text-align: left; margin: 0; }
|
|
89
108
|
.unsent { margin: 0; text-align: center; }
|
|
@@ -129,6 +148,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
129
148
|
.bet { width: 100%; opacity: 0; transform: translateY(20px); transition: opacity 0.45s ease-out, transform 0.55s cubic-bezier(0.22, 1, 0.36, 1); }
|
|
130
149
|
.bet.up { opacity: 1; transform: none; }
|
|
131
150
|
.bet.quick { transition-duration: 0.4s, 0.5s; }
|
|
151
|
+
/* after an act the next card fades up as the sphere settles, so the whole motion is about a
|
|
152
|
+
second, and its title drifts only a few pixels into the spot the last title had */
|
|
153
|
+
.bet.acted { transform: translateY(4px); transition-duration: 0.28s, 0.36s; }
|
|
154
|
+
.bet.acted.up { transform: none; }
|
|
132
155
|
/* card to card: a crossfade in place. the old card is a still copy that
|
|
133
156
|
fades over the box while the new card fades in under it from a small
|
|
134
157
|
drift, its title landing where the old title was; the box eases to the
|
|
@@ -193,7 +216,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
193
216
|
</div>
|
|
194
217
|
<p class="line" id="line" aria-live="polite">tap to think</p>
|
|
195
218
|
<article class="bet" id="bet" hidden></article>
|
|
196
|
-
<div class="foot" id="foot"><button type="button" class="quiet" id="
|
|
219
|
+
<div class="foot" id="foot"><button type="button" class="quiet" id="needs" hidden></button><button type="button" class="quiet" id="working">the team</button></div>
|
|
197
220
|
</main>
|
|
198
221
|
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap to go back to the big orb">
|
|
199
222
|
<div class="tx" style-vars="15%,8.4s,-2.1s"><div class="ty" style-vars="12%,10.1s,-5.1s"><div class="blob b1" spin-vars="36s,0deg"></div></div></div>
|
|
@@ -202,7 +225,6 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
202
225
|
<div class="core" id="lava-core"></div>
|
|
203
226
|
<div class="dim" id="lava-dim"></div>
|
|
204
227
|
</div>
|
|
205
|
-
<section class="team" id="team" hidden role="dialog" aria-label="the team"><div class="inner" id="team-inner"></div></section>
|
|
206
228
|
<script nonce="${nonce}">
|
|
207
229
|
(function () {
|
|
208
230
|
var token = new URLSearchParams(location.search).get('t') || '';
|
|
@@ -223,7 +245,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
223
245
|
opts.body = JSON.stringify(body);
|
|
224
246
|
}
|
|
225
247
|
return fetch(path + (body ? '' : '?t=' + encodeURIComponent(token)), opts)
|
|
226
|
-
.then(function (r) { return r.json().catch(function () { return { ok: false }; }).then(function (j) { j.http = r.status; return j; }); });
|
|
248
|
+
.then(function (r) { return r.json().catch(function () { return { ok: false }; }).then(function (j) { j.http = r.status; if (typeof j.needs_you === 'number') drawNeeds(j.needs_you); return j; }); });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// The yeses still waiting past the card on screen: one small line, and a
|
|
252
|
+
// tap opens the job deck with the ones that need the owner first.
|
|
253
|
+
function drawNeeds(n) {
|
|
254
|
+
var btn = document.getElementById('needs');
|
|
255
|
+
btn.hidden = !n;
|
|
256
|
+
btn.textContent = n ? (n === 1 ? '1 needs you' : n + ' need you') : '';
|
|
227
257
|
}
|
|
228
258
|
|
|
229
259
|
// ---- the one line under the orb: fades out, changes, fades in ------------------
|
|
@@ -274,6 +304,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
274
304
|
// the fast reveal stays contained: the lava grows to BLOOM of the smaller
|
|
275
305
|
// window side and never washes the page
|
|
276
306
|
var FAST_GROW_MS = 460, FAST_SETTLE_MS = 440, BLOOM = 0.72;
|
|
307
|
+
// the owner acted on a card: one motion of about a second when the answer
|
|
308
|
+
// is in. The card fades while the small orb glides into the sphere, the
|
|
309
|
+
// dots spiral in at ACT_PACE, and the next card blooms where they land.
|
|
310
|
+
var ACT_LEAVE_MS = 160, ACT_GLIDE_MS = 180, ACT_BEAT_MS = 100, ACT_PACE = 0.24, ACT_GROW_MS = 180, ACT_SETTLE_MS = 260;
|
|
311
|
+
var quick = false;
|
|
277
312
|
// Two speeds, chosen by truth. A bet already waiting in the queue plays
|
|
278
313
|
// the same curves at FAST of their length (tap to card in about two seconds, unhurried).
|
|
279
314
|
// A bet still being made plays the full landing length, and the spiral
|
|
@@ -463,8 +498,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
463
498
|
function grow() {
|
|
464
499
|
var rect = canvas.getBoundingClientRect();
|
|
465
500
|
var contained = pace < 1;
|
|
466
|
-
var growMs = contained ? FAST_GROW_MS : (GROW_MS * pace);
|
|
467
|
-
var settleMs = contained ? FAST_SETTLE_MS : (SETTLE_MS * pace);
|
|
501
|
+
var growMs = quick ? ACT_GROW_MS : contained ? FAST_GROW_MS : (GROW_MS * pace);
|
|
502
|
+
var settleMs = quick ? ACT_SETTLE_MS : contained ? FAST_SETTLE_MS : (SETTLE_MS * pace);
|
|
468
503
|
var vx = rect.left + cx, vy = rect.top + cy;
|
|
469
504
|
var reach = contained
|
|
470
505
|
? BLOOM * Math.min(window.innerWidth, window.innerHeight) / 2
|
|
@@ -536,7 +571,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
536
571
|
// swap() takes the card off the page once it has faded. At the centre the
|
|
537
572
|
// lava melts and the sphere grows back out of the point, holds for a
|
|
538
573
|
// beat so it reads, then calls back. Reduced motion: no travel.
|
|
539
|
-
|
|
574
|
+
// fast: the owner acted, so the glide is the short one
|
|
575
|
+
function melt(cb, to, swap, fast) {
|
|
576
|
+
var leaveMs = fast ? ACT_LEAVE_MS : LEAVE_MS, glideMs = fast ? ACT_GLIDE_MS : GLIDE_MS, beatMs = fast ? ACT_BEAT_MS : BEAT_MS;
|
|
540
577
|
lava.classList.remove('tap', 'alive');
|
|
541
578
|
lava.tabIndex = -1;
|
|
542
579
|
var swapped = false;
|
|
@@ -553,7 +590,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
553
590
|
mode = 'idle';
|
|
554
591
|
bridgeAt = reduced ? 0 : performance.now();
|
|
555
592
|
start();
|
|
556
|
-
setTimeout(cb, reduced ? 0 :
|
|
593
|
+
setTimeout(cb, reduced ? 0 : beatMs);
|
|
557
594
|
};
|
|
558
595
|
if (reduced || lava.style.visibility !== 'visible') { reform(); return; }
|
|
559
596
|
var r = lava.getBoundingClientRect();
|
|
@@ -564,9 +601,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
564
601
|
var sEnd = 30 / base;
|
|
565
602
|
var t0 = performance.now();
|
|
566
603
|
var step = function (now) {
|
|
567
|
-
var k = Math.min(1, (now - t0) /
|
|
604
|
+
var k = Math.min(1, (now - t0) / glideMs);
|
|
568
605
|
var t = ease(k);
|
|
569
|
-
if (now - t0 >=
|
|
606
|
+
if (now - t0 >= leaveMs) takeOff();
|
|
570
607
|
place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
|
|
571
608
|
core.style.opacity = String(smooth(0.3, 0.9, k));
|
|
572
609
|
lava.style.opacity = String(1 - smooth(0.7, 1, k));
|
|
@@ -612,11 +649,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
612
649
|
|
|
613
650
|
return {
|
|
614
651
|
// the bet is ready: the homepage tap, on the clock
|
|
615
|
-
|
|
652
|
+
// act: the owner acted on a card, so it is the one quick motion
|
|
653
|
+
play: function (cb, fast, act) {
|
|
616
654
|
onBorn = cb;
|
|
655
|
+
quick = Boolean(act);
|
|
617
656
|
if (reduced) { settled(); return; }
|
|
618
|
-
pace = fast ? FAST : 1;
|
|
619
|
-
|
|
657
|
+
pace = act ? ACT_PACE : fast ? FAST : 1;
|
|
658
|
+
// after an act the dots spiral in from where the re-formed sphere is
|
|
659
|
+
if (mode !== 'pull') startPull(fast || Boolean(act));
|
|
620
660
|
timed = true;
|
|
621
661
|
start();
|
|
622
662
|
},
|
|
@@ -643,181 +683,191 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
643
683
|
};
|
|
644
684
|
})();
|
|
645
685
|
|
|
646
|
-
// ---- the
|
|
647
|
-
|
|
648
|
-
//
|
|
686
|
+
// ---- the card: one renderer, one way to act --------------------------------------
|
|
687
|
+
// The page draws exactly the card the Mac served, with draw(card), and
|
|
688
|
+
// nothing else draws a card. Its options and its box come only from that
|
|
689
|
+
// card, and nothing from the card before survives a draw. Every tap posts
|
|
690
|
+
// the card's id and the option's id, or the words typed in its box, to
|
|
691
|
+
// /card; the Mac refuses anything else, and its answer is the next card.
|
|
692
|
+
var bet = { shown: null, watching: null, tapAt: 0 };
|
|
693
|
+
// while an act or a turn is under way the card takes no taps; the page
|
|
694
|
+
// marks it on the body too, so anyone reading the page can tell
|
|
695
|
+
var busyNow = false;
|
|
696
|
+
Object.defineProperty(bet, 'busy', { get: function () { return busyNow; }, set: function (v) { busyNow = Boolean(v); document.body.toggleAttribute('data-busy', busyNow); } });
|
|
697
|
+
// how long a line like "builder has it" stays under the orb before the next card
|
|
649
698
|
var AFTER_MS = 1500;
|
|
699
|
+
// a tapped option holds as chosen this long before the motion starts
|
|
700
|
+
var HOLD_MS = 150;
|
|
701
|
+
// what turns in place instead of the spiral: flipping the deck, more
|
|
702
|
+
// models, and who a swap is for are the same card moving, not an answer
|
|
703
|
+
var IN_PLACE = ['flip', 'more', 'scope'];
|
|
650
704
|
|
|
651
705
|
function buzz() { if (navigator.vibrate) { try { navigator.vibrate(10); } catch (e) { /* not on this device */ } } }
|
|
652
706
|
|
|
653
|
-
//
|
|
654
|
-
function
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
707
|
+
// a failed fetch reads like a refusal, so nothing ever hangs
|
|
708
|
+
function offline() { return { ok: false, http: 0, message: 'that did not reach the mac. try again.' }; }
|
|
709
|
+
|
|
710
|
+
function box() { return document.getElementById('bet'); }
|
|
711
|
+
|
|
712
|
+
// ---- draw: the one place a card becomes the page -----------------------------------
|
|
713
|
+
function draw(card) {
|
|
714
|
+
var b = box();
|
|
715
|
+
b.textContent = '';
|
|
716
|
+
b.setAttribute('data-card', card.id);
|
|
717
|
+
b.setAttribute('data-kind', card.kind);
|
|
718
|
+
document.body.classList.toggle('wide', Boolean(card.table));
|
|
719
|
+
if (card.count) b.appendChild(el('span', 'count', card.count));
|
|
720
|
+
if (card.label) b.appendChild(el('span', 'label ' + card.label.replace(/ /g, '-'), card.label));
|
|
721
|
+
var title = b.appendChild(el('h1', null, card.title));
|
|
722
|
+
// a reply of more than one line reads at the body size, in the same spot
|
|
723
|
+
if (card.kind === 'reply') {
|
|
724
|
+
var lines = document.createRange();
|
|
725
|
+
lines.selectNodeContents(title);
|
|
726
|
+
if (lines.getClientRects().length > 1) title.classList.add('said');
|
|
727
|
+
}
|
|
728
|
+
if (card.body) b.appendChild(el('p', 'why', card.body));
|
|
729
|
+
// a reply keeps the title's spot; the turns so far sit quietly under it
|
|
730
|
+
if (card.thread && card.thread.length) {
|
|
731
|
+
var thread = b.appendChild(el('div', 'thread'));
|
|
732
|
+
card.thread.forEach(function (t) { thread.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text)); });
|
|
733
|
+
}
|
|
734
|
+
// who it is and what they do; the rating and the model live on the team table
|
|
735
|
+
if (card.job && !card.table) b.appendChild(el('p', 'jobline', [card.job.member, card.job.role].filter(Boolean).join(' · ')));
|
|
736
|
+
var inCells = card.options.filter(function (o) { return o.cell; });
|
|
737
|
+
if (card.table) b.appendChild(drawTable(card, inCells));
|
|
738
|
+
if (card.time) b.appendChild(el('span', 'time', card.time));
|
|
739
|
+
if (card.line) b.appendChild(el('p', 'mission', card.line));
|
|
740
|
+
// the options in the card's own order; quiet ones next to each other share one line
|
|
741
|
+
var rest = card.options.filter(function (o) { return !o.cell; });
|
|
742
|
+
var quiet = function (o) { return !o.style || o.style === 'quiet'; };
|
|
743
|
+
var act = el('div', 'act' + (rest.some(function (o) { return o.style === 'choice' || o.style === 'option'; }) ? ' answers' : ''));
|
|
744
|
+
var links = null;
|
|
745
|
+
var group = null;
|
|
746
|
+
rest.forEach(function (o) {
|
|
747
|
+
if (!quiet(o)) { links = null; act.appendChild(optionButton(o)); return; }
|
|
748
|
+
// a group of quiet options shares one line, headed by its words
|
|
749
|
+
if (links && (o.group || null) === group) links.appendChild(document.createTextNode(' · '));
|
|
750
|
+
else {
|
|
751
|
+
links = act.appendChild(el('div', 'links'));
|
|
752
|
+
group = o.group || null;
|
|
753
|
+
if (group) links.appendChild(document.createTextNode(group + ': '));
|
|
672
754
|
}
|
|
673
|
-
|
|
674
|
-
run();
|
|
755
|
+
links.appendChild(optionButton(o));
|
|
675
756
|
});
|
|
757
|
+
// a quiet line under the options, empty until something does not go through
|
|
758
|
+
act.appendChild(el('p', 'links note'));
|
|
759
|
+
b.appendChild(act);
|
|
760
|
+
if (card.input) b.appendChild(inputLine(card.input));
|
|
676
761
|
}
|
|
677
762
|
|
|
678
|
-
// a line
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
} else if (s.kind === 'wait') {
|
|
701
|
-
// one calm line: the idea turns in on its own once it is written
|
|
702
|
-
watchWait();
|
|
703
|
-
box.appendChild(sayBox());
|
|
704
|
-
return;
|
|
705
|
-
} else {
|
|
706
|
-
var ok = el('button', 'pill', 'nice');
|
|
707
|
-
ok.type = 'button';
|
|
708
|
-
ok.addEventListener('click', function () {
|
|
709
|
-
buzz();
|
|
710
|
-
api('/bet/nice', {}).then(function (r) { moveOn(r.ack || 'nice.'); });
|
|
763
|
+
// a table: one row per line, a line between every cell; an option with a
|
|
764
|
+
// cell is drawn in that cell, and a tap anywhere on its row opens the
|
|
765
|
+
// row's first option
|
|
766
|
+
function drawTable(card, inCells) {
|
|
767
|
+
var t = card.table;
|
|
768
|
+
var grid = el('div', 'grid');
|
|
769
|
+
var table = grid.appendChild(el('table', 'k-' + card.kind));
|
|
770
|
+
var head = el('tr');
|
|
771
|
+
t.columns.forEach(function (c) { head.appendChild(el('th', c, c)).scope = 'col'; });
|
|
772
|
+
table.appendChild(el('thead')).appendChild(head);
|
|
773
|
+
var body = table.appendChild(el('tbody'));
|
|
774
|
+
t.rows.forEach(function (row, i) {
|
|
775
|
+
var mine = inCells.filter(function (o) { return o.cell.row === i; });
|
|
776
|
+
var tr = body.appendChild(el('tr', mine.length ? 'row' : null));
|
|
777
|
+
row.forEach(function (cell, j) {
|
|
778
|
+
var col = t.columns[j];
|
|
779
|
+
var td = tr.appendChild(el('td', col + (col === 'status' && t.status ? ' ' + String(t.status[i] || '').replace(/ /g, '-') : '')));
|
|
780
|
+
var o = mine.filter(function (x) { return x.cell.column === col; })[0];
|
|
781
|
+
if (o) {
|
|
782
|
+
var btn = td.appendChild(optionButton(o, true));
|
|
783
|
+
btn.addEventListener('click', function (e) { e.stopPropagation(); });
|
|
784
|
+
} else td.textContent = cell;
|
|
711
785
|
});
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
box.appendChild(sayBox());
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// While the calm line shows, look every WAIT_LOOK_MS for the idea; once the
|
|
719
|
-
// server has one ready, it is asked for the way a tap does and turns in
|
|
720
|
-
// with one crossfade. No tap, and nothing moves on the page while it
|
|
721
|
-
// waits. It also waits while the owner types there (the box has the
|
|
722
|
-
// cursor or any words), so it comes in after they send, or once they
|
|
723
|
-
// clear the box and leave it; and on the big orb, it waits for their tap.
|
|
724
|
-
var WAIT_LOOK_MS = 1500;
|
|
725
|
-
function typingOnCard() {
|
|
726
|
-
var input = document.getElementById('bet').querySelector('.field input');
|
|
727
|
-
return Boolean(input && (input === document.activeElement || input.value.trim()));
|
|
728
|
-
}
|
|
729
|
-
function watchWait() {
|
|
730
|
-
if (bet.watching) return;
|
|
731
|
-
var quiet = function () { return !bet.busy && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
732
|
-
bet.watching = setInterval(function () {
|
|
733
|
-
if (!bet.last || bet.last.kind !== 'wait') { clearInterval(bet.watching); bet.watching = null; return; }
|
|
734
|
-
if (!quiet()) return;
|
|
735
|
-
api('/bet').then(function (s) {
|
|
736
|
-
if (s.http !== 200 || !quiet() || !bet.last || bet.last.kind !== 'wait') return;
|
|
737
|
-
if (s.id !== bet.last.id && s.kind !== 'wait' && isCard(s)) { bet.status = s.status; turn(s); return; }
|
|
738
|
-
if (s.kind === 'wait' && s.queue > 0) think(false, 0);
|
|
739
|
-
}).catch(function () { /* the next look tries again */ });
|
|
740
|
-
}, WAIT_LOOK_MS);
|
|
786
|
+
if (mine.length) tr.addEventListener('click', function () { var first = tr.querySelector('button'); if (first) first.click(); });
|
|
787
|
+
});
|
|
788
|
+
return grid;
|
|
741
789
|
}
|
|
742
790
|
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
var
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
791
|
+
// One option, as the card says: big or quiet, a second tap when it asks
|
|
792
|
+
// for one, a box when it takes words. bare: no class of its own (in a cell).
|
|
793
|
+
function optionButton(o, bare) {
|
|
794
|
+
var cls = bare ? null : o.style === 'primary' ? 'pill' : o.style === 'choice' ? 'choice' : o.style === 'option' ? 'option' : null;
|
|
795
|
+
var btn = el('button', cls);
|
|
796
|
+
btn.type = 'button';
|
|
797
|
+
btn.setAttribute('data-option', o.id);
|
|
798
|
+
if (o.style === 'option' || (o.detail && o.style === 'choice')) {
|
|
799
|
+
btn.appendChild(el('span', 't', o.label));
|
|
800
|
+
if (o.detail) btn.appendChild(el('span', 'w', o.detail));
|
|
801
|
+
} else btn.textContent = o.label;
|
|
802
|
+
if (o.chosen) btn.classList.add('chosen');
|
|
803
|
+
if (o.type) btn.classList.add('other');
|
|
804
|
+
if (o.action === 'none') { btn.disabled = true; return btn; }
|
|
805
|
+
if (o.confirm) {
|
|
806
|
+
// the first tap arms this one button; the second sends; it goes quiet after five seconds
|
|
807
|
+
var armed = false, quiet = null;
|
|
808
|
+
btn.addEventListener('click', function () {
|
|
755
809
|
if (bet.busy) return;
|
|
756
810
|
buzz();
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
811
|
+
if (!armed) {
|
|
812
|
+
armed = true; btn.textContent = o.confirm; btn.classList.add('armed');
|
|
813
|
+
quiet = setTimeout(function () { armed = false; btn.textContent = o.label; btn.classList.remove('armed'); }, 5000);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
clearTimeout(quiet);
|
|
817
|
+
armed = false;
|
|
818
|
+
send(btn, { option_id: o.id, confirm: true }, o);
|
|
760
819
|
});
|
|
761
|
-
|
|
762
|
-
|
|
820
|
+
return btn;
|
|
821
|
+
}
|
|
822
|
+
btn.addEventListener('click', function () {
|
|
823
|
+
if (bet.busy) return;
|
|
824
|
+
buzz();
|
|
825
|
+
if (o.type) { typeHere(btn, o.type.placeholder || o.label, o.type.max || 280, function (text, field) { send(field, { option_id: o.id, text: text }, o); }); return; }
|
|
826
|
+
send(btn, { option_id: o.id }, o);
|
|
763
827
|
});
|
|
764
|
-
|
|
765
|
-
act.appendChild(el('div', 'links'));
|
|
766
|
-
box.appendChild(act);
|
|
767
|
-
// its own something else box is the one place to type
|
|
768
|
-
if (!q.choices.some(function (c) { return c.other; })) box.appendChild(sayBox());
|
|
828
|
+
return btn;
|
|
769
829
|
}
|
|
770
830
|
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
831
|
+
// the card's box: a quiet line that turns into the field in its own spot
|
|
832
|
+
function inputLine(input) {
|
|
833
|
+
var wrap = el('div', 'links sayline');
|
|
834
|
+
var open = wrap.appendChild(el('button', 'quiet', input.label || 'or say something'));
|
|
835
|
+
open.type = 'button';
|
|
836
|
+
open.addEventListener('click', function () {
|
|
837
|
+
if (bet.busy) return;
|
|
838
|
+
typeHere(open, input.placeholder || 'say something', input.max || 280, function (text, field) { send(field, { text: text }, null); });
|
|
839
|
+
});
|
|
840
|
+
return wrap;
|
|
841
|
+
}
|
|
779
842
|
|
|
780
|
-
//
|
|
843
|
+
// ---- answering in place: the eyes stay in one spot ---------------------------------
|
|
844
|
+
// every row of the card dims but the one in use; no spot, none
|
|
781
845
|
function dimOthers(spot) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
n.classList.toggle('muted', Boolean(spot) && n !== spot && !n.contains(spot));
|
|
846
|
+
box().querySelectorAll('.act > *, .act .links > button, .sayline, .grid').forEach(function (n) {
|
|
847
|
+
n.classList.toggle('muted', Boolean(spot) && n !== spot && !n.contains(spot) && !spot.contains(n));
|
|
785
848
|
});
|
|
786
849
|
}
|
|
787
850
|
|
|
788
|
-
// the spot holds as chosen and the card takes no taps; returns when it
|
|
789
|
-
// started, which is also where the time to the next card counts from
|
|
851
|
+
// the spot holds as chosen and the card takes no taps; returns when it started
|
|
790
852
|
function hold(spot) {
|
|
791
|
-
var box = document.getElementById('bet');
|
|
792
853
|
dimOthers(spot);
|
|
793
854
|
spot.classList.add('chosen');
|
|
794
|
-
box.classList.add('holding');
|
|
855
|
+
box().classList.add('holding');
|
|
795
856
|
var input = spot.querySelector('input');
|
|
796
857
|
if (input) { input.readOnly = true; input.blur(); }
|
|
797
858
|
bet.busy = true;
|
|
798
|
-
bet.armed = false;
|
|
799
859
|
bet.tapAt = performance.now();
|
|
800
860
|
document.body.removeAttribute('data-reveal-ms');
|
|
801
861
|
return bet.tapAt;
|
|
802
862
|
}
|
|
803
863
|
|
|
804
|
-
// it did not go through: the card is as it was
|
|
805
|
-
function unhold() {
|
|
806
|
-
var box = document.getElementById('bet');
|
|
807
|
-
box.classList.remove('holding');
|
|
808
|
-
dimOthers(null);
|
|
809
|
-
box.querySelectorAll('.chosen').forEach(function (n) { n.classList.remove('chosen'); });
|
|
810
|
-
bet.busy = false;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// resolves once the hold that started at heldAt has run HOLD_MS
|
|
814
864
|
function afterHold(heldAt) {
|
|
815
865
|
var left = heldAt ? heldAt + HOLD_MS - performance.now() : 0;
|
|
816
866
|
return new Promise(function (resolve) { setTimeout(resolve, Math.max(0, left)); });
|
|
817
867
|
}
|
|
818
868
|
|
|
819
869
|
// The tapped button becomes the field at its own size and spot, with the
|
|
820
|
-
// cursor already in it, and the other
|
|
870
|
+
// cursor already in it, and the other options dim. Enter or send hands
|
|
821
871
|
// the words to sent(text, field). Left empty, it turns back into the button.
|
|
822
872
|
function typeHere(btn, placeholder, maxLength, sent) {
|
|
823
873
|
var field = el('div', btn.classList.contains('choice') ? 'choice field' : 'field');
|
|
@@ -829,18 +879,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
829
879
|
input.placeholder = placeholder;
|
|
830
880
|
input.enterKeyHint = 'send';
|
|
831
881
|
input.setAttribute('aria-label', placeholder);
|
|
832
|
-
var
|
|
833
|
-
|
|
882
|
+
var go = el('button', 'send', 'send');
|
|
883
|
+
go.type = 'button';
|
|
834
884
|
var submit = function () {
|
|
835
885
|
var text = input.value.trim();
|
|
836
886
|
if (text && !bet.busy) sent(text, field);
|
|
837
887
|
};
|
|
838
888
|
// a press on send keeps the cursor in the field, so it never closes first
|
|
839
|
-
|
|
840
|
-
|
|
889
|
+
go.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
890
|
+
go.addEventListener('click', function () { buzz(); submit(); });
|
|
841
891
|
input.addEventListener('keydown', function (e) {
|
|
842
892
|
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
843
|
-
if (e.key === 'Escape') { input.value = ''; input.blur(); }
|
|
844
893
|
});
|
|
845
894
|
input.addEventListener('blur', function () {
|
|
846
895
|
if (input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
|
|
@@ -848,370 +897,130 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
848
897
|
dimOthers(null);
|
|
849
898
|
});
|
|
850
899
|
field.appendChild(input);
|
|
851
|
-
field.appendChild(
|
|
900
|
+
field.appendChild(go);
|
|
852
901
|
btn.replaceWith(field);
|
|
853
902
|
dimOthers(field);
|
|
854
903
|
input.focus();
|
|
904
|
+
return field;
|
|
855
905
|
}
|
|
856
906
|
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
|
|
861
|
-
body.card = shownCard();
|
|
862
|
-
var heldAt = hold(spot);
|
|
863
|
-
api('/bet/answer', body).catch(offline).then(function (r) {
|
|
864
|
-
if (r.ok) { think(false, heldAt); return; }
|
|
865
|
-
unhold();
|
|
866
|
-
var hint = document.getElementById('bet').querySelector('.act .links');
|
|
867
|
-
if (hint) hint.textContent = r.message || 'that did not go through';
|
|
868
|
-
var input = spot.querySelector('input');
|
|
869
|
-
if (input) { input.readOnly = false; dimOthers(spot); input.focus(); }
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
// the card the owner is looking at, named with every answer and every
|
|
874
|
-
// word said, so the server acts on that card or on nothing
|
|
875
|
-
function shownCard() { return bet.last && bet.last.id ? bet.last.id : ''; }
|
|
876
|
-
|
|
877
|
-
// Words said on a card hold their spot as chosen while the server reads
|
|
878
|
-
// them. An answer or an instruction brings the next card; a question
|
|
879
|
-
// back or a second tap it needs turns in as the orb's reply, in the
|
|
880
|
-
// title's spot. If it did not send, the words stay where they were.
|
|
881
|
-
// One id per typed send: made when these words are first sent from this
|
|
882
|
-
// spot, the same on every retry of them, and new when the words change,
|
|
883
|
-
// so a send whose reply was lost is never sent twice. getRandomValues
|
|
907
|
+
// One id per send: made when these words are first sent from this card's
|
|
908
|
+
// option or box, the same on every retry of them there, and new for any
|
|
909
|
+
// other card, option, or words, so a send whose answer was lost is never
|
|
910
|
+
// sent twice and the same words to someone else always go. getRandomValues
|
|
884
911
|
// works on the plain http of the wifi link, where randomUUID does not.
|
|
885
|
-
|
|
886
|
-
|
|
912
|
+
var sent = { key: null, id: null };
|
|
913
|
+
function sendId(key) {
|
|
914
|
+
if (sent.key !== key) {
|
|
887
915
|
var n = new Uint32Array(2);
|
|
888
916
|
crypto.getRandomValues(n);
|
|
889
|
-
|
|
890
|
-
|
|
917
|
+
sent.key = key;
|
|
918
|
+
sent.id = ('000000' + n[0].toString(36)).slice(-7) + ('000000' + n[1].toString(36)).slice(-7);
|
|
891
919
|
}
|
|
892
|
-
return
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// while the words are read, one quiet ellipsis in the held spot: where
|
|
896
|
-
// send was in a box, or after a tapped reply. Nowhere else.
|
|
897
|
-
function reading(spot) {
|
|
898
|
-
var send = spot.querySelector('.send');
|
|
899
|
-
if (send) { send.textContent = '\u2026'; send.classList.add('reading'); return; }
|
|
900
|
-
spot.appendChild(el('span', 'reading', ' \u2026'));
|
|
901
|
-
}
|
|
902
|
-
function doneReading(spot) {
|
|
903
|
-
var send = spot.querySelector('.send.reading');
|
|
904
|
-
if (send) { send.textContent = 'send'; send.classList.remove('reading'); }
|
|
905
|
-
var dots = spot.querySelector('span.reading');
|
|
906
|
-
if (dots) dots.remove();
|
|
920
|
+
return sent.id;
|
|
907
921
|
}
|
|
908
922
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
923
|
+
// ---- acting: one post, then one motion to the next card ------------------------------
|
|
924
|
+
// The option holds as chosen for HOLD_MS. Then the card fades while the
|
|
925
|
+
// small orb glides into the sphere; once the answer is in, the dots
|
|
926
|
+
// spiral in and the next card blooms where they land. An answer that is
|
|
927
|
+
// late leaves the sphere in its idle drift until it lands. A line from the
|
|
928
|
+
// Mac (who has it now) shows under the sphere while it waits. When it did
|
|
929
|
+
// not go through, the same card comes back with the words still in its
|
|
930
|
+
// box and one line why. Flipping the deck turns the card in place.
|
|
931
|
+
function send(spot, body, option) {
|
|
932
|
+
if (!bet.shown) return;
|
|
933
|
+
var from = bet.shown;
|
|
934
|
+
body.card_id = from.id;
|
|
935
|
+
if (body.text) body.send = sendId([from.id, option ? option.id : 'the box', body.text].join(' | '));
|
|
912
936
|
var heldAt = hold(spot);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
// It did not send: the card is as it was, the words still in the box with
|
|
922
|
-
// the cursor in it, and one short line under it says why, so Enter (or
|
|
923
|
-
// the same tap) tries again. Left empty, the box turns back into the
|
|
924
|
-
// button and the line goes with it.
|
|
925
|
-
function unsent(spot, why) {
|
|
926
|
-
doneReading(spot);
|
|
927
|
-
unhold();
|
|
928
|
-
var note = spot.nextElementSibling;
|
|
929
|
-
var input = spot.querySelector('input');
|
|
930
|
-
if (!note || !note.classList.contains('unsent')) {
|
|
931
|
-
note = el('p', 'links unsent');
|
|
932
|
-
spot.after(note);
|
|
933
|
-
if (input) input.addEventListener('blur', function () { if (!spot.isConnected) note.remove(); });
|
|
934
|
-
}
|
|
935
|
-
note.textContent = why;
|
|
936
|
-
if (input) { input.readOnly = false; dimOthers(spot); input.focus(); }
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// one reply to tap: it holds as chosen and is said
|
|
940
|
-
function sayChoice(label, body) {
|
|
941
|
-
var b = el('button', 'choice', label);
|
|
942
|
-
b.type = 'button';
|
|
943
|
-
b.addEventListener('click', function () {
|
|
944
|
-
if (bet.busy) return;
|
|
945
|
-
buzz();
|
|
946
|
-
say(b, body);
|
|
947
|
-
});
|
|
948
|
-
return b;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// the orb's reply is the title, with the card's own answer right under it
|
|
952
|
-
// (do it, yes, nice, or the answers), then up to three suggested replies;
|
|
953
|
-
// starting a member who is not running takes one more tap, or it is kept
|
|
954
|
-
// as a note. The say line takes anything else.
|
|
955
|
-
function drawReply(r, said) {
|
|
956
|
-
var box = document.getElementById('bet');
|
|
957
|
-
// a reply never takes the card's own answer away: the card is drawn
|
|
958
|
-
// once more, and its row is kept, buttons and all
|
|
959
|
-
var own = null;
|
|
960
|
-
if (bet.last) { drawBetCard(bet.last); own = box.querySelector(':scope > .act, :scope > .options'); }
|
|
961
|
-
box.textContent = '';
|
|
962
|
-
bet.drawn = null;
|
|
963
|
-
var title = el('h1', null, r.stay ? r.reply : (r.message || 'that did not go through'));
|
|
964
|
-
box.appendChild(title);
|
|
965
|
-
// more than one line reads at the body size, in the same spot
|
|
966
|
-
var lines = document.createRange();
|
|
967
|
-
lines.selectNodeContents(title);
|
|
968
|
-
if (lines.getClientRects().length > 1) title.classList.add('said');
|
|
969
|
-
if (own) box.appendChild(own);
|
|
970
|
-
var act = el('div', 'act answers');
|
|
971
|
-
var have = own ? Array.prototype.map.call(own.querySelectorAll('button'), function (b) { return b.textContent.trim().toLowerCase(); }) : [];
|
|
972
|
-
if (r.stay) (r.suggestions || []).forEach(function (sug) {
|
|
973
|
-
if (have.indexOf(String(sug).trim().toLowerCase()) < 0) act.appendChild(sayChoice(sug, { text: sug }));
|
|
974
|
-
});
|
|
975
|
-
if (r.reason === 'needs_confirm') {
|
|
976
|
-
act.appendChild(sayChoice('start it', { text: said, confirm: true }));
|
|
977
|
-
act.appendChild(sayChoice('keep it as a note instead', { text: said, note: true }));
|
|
937
|
+
var answer = api('/act', body).catch(offline);
|
|
938
|
+
if (option && IN_PLACE.indexOf(option.action) >= 0) {
|
|
939
|
+
Promise.all([answer, afterHold(heldAt)]).then(function (both) {
|
|
940
|
+
var r = both[0];
|
|
941
|
+
if (!r.ok || !r.card) { bet.busy = false; unsent(from, r, body.text, option); return; }
|
|
942
|
+
turnTo(r.card);
|
|
943
|
+
});
|
|
944
|
+
return;
|
|
978
945
|
}
|
|
979
|
-
|
|
980
|
-
if (!own || !own.querySelector('.other')) box.appendChild(sayBox());
|
|
946
|
+
afterHold(heldAt).then(function () { flow(answer, from, body.text, option); });
|
|
981
947
|
}
|
|
982
948
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
var
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
input.value = '';
|
|
1003
|
-
input.placeholder = r.message || 'that did not go through';
|
|
1004
|
-
});
|
|
949
|
+
function flow(answer, from, typed, option) {
|
|
950
|
+
var landed = null;
|
|
951
|
+
var sphere = false;
|
|
952
|
+
var lineAt = 0;
|
|
953
|
+
var land = function () {
|
|
954
|
+
if (!landed || !sphere) return;
|
|
955
|
+
var r = landed;
|
|
956
|
+
landed = null;
|
|
957
|
+
var wait = r.ack && lineAt ? Math.max(0, AFTER_MS - (performance.now() - lineAt)) : 0;
|
|
958
|
+
setTimeout(function () {
|
|
959
|
+
// nothing more to show: the sphere stays, with its line
|
|
960
|
+
if (r.ok && (r.home || !r.card)) { bet.busy = false; bet.shown = null; line.set(r.home ? 'not now. tap when you want another' : 'tap to think', false); return; }
|
|
961
|
+
// the card the Mac has on screen now is the one that comes back; a
|
|
962
|
+
// refusal comes back on it with one line why
|
|
963
|
+
var then = r.card || from;
|
|
964
|
+
if (!then) { bet.busy = false; line.set('that did not go through. tap to try again', true); return; }
|
|
965
|
+
if (!r.ok) { spiralTo(then, function () { unsent(then, r, typed, then.id === (from && from.id) ? option : null); }); return; }
|
|
966
|
+
spiralTo(then, null);
|
|
967
|
+
}, wait);
|
|
1005
968
|
};
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
// a quiet way out: moves on, saves nothing, and is not asked again today
|
|
1011
|
-
var links = el('div', 'links');
|
|
1012
|
-
var later = el('button', null, 'not now');
|
|
1013
|
-
later.type = 'button';
|
|
1014
|
-
later.addEventListener('click', function () { if (bet.busy) return; buzz(); think(false, 0); });
|
|
1015
|
-
links.appendChild(later);
|
|
1016
|
-
act.appendChild(links);
|
|
1017
|
-
box.appendChild(act);
|
|
1018
|
-
box.appendChild(sayBox());
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
// A conversation in one box. While the words are read the reply spot shows
|
|
1022
|
-
// a quiet ellipsis; a question back is answered in place, with the turns so
|
|
1023
|
-
// far above it and up to three suggested replies to tap. Starting a member
|
|
1024
|
-
// who is not running takes a second tap, or it is kept as a note.
|
|
1025
|
-
// post(body) sends it; done(ack) runs when something real came of it.
|
|
1026
|
-
function convo(post, done, placeholder) {
|
|
1027
|
-
var wrap = el('div', 'links sayline');
|
|
1028
|
-
var thread = el('div', 'thread');
|
|
1029
|
-
var reply = el('p', 'why reply');
|
|
1030
|
-
reply.hidden = true;
|
|
1031
|
-
var input = el('input', 'say');
|
|
1032
|
-
input.type = 'text';
|
|
1033
|
-
input.maxLength = 280;
|
|
1034
|
-
input.setAttribute('aria-label', placeholder);
|
|
1035
|
-
input.placeholder = placeholder;
|
|
1036
|
-
var send = el('button', 'pill', 'send');
|
|
1037
|
-
send.type = 'button';
|
|
1038
|
-
var extra = el('div', 'links');
|
|
1039
|
-
var pending = null;
|
|
1040
|
-
// an armed second tap belongs to the words it was asked about: any edit clears it
|
|
1041
|
-
function disarm() {
|
|
1042
|
-
pending = null;
|
|
1043
|
-
send.textContent = 'send';
|
|
1044
|
-
send.className = 'pill';
|
|
1045
|
-
extra.textContent = '';
|
|
1046
|
-
reply.textContent = '';
|
|
1047
|
-
reply.hidden = true;
|
|
1048
|
-
}
|
|
1049
|
-
function drawThread(turns) {
|
|
1050
|
-
thread.textContent = '';
|
|
1051
|
-
turns.forEach(function (t) { thread.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text)); });
|
|
1052
|
-
}
|
|
1053
|
-
function go(more, said) {
|
|
1054
|
-
var text = said || input.value.trim();
|
|
1055
|
-
if (!text) return;
|
|
1056
|
-
send.disabled = true;
|
|
1057
|
-
reply.hidden = false;
|
|
1058
|
-
reply.textContent = '\u2026';
|
|
1059
|
-
extra.textContent = '';
|
|
1060
|
-
// the same words carry the same id on every try, so a retry never sends twice
|
|
1061
|
-
var body = { text: text, send: sendId(wrap, text) };
|
|
1062
|
-
if (more) Object.keys(more).forEach(function (k) { body[k] = more[k]; });
|
|
1063
|
-
post(body).catch(offline).then(function (r) {
|
|
1064
|
-
if (r.ok && !r.stay) { done(r.ack); return; }
|
|
1065
|
-
send.disabled = false;
|
|
1066
|
-
if (r.stay) { reply.textContent = r.reply;
|
|
1067
|
-
input.value = '';
|
|
1068
|
-
drawThread(r.thread.slice(0, -1));
|
|
1069
|
-
(r.suggestions || []).forEach(function (sug) {
|
|
1070
|
-
var b = el('button', 'quiet', sug);
|
|
1071
|
-
b.type = 'button';
|
|
1072
|
-
b.addEventListener('click', function () { buzz(); go(null, sug); });
|
|
1073
|
-
extra.appendChild(b);
|
|
1074
|
-
});
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
if (r.reason === 'needs_confirm') {
|
|
1078
|
-
pending = text;
|
|
1079
|
-
reply.textContent = r.message;
|
|
1080
|
-
send.textContent = 'tap again';
|
|
1081
|
-
send.className = 'pill armed';
|
|
1082
|
-
var keep = el('button', 'quiet', 'keep it as a note instead');
|
|
1083
|
-
keep.type = 'button';
|
|
1084
|
-
keep.addEventListener('click', function () { disarm(); go({ note: true }); });
|
|
1085
|
-
extra.appendChild(keep);
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
// it did not send: the words are in the box, send works again, and
|
|
1089
|
-
// one short line says so
|
|
1090
|
-
if (said && !input.value) input.value = said;
|
|
1091
|
-
reply.textContent = r.message || 'that did not send. tap send to try again';
|
|
1092
|
-
input.focus();
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
send.addEventListener('click', function () {
|
|
1096
|
-
buzz();
|
|
1097
|
-
if (pending) { disarm(); go({ confirm: true }); return; }
|
|
1098
|
-
go();
|
|
969
|
+
answer.then(function (r) {
|
|
970
|
+
landed = r;
|
|
971
|
+
if (r.ok && r.ack) { lineAt = performance.now(); line.set(r.ack, false); }
|
|
972
|
+
land();
|
|
1099
973
|
});
|
|
1100
|
-
|
|
1101
|
-
input.addEventListener('input', function () { if (pending) disarm(); });
|
|
1102
|
-
wrap.appendChild(thread);
|
|
1103
|
-
wrap.appendChild(reply);
|
|
1104
|
-
wrap.appendChild(input);
|
|
1105
|
-
wrap.appendChild(send);
|
|
1106
|
-
wrap.appendChild(extra);
|
|
1107
|
-
return wrap;
|
|
974
|
+
leave(function () { sphere = true; land(); }, true);
|
|
1108
975
|
}
|
|
1109
976
|
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
977
|
+
// the card fades in place while the small orb glides down and becomes the
|
|
978
|
+
// sphere; the card leaves the page once it has faded
|
|
979
|
+
function leave(then, fast) {
|
|
980
|
+
var b = box();
|
|
981
|
+
var to = sphereSpot();
|
|
982
|
+
b.classList.remove('quick', 'acted', 'up');
|
|
983
|
+
b.classList.add('leave');
|
|
984
|
+
var swap = function () {
|
|
985
|
+
b.style.minHeight = '';
|
|
986
|
+
b.hidden = true;
|
|
987
|
+
b.classList.remove('leave', 'holding');
|
|
988
|
+
document.body.classList.remove('has-card', 'wide');
|
|
989
|
+
window.scrollTo(0, 0);
|
|
990
|
+
};
|
|
991
|
+
if (reduced) { swap(); orb.melt(then, to, function () {}, fast); return; }
|
|
992
|
+
orb.melt(then, to, swap, fast);
|
|
1124
993
|
}
|
|
1125
994
|
|
|
1126
|
-
//
|
|
1127
|
-
function
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
b.appendChild(el('span', 't', o.title));
|
|
1136
|
-
b.appendChild(el('span', 'w', o.why));
|
|
1137
|
-
b.addEventListener('click', function () {
|
|
1138
|
-
if (bet.busy) return;
|
|
1139
|
-
buzz();
|
|
1140
|
-
var heldAt = hold(b);
|
|
1141
|
-
api('/bet/pick', { id: o.id }).then(function (r) {
|
|
1142
|
-
if (r.http === 200 && r.bet) { afterHold(heldAt).then(function () { turn(r); }); return; }
|
|
1143
|
-
think(false, heldAt);
|
|
1144
|
-
}, function () { think(false, heldAt); });
|
|
1145
|
-
});
|
|
1146
|
-
list.appendChild(b);
|
|
1147
|
-
});
|
|
1148
|
-
box.appendChild(list);
|
|
1149
|
-
box.appendChild(sayBox());
|
|
995
|
+
// from the sphere: the dots spiral in and the card blooms where they land
|
|
996
|
+
function spiralTo(card, after) {
|
|
997
|
+
line.set('', false);
|
|
998
|
+
if (reduced) { orb.settled(); showCard(card, false); bet.busy = false; if (after) after(); return; }
|
|
999
|
+
orb.play(function (phase) {
|
|
1000
|
+
// the card blooms where the dots land; what comes with it (a line why, the words back in their box) comes with it
|
|
1001
|
+
if (phase === 'cover') { showCard(card, true, true, true); if (after) after(); }
|
|
1002
|
+
if (phase === 'settled') bet.busy = false;
|
|
1003
|
+
}, true, true);
|
|
1150
1004
|
}
|
|
1151
1005
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
if (
|
|
1156
|
-
|
|
1157
|
-
var
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
//
|
|
1162
|
-
box.
|
|
1163
|
-
if (
|
|
1164
|
-
if (
|
|
1165
|
-
var
|
|
1166
|
-
|
|
1167
|
-
go.type = 'button';
|
|
1168
|
-
go.setAttribute('aria-label', bet.armed ? 'tap again to start the team on this idea' : 'do it. starts the team on the steps; your yes steps wait for you');
|
|
1169
|
-
go.addEventListener('click', function () {
|
|
1170
|
-
buzz();
|
|
1171
|
-
if (!bet.armed) {
|
|
1172
|
-
bet.armed = true; drawBetCard(s);
|
|
1173
|
-
setTimeout(function () { if (bet.armed) { bet.armed = false; if (bet.last) drawBetCard(bet.last); } }, 5000);
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
bet.armed = false;
|
|
1177
|
-
s.status = 'starting'; s.unstarted = null; drawBetCard(s);
|
|
1178
|
-
api('/bet/do', { confirm: true }).catch(offline).then(function (r) {
|
|
1179
|
-
if (!r || !r.ok) {
|
|
1180
|
-
// it did not get going: the card comes back as it was, do it and
|
|
1181
|
-
// all, with one line why, and the page follows this card again
|
|
1182
|
-
s.status = 'ready';
|
|
1183
|
-
s.unstarted = (r && r.message) || 'that did not start. tap do it to try again';
|
|
1184
|
-
bet.drawn = s.id;
|
|
1185
|
-
drawBetCard(s);
|
|
1186
|
-
refresh();
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
loadBet(); refresh();
|
|
1190
|
-
setTimeout(function () { tapOrb(false); }, AFTER_MS);
|
|
1191
|
-
});
|
|
1192
|
-
});
|
|
1193
|
-
act.appendChild(go);
|
|
1194
|
-
if (s.unstarted) act.appendChild(el('p', 'links unsent', s.unstarted));
|
|
1195
|
-
var links = el('div', 'links');
|
|
1196
|
-
var sharp = el('button', null, 'sharper idea');
|
|
1197
|
-
sharp.type = 'button';
|
|
1198
|
-
sharp.addEventListener('click', function () { tapOrb(true); });
|
|
1199
|
-
var skip = el('button', null, 'not now');
|
|
1200
|
-
skip.type = 'button';
|
|
1201
|
-
skip.addEventListener('click', function () {
|
|
1202
|
-
buzz();
|
|
1203
|
-
api('/bet/skip', {}).then(function () {
|
|
1204
|
-
dissolve(function () { bet.status = 'skipped'; line.set('not now. tap when you want another', false); });
|
|
1205
|
-
});
|
|
1206
|
-
});
|
|
1207
|
-
links.appendChild(sharp);
|
|
1208
|
-
links.appendChild(document.createTextNode(' · '));
|
|
1209
|
-
links.appendChild(skip);
|
|
1210
|
-
act.appendChild(links);
|
|
1211
|
-
box.appendChild(act);
|
|
1212
|
-
box.appendChild(sayBox());
|
|
1006
|
+
// It did not go through: this card as it was, one line why under its
|
|
1007
|
+
// options, and typed words back in their box with the cursor in it.
|
|
1008
|
+
function unsent(card, r, typed, option) {
|
|
1009
|
+
if (bet.shown !== card || box().getAttribute('data-card') !== card.id) showCard(card, false);
|
|
1010
|
+
box().classList.remove('holding');
|
|
1011
|
+
var note = box().querySelector('.act .note');
|
|
1012
|
+
// the Mac's own refusals are plain words; anything else reads as one plain line
|
|
1013
|
+
if (note) note.textContent = (r && r.http === 409 && r.message) || (r && r.http === 0 && r.message) || 'that did not go through. try again.';
|
|
1014
|
+
if (!typed) return;
|
|
1015
|
+
// the words go back where they were typed, or to this card's own box when the card is a new one
|
|
1016
|
+
var spot = option ? box().querySelector('[data-option="' + option.id + '"]') : box().querySelector('.sayline .quiet');
|
|
1017
|
+
if (!spot) return;
|
|
1018
|
+
if (!option && !card.input) return;
|
|
1019
|
+
var field = typeHere(spot, (option && option.type && option.type.placeholder) || (card.input && card.input.placeholder) || 'say something', 280, function (text, f) { send(f, option ? { option_id: option.id, text: text } : { text: text }, option); });
|
|
1020
|
+
field.querySelector('input').value = typed;
|
|
1213
1021
|
}
|
|
1214
1022
|
|
|
1023
|
+
// ---- showing a card ---------------------------------------------------------------------
|
|
1215
1024
|
// the time from the tap to the card fully on screen, kept on the page so
|
|
1216
1025
|
// the speed can be measured
|
|
1217
1026
|
function measured() {
|
|
@@ -1219,301 +1028,261 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1219
1028
|
bet.tapAt = 0;
|
|
1220
1029
|
}
|
|
1221
1030
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
bet.
|
|
1226
|
-
|
|
1031
|
+
// fast: the quick fade from the big orb; act: the short one after an act
|
|
1032
|
+
function showCard(card, animate, fast, act) {
|
|
1033
|
+
var b = box();
|
|
1034
|
+
bet.shown = card;
|
|
1035
|
+
// where it starts from is set first: drawing measures the title, and a
|
|
1036
|
+
// measure is a frame the card would otherwise start from somewhere else
|
|
1037
|
+
b.classList.remove('leave', 'holding');
|
|
1038
|
+
b.classList.toggle('quick', Boolean(fast) && !act);
|
|
1039
|
+
b.classList.toggle('acted', Boolean(act));
|
|
1040
|
+
b.classList.toggle('up', !animate);
|
|
1041
|
+
// on the page before it is drawn, so a long title can tell it runs past one line
|
|
1227
1042
|
document.body.classList.add('has-card');
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
if (!animate) {
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
requestAnimationFrame(function () { box.classList.add('up'); });
|
|
1043
|
+
b.hidden = false;
|
|
1044
|
+
draw(card);
|
|
1045
|
+
if (card.watch) watchWait(); else stopWatch();
|
|
1046
|
+
if (!animate) { measured(); return; }
|
|
1047
|
+
var done = function (e) { if (e.propertyName === 'opacity') { b.removeEventListener('transitionend', done); measured(); } };
|
|
1048
|
+
b.addEventListener('transitionend', done);
|
|
1049
|
+
void b.offsetWidth;
|
|
1050
|
+
requestAnimationFrame(function () { b.classList.add('up'); });
|
|
1237
1051
|
}
|
|
1238
1052
|
|
|
1239
1053
|
// where the sphere sits once the card is gone, read before anything moves,
|
|
1240
1054
|
// in page coordinates (the screen's once the page is back at the top)
|
|
1241
1055
|
function sphereSpot() {
|
|
1242
|
-
var
|
|
1243
|
-
var y0 = window.scrollY, was =
|
|
1244
|
-
document.body.classList.remove('has-card');
|
|
1245
|
-
|
|
1056
|
+
var b = box();
|
|
1057
|
+
var y0 = window.scrollY, was = b.hidden, wide = document.body.classList.contains('wide');
|
|
1058
|
+
document.body.classList.remove('has-card', 'wide');
|
|
1059
|
+
b.hidden = true;
|
|
1246
1060
|
var r = document.getElementById('orb').getBoundingClientRect();
|
|
1247
1061
|
var spot = { x: r.left + r.width / 2, y: r.top + window.scrollY + r.height / 2 };
|
|
1248
1062
|
document.body.classList.add('has-card');
|
|
1249
|
-
|
|
1063
|
+
if (wide) document.body.classList.add('wide');
|
|
1064
|
+
b.hidden = was;
|
|
1250
1065
|
if (window.scrollY !== y0) window.scrollTo(0, y0);
|
|
1251
1066
|
return spot;
|
|
1252
1067
|
}
|
|
1253
1068
|
|
|
1254
|
-
//
|
|
1255
|
-
// the
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
var to = sphereSpot();
|
|
1259
|
-
box.classList.remove('quick', 'up');
|
|
1260
|
-
box.classList.add('leave');
|
|
1261
|
-
var swap = function () {
|
|
1262
|
-
box.style.minHeight = '';
|
|
1263
|
-
box.hidden = true;
|
|
1264
|
-
box.classList.remove('leave');
|
|
1265
|
-
document.body.classList.remove('has-card');
|
|
1266
|
-
window.scrollTo(0, 0);
|
|
1267
|
-
};
|
|
1268
|
-
if (reduced) { setTimeout(function () { orb.melt(then, to, swap); }, orb.leaveMs); return; }
|
|
1269
|
-
orb.melt(then, to, swap);
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
// what the page does with the server's answer once the sphere is ready.
|
|
1273
|
-
// Every tap comes back with a card, so the orb plays straight into it.
|
|
1274
|
-
function reveal(s, fast) {
|
|
1275
|
-
bet.status = s.status;
|
|
1276
|
-
if ((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
1277
|
-
line.set('', false);
|
|
1278
|
-
orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(s, true, fast); } }, fast);
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
bet.busy = false;
|
|
1282
|
-
orb.relax();
|
|
1283
|
-
line.set('tap to think', false);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function isCard(s) {
|
|
1287
|
-
return Boolean((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting'));
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// Card to card: a crossfade in place. The small orb stays where it is.
|
|
1291
|
-
// A still copy of the old card fades over the box while the new card,
|
|
1292
|
-
// drawn under it, fades in from DRIFT below over TURN_IN_MS, starting
|
|
1293
|
-
// 80ms in, so its title lands where the old title was. The box eases to
|
|
1294
|
-
// its new height. No layout jump.
|
|
1069
|
+
// Card to card in place: a still copy of the old card fades over the box
|
|
1070
|
+
// while the new card, drawn under it, fades in from DRIFT below over
|
|
1071
|
+
// TURN_IN_MS, starting 80ms in, so its title lands where the old title
|
|
1072
|
+
// was. The box eases to its new height. No layout jump.
|
|
1295
1073
|
var TURN_IN_MS = 320, DRIFT = 4;
|
|
1296
|
-
function
|
|
1297
|
-
|
|
1074
|
+
function turnTo(card) {
|
|
1075
|
+
var b = box();
|
|
1076
|
+
b.classList.remove('holding');
|
|
1077
|
+
bet.shown = card;
|
|
1078
|
+
turnInPlace(b, function () { draw(card); }, function () { bet.busy = false; measured(); if (card.watch) watchWait(); else stopWatch(); });
|
|
1298
1079
|
}
|
|
1299
1080
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
var
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
var
|
|
1306
|
-
box.style.height = from + 'px';
|
|
1307
|
-
box.style.minHeight = '';
|
|
1308
|
-
var ghost = box.cloneNode(true);
|
|
1081
|
+
function turnInPlace(b, paint, after) {
|
|
1082
|
+
if (reduced) { b.style.minHeight = ''; paint(); after(); return; }
|
|
1083
|
+
var from = b.offsetHeight;
|
|
1084
|
+
b.style.height = from + 'px';
|
|
1085
|
+
b.style.minHeight = '';
|
|
1086
|
+
var ghost = b.cloneNode(true);
|
|
1309
1087
|
ghost.removeAttribute('id');
|
|
1310
|
-
ghost.classList.remove('quick', 'turning', 'turn-in');
|
|
1088
|
+
ghost.classList.remove('quick', 'acted', 'turning', 'turn-in');
|
|
1311
1089
|
ghost.classList.add('up', 'ghost');
|
|
1312
1090
|
ghost.setAttribute('aria-hidden', 'true');
|
|
1313
1091
|
ghost.inert = true;
|
|
1314
|
-
var r =
|
|
1092
|
+
var r = b.getBoundingClientRect(), p = b.offsetParent.getBoundingClientRect();
|
|
1315
1093
|
ghost.style.top = (r.top - p.top) + 'px';
|
|
1316
1094
|
ghost.style.left = (r.left - p.left) + 'px';
|
|
1317
1095
|
ghost.style.width = r.width + 'px';
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
var to =
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
void
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1096
|
+
b.after(ghost);
|
|
1097
|
+
b.classList.remove('quick', 'acted', 'turning');
|
|
1098
|
+
paint();
|
|
1099
|
+
b.style.height = 'auto';
|
|
1100
|
+
var to = b.offsetHeight;
|
|
1101
|
+
b.style.height = from + 'px';
|
|
1102
|
+
b.classList.add('turn-in');
|
|
1103
|
+
void b.offsetHeight;
|
|
1104
|
+
b.classList.add('turning');
|
|
1105
|
+
b.classList.remove('turn-in');
|
|
1106
|
+
b.style.height = to + 'px';
|
|
1329
1107
|
ghost.classList.add('gone');
|
|
1330
1108
|
setTimeout(function () {
|
|
1331
1109
|
ghost.remove();
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
measured();
|
|
1110
|
+
b.classList.remove('turning');
|
|
1111
|
+
b.style.height = '';
|
|
1112
|
+
after();
|
|
1336
1113
|
}, 80 + TURN_IN_MS + 20);
|
|
1337
1114
|
}
|
|
1338
1115
|
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1116
|
+
// While the calm line shows, look every WAIT_LOOK_MS for the idea; once the
|
|
1117
|
+
// Mac has one ready, it is asked for the way a tap does and turns in with
|
|
1118
|
+
// one crossfade. It waits while the owner types there (the box has the
|
|
1119
|
+
// cursor or any words), and on the big orb it waits for their tap.
|
|
1120
|
+
var WAIT_LOOK_MS = 1500;
|
|
1121
|
+
function typingOnCard() {
|
|
1122
|
+
var input = box().querySelector('.field input');
|
|
1123
|
+
return Boolean(input && (input === document.activeElement || input.value.trim()));
|
|
1124
|
+
}
|
|
1125
|
+
function stopWatch() { if (bet.watching) { clearInterval(bet.watching); bet.watching = null; } }
|
|
1126
|
+
function watchWait() {
|
|
1127
|
+
if (bet.watching) return;
|
|
1128
|
+
var quiet = function () { return !bet.busy && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
1129
|
+
bet.watching = setInterval(function () {
|
|
1130
|
+
if (!bet.shown || !bet.shown.watch) { stopWatch(); return; }
|
|
1131
|
+
if (!quiet()) return;
|
|
1132
|
+
api('/card').then(function (s) {
|
|
1133
|
+
if (s.http !== 200 || !quiet() || !bet.shown || !bet.shown.watch || !s.card) return;
|
|
1134
|
+
if (s.card.id !== bet.shown.id && isLive(s)) { turnTo(s.card); return; }
|
|
1135
|
+
if (s.queue > 0) {
|
|
1136
|
+
bet.busy = true;
|
|
1137
|
+
api('/act', { orb: true, card_id: bet.shown.id }).catch(offline).then(function (r) {
|
|
1138
|
+
if (r.ok && r.card) turnTo(r.card); else bet.busy = false;
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}).catch(function () { /* the next look tries again */ });
|
|
1142
|
+
}, WAIT_LOOK_MS);
|
|
1345
1143
|
}
|
|
1346
1144
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
//
|
|
1350
|
-
|
|
1145
|
+
function isLive(s) { return Boolean(s && s.card); }
|
|
1146
|
+
|
|
1147
|
+
// ---- the orb ----------------------------------------------------------------------------
|
|
1148
|
+
// The big orb: the next card, with the spiral, as on page open. An idea
|
|
1149
|
+
// that landed behind the calm line is the next one, so it is never passed.
|
|
1150
|
+
function think() {
|
|
1151
|
+
if (bet.busy) return;
|
|
1152
|
+
buzz();
|
|
1351
1153
|
bet.busy = true;
|
|
1352
|
-
bet.
|
|
1353
|
-
bet.tapAt = heldAt || performance.now();
|
|
1154
|
+
bet.tapAt = performance.now();
|
|
1354
1155
|
document.body.removeAttribute('data-reveal-ms');
|
|
1355
|
-
var onCard = document.body.classList.contains('has-card');
|
|
1356
1156
|
var failed = function () {
|
|
1357
1157
|
bet.busy = false;
|
|
1358
|
-
document.getElementById('bet').classList.remove('holding');
|
|
1359
1158
|
orb.relax();
|
|
1360
1159
|
line.set('that did not go through. tap to try again', true);
|
|
1361
1160
|
};
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
bet.
|
|
1366
|
-
if (onCard && isCard(s)) { turn(s); return; }
|
|
1367
|
-
if (onCard) { dissolve(function () { reveal(s, true); }); return; }
|
|
1368
|
-
reveal(s, true);
|
|
1161
|
+
api('/act', { orb: true, card_id: bet.shown ? bet.shown.id : null }).then(function (r) {
|
|
1162
|
+
if (r.http !== 200 || !r.card) { if (r.http === 200) { bet.busy = false; orb.relax(); line.set('tap to think', false); return; } failed(); return; }
|
|
1163
|
+
line.set('', false);
|
|
1164
|
+
orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(r.card, true, true); } }, true);
|
|
1369
1165
|
}, failed);
|
|
1370
1166
|
}
|
|
1371
1167
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
if ((s.bet || s.job || s.question || s.choice || s.report) && bet.drawn === s.id) { bet.last = s; drawBetCard(s); if (s.status === 'starting') poll(); }
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
function poll() {
|
|
1381
|
-
if (bet.polling) return;
|
|
1382
|
-
bet.polling = true;
|
|
1383
|
-
setTimeout(function () { bet.polling = false; loadBet(); }, 400);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// first look: a card already on screen shows settled, anything else is the sphere
|
|
1387
|
-
function firstLook() {
|
|
1388
|
-
api('/bet').then(function (s) {
|
|
1389
|
-
if (s.http !== 200) return;
|
|
1390
|
-
bet.status = s.status;
|
|
1391
|
-
if ((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
1392
|
-
orb.settled();
|
|
1393
|
-
showCard(s, false);
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
line.set(s.status === 'skipped' ? 'not now. tap when you want another' : 'tap to think', false);
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// The small orb on a card goes back to the big dotted orb, the same calm
|
|
1401
|
-
// way the page goes home before a new idea: the card fades, the small orb
|
|
1402
|
-
// glides down and the dots form again, idle under the pointer. The card
|
|
1403
|
-
// stays where it was on the server, so nothing is passed yet.
|
|
1168
|
+
// The small orb on a card goes back to the big dotted orb, from any card:
|
|
1169
|
+
// the card fades, the small orb glides down and the dots form again, idle
|
|
1170
|
+
// under the pointer. The card stays where it is on the Mac, so nothing is
|
|
1171
|
+
// passed until the big orb is tapped.
|
|
1404
1172
|
function goHome() {
|
|
1405
1173
|
if (bet.busy) return;
|
|
1406
1174
|
buzz();
|
|
1407
1175
|
bet.busy = true;
|
|
1408
|
-
|
|
1409
|
-
|
|
1176
|
+
stopWatch();
|
|
1177
|
+
// the line under the big orb is ready before the sphere is back
|
|
1178
|
+
line.set('tap to think', false);
|
|
1179
|
+
leave(function () { bet.busy = false; });
|
|
1410
1180
|
}
|
|
1411
1181
|
|
|
1412
|
-
// The big orb: the next idea, with the spiral, as on page open. An idea
|
|
1413
|
-
// that landed behind the calm line is the next one, so it is never passed.
|
|
1414
|
-
function tapBig() { tapOrb(false); }
|
|
1415
|
-
|
|
1416
1182
|
function tapSmallOrb() {
|
|
1417
|
-
if (document.body.classList.contains('has-card')) goHome(); else
|
|
1183
|
+
if (document.body.classList.contains('has-card')) goHome(); else think();
|
|
1418
1184
|
}
|
|
1419
1185
|
|
|
1420
1186
|
var canvas = document.getElementById('orb');
|
|
1421
1187
|
var lavaTap = document.getElementById('lava');
|
|
1422
|
-
[[canvas,
|
|
1188
|
+
[[canvas, think], [lavaTap, tapSmallOrb]].forEach(function (pair) {
|
|
1423
1189
|
pair[0].addEventListener('click', pair[1]);
|
|
1424
1190
|
pair[0].addEventListener('keydown', function (e) {
|
|
1425
1191
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pair[1](); }
|
|
1426
1192
|
});
|
|
1427
1193
|
});
|
|
1428
1194
|
|
|
1429
|
-
//
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
function closeTeam() { teamBox.hidden = true; }
|
|
1437
|
-
|
|
1438
|
-
function teamHead(title) {
|
|
1439
|
-
teamInner.textContent = '';
|
|
1440
|
-
teamBack = closeTeam;
|
|
1441
|
-
var head = el('div', 'head');
|
|
1442
|
-
head.appendChild(el('h1', null, title));
|
|
1443
|
-
var close = el('button', 'quiet', 'close');
|
|
1444
|
-
close.type = 'button';
|
|
1445
|
-
close.addEventListener('click', closeTeam);
|
|
1446
|
-
head.appendChild(close);
|
|
1447
|
-
teamInner.appendChild(head);
|
|
1195
|
+
// first look: a card already on screen shows settled, anything else is the sphere
|
|
1196
|
+
function firstLook() {
|
|
1197
|
+
api('/card').then(function (s) {
|
|
1198
|
+
if (s.http !== 200) return;
|
|
1199
|
+
if (s.card) { orb.settled(); showCard(s.card, false); return; }
|
|
1200
|
+
line.set(s.status === 'skipped' ? 'not now. tap when you want another' : 'tap to think', false);
|
|
1201
|
+
});
|
|
1448
1202
|
}
|
|
1449
1203
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1204
|
+
// ---- the footer: the team and the yeses waiting, both cards like any other ---------------
|
|
1205
|
+
// From any card, or the big orb: the Mac serves the team table or the job
|
|
1206
|
+
// deck as the next card, and its back option returns to where the owner was.
|
|
1207
|
+
function openFrom(which) {
|
|
1208
|
+
if (bet.busy) return;
|
|
1209
|
+
buzz();
|
|
1210
|
+
bet.busy = true;
|
|
1211
|
+
bet.tapAt = performance.now();
|
|
1212
|
+
var answer = api('/act', { open: which, card_id: bet.shown ? bet.shown.id : null }).catch(offline);
|
|
1213
|
+
if (!document.body.classList.contains('has-card')) {
|
|
1214
|
+
answer.then(function (r) {
|
|
1215
|
+
if (!r.ok || !r.card) { bet.busy = false; line.set((r && r.message) || 'that did not go through. tap to try again', true); return; }
|
|
1216
|
+
spiralTo(r.card, null);
|
|
1217
|
+
});
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
flow(answer, bet.shown, null, null);
|
|
1454
1221
|
}
|
|
1222
|
+
document.getElementById('working').addEventListener('click', function () { openFrom('team'); });
|
|
1223
|
+
document.getElementById('needs').addEventListener('click', function () { openFrom('deck'); });
|
|
1455
1224
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
if (!
|
|
1459
|
-
|
|
1460
|
-
var row = el('button', 'row');
|
|
1461
|
-
row.type = 'button';
|
|
1462
|
-
row.appendChild(el('span', 'who', r.member));
|
|
1463
|
-
row.appendChild(el('span', 'st ' + r.status.replace(/ /g, '-'), r.status));
|
|
1464
|
-
row.appendChild(el('span', 'on', r.on));
|
|
1465
|
-
row.appendChild(el('span', 'time', r.time));
|
|
1466
|
-
row.addEventListener('click', function () { drawTalk(r); });
|
|
1467
|
-
teamInner.appendChild(row);
|
|
1468
|
-
});
|
|
1225
|
+
// the option on the card that does this, if it has one
|
|
1226
|
+
function optionFor(action, step) {
|
|
1227
|
+
if (!bet.shown) return null;
|
|
1228
|
+
return bet.shown.options.filter(function (o) { return o.action === action && (step == null || (o.binds && o.binds.step === step)); })[0] || null;
|
|
1469
1229
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
teamHead(r.member);
|
|
1474
|
-
teamBack = openTeam;
|
|
1475
|
-
teamInner.appendChild(el('p', 'why', r.status + ': ' + r.on + '. ' + r.time + '.'));
|
|
1476
|
-
var talkBox = convo(function (body) { body.member = r.member; return api('/bet/steer', body); }, function (ack) {
|
|
1477
|
-
talkBox.replaceWith(el('p', 'mission', ack));
|
|
1478
|
-
}, 'tell ' + r.member + ' something');
|
|
1479
|
-
var back = el('button', 'quiet', 'back to the team');
|
|
1480
|
-
back.type = 'button';
|
|
1481
|
-
back.addEventListener('click', openTeam);
|
|
1482
|
-
teamInner.appendChild(talkBox);
|
|
1483
|
-
teamInner.appendChild(back);
|
|
1230
|
+
function useOption(o) {
|
|
1231
|
+
var b = o && box().querySelector('[data-option="' + o.id + '"]');
|
|
1232
|
+
if (b && !bet.busy) b.click();
|
|
1484
1233
|
}
|
|
1485
1234
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
// Esc on the team page steps back one layer, the way its own buttons do:
|
|
1489
|
-
// words being typed are cancelled and the cursor leaves first, then the
|
|
1490
|
-
// talk card goes back to the team, then the team closes to the card.
|
|
1491
|
-
// On the card itself it does nothing. It listens on the way down, so the
|
|
1492
|
-
// focused element is still the field the key was pressed in.
|
|
1235
|
+
// Esc: words being typed are cancelled and the cursor leaves first; then
|
|
1236
|
+
// a card with a way back takes it. On a card with none it does nothing.
|
|
1493
1237
|
document.addEventListener('keydown', function (e) {
|
|
1494
|
-
if (e.key !== 'Escape' || e.isComposing ||
|
|
1495
|
-
e.preventDefault();
|
|
1238
|
+
if (e.key !== 'Escape' || e.isComposing || !bet.shown || !document.body.classList.contains('has-card')) return;
|
|
1496
1239
|
var field = document.activeElement;
|
|
1497
|
-
if (field &&
|
|
1240
|
+
if (field && field.tagName === 'INPUT') {
|
|
1241
|
+
e.preventDefault();
|
|
1498
1242
|
field.value = '';
|
|
1499
|
-
// the field's own input listener drops a second tap armed for those words
|
|
1500
|
-
field.dispatchEvent(new Event('input'));
|
|
1501
1243
|
field.blur();
|
|
1502
1244
|
return;
|
|
1503
1245
|
}
|
|
1504
|
-
|
|
1246
|
+
var back = optionFor('back');
|
|
1247
|
+
if (back) { e.preventDefault(); useOption(back); }
|
|
1505
1248
|
}, true);
|
|
1506
1249
|
|
|
1250
|
+
// the arrows and a sideways swipe flip the job deck, unless the owner is typing
|
|
1251
|
+
document.addEventListener('keydown', function (e) {
|
|
1252
|
+
var step = e.key === 'ArrowLeft' ? -1 : e.key === 'ArrowRight' ? 1 : 0;
|
|
1253
|
+
var field = document.activeElement;
|
|
1254
|
+
if (!step || e.isComposing || (field && field.tagName === 'INPUT')) return;
|
|
1255
|
+
var o = optionFor('flip', step);
|
|
1256
|
+
if (!o) return;
|
|
1257
|
+
e.preventDefault();
|
|
1258
|
+
useOption(o);
|
|
1259
|
+
});
|
|
1260
|
+
var swipe = null;
|
|
1261
|
+
box().addEventListener('touchstart', function (e) { swipe = { x: e.touches[0].clientX, y: e.touches[0].clientY }; }, { passive: true });
|
|
1262
|
+
box().addEventListener('touchend', function (e) {
|
|
1263
|
+
if (!swipe) return;
|
|
1264
|
+
var dx = e.changedTouches[0].clientX - swipe.x, dy = e.changedTouches[0].clientY - swipe.y;
|
|
1265
|
+
swipe = null;
|
|
1266
|
+
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) useOption(optionFor('flip', dx < 0 ? 1 : -1));
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1507
1269
|
// the footer: how many members are really working, or the team when none are
|
|
1508
1270
|
function drawWorking(n) {
|
|
1509
1271
|
var btn = document.getElementById('working');
|
|
1510
1272
|
btn.textContent = n ? n + ' working' : 'the team';
|
|
1511
1273
|
}
|
|
1512
1274
|
|
|
1275
|
+
// Every few seconds: the footer counts, and the card on screen. When the
|
|
1276
|
+
// Mac's card has changed (a new id: its options are not the ones shown),
|
|
1277
|
+
// it turns in place, unless the owner is typing or a motion is under way.
|
|
1513
1278
|
function refresh() {
|
|
1514
|
-
return api('/
|
|
1279
|
+
return api('/card').then(function (s) {
|
|
1515
1280
|
if (s.http !== 200) { line.set('this link stopped working. start gm phone again on the mac.', true); return; }
|
|
1516
1281
|
drawWorking(s.team_working || 0);
|
|
1282
|
+
if (s.card && bet.shown && s.card.id !== bet.shown.id && !bet.busy && !typingOnCard() && document.body.classList.contains('has-card')) {
|
|
1283
|
+
bet.busy = true;
|
|
1284
|
+
turnTo(s.card);
|
|
1285
|
+
}
|
|
1517
1286
|
}).catch(function () { /* the next refresh tries again */ });
|
|
1518
1287
|
}
|
|
1519
1288
|
|