atris 3.74.5 → 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/commands/gm.js +1 -1
- package/commands/task.js +4 -2
- package/commands/xp.js +68 -15
- package/commands/zen.js +10 -3
- package/lib/career-xp-lock.js +61 -0
- package/lib/career-xp-root.js +61 -0
- package/lib/gm-bet.js +405 -17
- package/lib/gm-card.js +431 -0
- package/lib/gm-phone-page.js +481 -1004
- package/lib/gm-phone.js +33 -38
- package/lib/task-db.js +8 -4
- package/lib/yes-lock.js +22 -3
- package/lib/zen-apply.js +1 -0
- package/lib/zen.js +15 -0
- package/package.json +1 -1
package/lib/gm-phone-page.js
CHANGED
|
@@ -71,55 +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
|
-
.
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.
|
|
96
|
-
.
|
|
97
|
-
.
|
|
98
|
-
/*
|
|
99
|
-
.
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
.
|
|
104
|
-
.team .models h2 { margin: 10px 0 0; color: var(--text-2); font-size: 17px; font-weight: 400; }
|
|
105
|
-
.team .models .choice { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
106
|
-
.team .models .choice .w { color: var(--text-2); font-size: 17px; white-space: nowrap; }
|
|
107
|
-
.team .models .choice[disabled] { cursor: default; }
|
|
108
|
-
.team td.st { min-width: 16rem; color: var(--text-2); }
|
|
109
|
-
.team td.st.working { color: var(--text); }
|
|
110
|
-
.team td.st.waiting-for-your-yes { color: var(--amber); }
|
|
111
|
-
/* the job deck: one running job at a time, in one spot, with its place in
|
|
112
|
-
the deck. the card is a .bet card, so a flip is the main card's own turn */
|
|
113
|
-
.deckspot { position: relative; }
|
|
114
|
-
.deckcard { display: flex; flex-direction: column; gap: 12px; touch-action: pan-y; }
|
|
115
|
-
.deckcard .count, .deckcard .label, .deckcard .time { font-size: 17px; color: var(--text-2); }
|
|
116
|
-
.deckcard .label.needs-you { color: var(--amber); }
|
|
117
|
-
.deckcard .why, .deckcard .act, .deckcard .mission { margin: 0; }
|
|
118
|
-
.jobs-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; border: 1px solid var(--line); border-radius: 14px; }
|
|
119
|
-
.team table.jobs { min-width: 56rem; }
|
|
120
|
-
.jobs td.work { min-width: 14rem; }
|
|
121
|
-
.jobs td.done, .jobs td.member, .jobs td.engine, .jobs td.status { white-space: nowrap; }
|
|
122
|
-
.deckbar { display: flex; justify-content: space-between; gap: 12px; }
|
|
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; }
|
|
123
106
|
.sayline { margin: 10px 0 0; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
124
107
|
.sayline .thread, .sayline .reply { width: 100%; text-align: left; margin: 0; }
|
|
125
108
|
.unsent { margin: 0; text-align: center; }
|
|
@@ -165,6 +148,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
165
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); }
|
|
166
149
|
.bet.up { opacity: 1; transform: none; }
|
|
167
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; }
|
|
168
155
|
/* card to card: a crossfade in place. the old card is a still copy that
|
|
169
156
|
fades over the box while the new card fades in under it from a small
|
|
170
157
|
drift, its title landing where the old title was; the box eases to the
|
|
@@ -229,7 +216,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
229
216
|
</div>
|
|
230
217
|
<p class="line" id="line" aria-live="polite">tap to think</p>
|
|
231
218
|
<article class="bet" id="bet" hidden></article>
|
|
232
|
-
<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>
|
|
233
220
|
</main>
|
|
234
221
|
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap to go back to the big orb">
|
|
235
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>
|
|
@@ -238,7 +225,6 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
238
225
|
<div class="core" id="lava-core"></div>
|
|
239
226
|
<div class="dim" id="lava-dim"></div>
|
|
240
227
|
</div>
|
|
241
|
-
<section class="team" id="team" hidden role="dialog" aria-label="the team"><div class="inner" id="team-inner"></div></section>
|
|
242
228
|
<script nonce="${nonce}">
|
|
243
229
|
(function () {
|
|
244
230
|
var token = new URLSearchParams(location.search).get('t') || '';
|
|
@@ -259,7 +245,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
259
245
|
opts.body = JSON.stringify(body);
|
|
260
246
|
}
|
|
261
247
|
return fetch(path + (body ? '' : '?t=' + encodeURIComponent(token)), opts)
|
|
262
|
-
.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') : '';
|
|
263
257
|
}
|
|
264
258
|
|
|
265
259
|
// ---- the one line under the orb: fades out, changes, fades in ------------------
|
|
@@ -310,6 +304,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
310
304
|
// the fast reveal stays contained: the lava grows to BLOOM of the smaller
|
|
311
305
|
// window side and never washes the page
|
|
312
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;
|
|
313
312
|
// Two speeds, chosen by truth. A bet already waiting in the queue plays
|
|
314
313
|
// the same curves at FAST of their length (tap to card in about two seconds, unhurried).
|
|
315
314
|
// A bet still being made plays the full landing length, and the spiral
|
|
@@ -499,8 +498,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
499
498
|
function grow() {
|
|
500
499
|
var rect = canvas.getBoundingClientRect();
|
|
501
500
|
var contained = pace < 1;
|
|
502
|
-
var growMs = contained ? FAST_GROW_MS : (GROW_MS * pace);
|
|
503
|
-
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);
|
|
504
503
|
var vx = rect.left + cx, vy = rect.top + cy;
|
|
505
504
|
var reach = contained
|
|
506
505
|
? BLOOM * Math.min(window.innerWidth, window.innerHeight) / 2
|
|
@@ -572,7 +571,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
572
571
|
// swap() takes the card off the page once it has faded. At the centre the
|
|
573
572
|
// lava melts and the sphere grows back out of the point, holds for a
|
|
574
573
|
// beat so it reads, then calls back. Reduced motion: no travel.
|
|
575
|
-
|
|
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;
|
|
576
577
|
lava.classList.remove('tap', 'alive');
|
|
577
578
|
lava.tabIndex = -1;
|
|
578
579
|
var swapped = false;
|
|
@@ -589,7 +590,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
589
590
|
mode = 'idle';
|
|
590
591
|
bridgeAt = reduced ? 0 : performance.now();
|
|
591
592
|
start();
|
|
592
|
-
setTimeout(cb, reduced ? 0 :
|
|
593
|
+
setTimeout(cb, reduced ? 0 : beatMs);
|
|
593
594
|
};
|
|
594
595
|
if (reduced || lava.style.visibility !== 'visible') { reform(); return; }
|
|
595
596
|
var r = lava.getBoundingClientRect();
|
|
@@ -600,9 +601,9 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
600
601
|
var sEnd = 30 / base;
|
|
601
602
|
var t0 = performance.now();
|
|
602
603
|
var step = function (now) {
|
|
603
|
-
var k = Math.min(1, (now - t0) /
|
|
604
|
+
var k = Math.min(1, (now - t0) / glideMs);
|
|
604
605
|
var t = ease(k);
|
|
605
|
-
if (now - t0 >=
|
|
606
|
+
if (now - t0 >= leaveMs) takeOff();
|
|
606
607
|
place(from.s + (sEnd - from.s) * t, from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t);
|
|
607
608
|
core.style.opacity = String(smooth(0.3, 0.9, k));
|
|
608
609
|
lava.style.opacity = String(1 - smooth(0.7, 1, k));
|
|
@@ -648,11 +649,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
648
649
|
|
|
649
650
|
return {
|
|
650
651
|
// the bet is ready: the homepage tap, on the clock
|
|
651
|
-
|
|
652
|
+
// act: the owner acted on a card, so it is the one quick motion
|
|
653
|
+
play: function (cb, fast, act) {
|
|
652
654
|
onBorn = cb;
|
|
655
|
+
quick = Boolean(act);
|
|
653
656
|
if (reduced) { settled(); return; }
|
|
654
|
-
pace = fast ? FAST : 1;
|
|
655
|
-
|
|
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));
|
|
656
660
|
timed = true;
|
|
657
661
|
start();
|
|
658
662
|
},
|
|
@@ -679,194 +683,191 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
679
683
|
};
|
|
680
684
|
})();
|
|
681
685
|
|
|
682
|
-
// ---- the
|
|
683
|
-
|
|
684
|
-
//
|
|
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
|
|
685
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'];
|
|
686
704
|
|
|
687
705
|
function buzz() { if (navigator.vibrate) { try { navigator.vibrate(10); } catch (e) { /* not on this device */ } } }
|
|
688
706
|
|
|
689
|
-
//
|
|
690
|
-
function
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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 + ': '));
|
|
713
754
|
}
|
|
714
|
-
|
|
715
|
-
armed = false;
|
|
716
|
-
go.disabled = true;
|
|
717
|
-
run();
|
|
755
|
+
links.appendChild(optionButton(o));
|
|
718
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));
|
|
719
761
|
}
|
|
720
762
|
|
|
721
|
-
// a line
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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;
|
|
785
|
+
});
|
|
786
|
+
if (mine.length) tr.addEventListener('click', function () { var first = tr.querySelector('button'); if (first) first.click(); });
|
|
734
787
|
});
|
|
735
|
-
return
|
|
788
|
+
return grid;
|
|
736
789
|
}
|
|
737
790
|
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
function
|
|
741
|
-
var
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
ok.addEventListener('click', function () {
|
|
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 () {
|
|
809
|
+
if (bet.busy) return;
|
|
758
810
|
buzz();
|
|
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
|
-
|
|
820
|
+
return btn;
|
|
762
821
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
// waits. It also waits while the owner types there (the box has the
|
|
771
|
-
// cursor or any words), so it comes in after they send, or once they
|
|
772
|
-
// clear the box and leave it; and on the big orb, it waits for their tap.
|
|
773
|
-
var WAIT_LOOK_MS = 1500;
|
|
774
|
-
function typingOnCard() {
|
|
775
|
-
var input = document.getElementById('bet').querySelector('.field input');
|
|
776
|
-
return Boolean(input && (input === document.activeElement || input.value.trim()));
|
|
777
|
-
}
|
|
778
|
-
function watchWait() {
|
|
779
|
-
if (bet.watching) return;
|
|
780
|
-
var quiet = function () { return !bet.busy && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
781
|
-
bet.watching = setInterval(function () {
|
|
782
|
-
if (!bet.last || bet.last.kind !== 'wait') { clearInterval(bet.watching); bet.watching = null; return; }
|
|
783
|
-
if (!quiet()) return;
|
|
784
|
-
api('/bet').then(function (s) {
|
|
785
|
-
if (s.http !== 200 || !quiet() || !bet.last || bet.last.kind !== 'wait') return;
|
|
786
|
-
if (s.id !== bet.last.id && s.kind !== 'wait' && isCard(s)) { bet.status = s.status; turn(s); return; }
|
|
787
|
-
if (s.kind === 'wait' && s.queue > 0) think(false, 0);
|
|
788
|
-
}).catch(function () { /* the next look tries again */ });
|
|
789
|
-
}, WAIT_LOOK_MS);
|
|
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);
|
|
827
|
+
});
|
|
828
|
+
return btn;
|
|
790
829
|
}
|
|
791
830
|
|
|
792
|
-
//
|
|
793
|
-
function
|
|
794
|
-
var
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
q.choices.forEach(function (c) {
|
|
801
|
-
var b = el('button', 'choice', c.label);
|
|
802
|
-
b.type = 'button';
|
|
803
|
-
b.addEventListener('click', function () {
|
|
804
|
-
if (bet.busy) return;
|
|
805
|
-
buzz();
|
|
806
|
-
// typed words are read first, so a question back is explained and nothing is saved
|
|
807
|
-
if (c.other) { typeHere(b, c.label, 60, function (text, field) { say(field, { text: text }); }); return; }
|
|
808
|
-
answer(b, { choice: c.id });
|
|
809
|
-
});
|
|
810
|
-
if (c.other) b.classList.add('other');
|
|
811
|
-
act.appendChild(b);
|
|
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); });
|
|
812
839
|
});
|
|
813
|
-
|
|
814
|
-
act.appendChild(el('div', 'links'));
|
|
815
|
-
box.appendChild(act);
|
|
816
|
-
// its own something else box is the one place to type
|
|
817
|
-
if (!q.choices.some(function (c) { return c.other; })) box.appendChild(sayBox());
|
|
840
|
+
return wrap;
|
|
818
841
|
}
|
|
819
842
|
|
|
820
|
-
// ---- answering in place: the eyes stay in one spot
|
|
821
|
-
//
|
|
822
|
-
// chosen for HOLD_MS while the rest of the card dims and stays put. The
|
|
823
|
-
// next card is asked for during the hold and turns in once it is over.
|
|
824
|
-
var HOLD_MS = 400;
|
|
825
|
-
|
|
826
|
-
// a failed fetch reads like a refusal, so a hold never hangs
|
|
827
|
-
function offline() { return { ok: false, http: 0 }; }
|
|
828
|
-
|
|
829
|
-
// every answer row of the card dims but the one in use; no spot, none
|
|
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
|
|
830
845
|
function dimOthers(spot) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
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));
|
|
834
848
|
});
|
|
835
849
|
}
|
|
836
850
|
|
|
837
|
-
// the spot holds as chosen and the card takes no taps; returns when it
|
|
838
|
-
// 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
|
|
839
852
|
function hold(spot) {
|
|
840
|
-
var box = document.getElementById('bet');
|
|
841
853
|
dimOthers(spot);
|
|
842
854
|
spot.classList.add('chosen');
|
|
843
|
-
box.classList.add('holding');
|
|
855
|
+
box().classList.add('holding');
|
|
844
856
|
var input = spot.querySelector('input');
|
|
845
857
|
if (input) { input.readOnly = true; input.blur(); }
|
|
846
858
|
bet.busy = true;
|
|
847
|
-
bet.armed = false;
|
|
848
859
|
bet.tapAt = performance.now();
|
|
849
860
|
document.body.removeAttribute('data-reveal-ms');
|
|
850
861
|
return bet.tapAt;
|
|
851
862
|
}
|
|
852
863
|
|
|
853
|
-
// it did not go through: the card is as it was
|
|
854
|
-
function unhold() {
|
|
855
|
-
var box = document.getElementById('bet');
|
|
856
|
-
box.classList.remove('holding');
|
|
857
|
-
dimOthers(null);
|
|
858
|
-
box.querySelectorAll('.chosen').forEach(function (n) { n.classList.remove('chosen'); });
|
|
859
|
-
bet.busy = false;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// resolves once the hold that started at heldAt has run HOLD_MS
|
|
863
864
|
function afterHold(heldAt) {
|
|
864
865
|
var left = heldAt ? heldAt + HOLD_MS - performance.now() : 0;
|
|
865
866
|
return new Promise(function (resolve) { setTimeout(resolve, Math.max(0, left)); });
|
|
866
867
|
}
|
|
867
868
|
|
|
868
869
|
// The tapped button becomes the field at its own size and spot, with the
|
|
869
|
-
// cursor already in it, and the other
|
|
870
|
+
// cursor already in it, and the other options dim. Enter or send hands
|
|
870
871
|
// the words to sent(text, field). Left empty, it turns back into the button.
|
|
871
872
|
function typeHere(btn, placeholder, maxLength, sent) {
|
|
872
873
|
var field = el('div', btn.classList.contains('choice') ? 'choice field' : 'field');
|
|
@@ -878,18 +879,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
878
879
|
input.placeholder = placeholder;
|
|
879
880
|
input.enterKeyHint = 'send';
|
|
880
881
|
input.setAttribute('aria-label', placeholder);
|
|
881
|
-
var
|
|
882
|
-
|
|
882
|
+
var go = el('button', 'send', 'send');
|
|
883
|
+
go.type = 'button';
|
|
883
884
|
var submit = function () {
|
|
884
885
|
var text = input.value.trim();
|
|
885
886
|
if (text && !bet.busy) sent(text, field);
|
|
886
887
|
};
|
|
887
888
|
// a press on send keeps the cursor in the field, so it never closes first
|
|
888
|
-
|
|
889
|
-
|
|
889
|
+
go.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
890
|
+
go.addEventListener('click', function () { buzz(); submit(); });
|
|
890
891
|
input.addEventListener('keydown', function (e) {
|
|
891
892
|
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
892
|
-
if (e.key === 'Escape') { input.value = ''; input.blur(); }
|
|
893
893
|
});
|
|
894
894
|
input.addEventListener('blur', function () {
|
|
895
895
|
if (input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
|
|
@@ -897,370 +897,130 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
897
897
|
dimOthers(null);
|
|
898
898
|
});
|
|
899
899
|
field.appendChild(input);
|
|
900
|
-
field.appendChild(
|
|
900
|
+
field.appendChild(go);
|
|
901
901
|
btn.replaceWith(field);
|
|
902
902
|
dimOthers(field);
|
|
903
903
|
input.focus();
|
|
904
|
+
return field;
|
|
904
905
|
}
|
|
905
906
|
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
body.card = shownCard();
|
|
911
|
-
var heldAt = hold(spot);
|
|
912
|
-
api('/bet/answer', body).catch(offline).then(function (r) {
|
|
913
|
-
if (r.ok) { think(false, heldAt); return; }
|
|
914
|
-
unhold();
|
|
915
|
-
var hint = document.getElementById('bet').querySelector('.act .links');
|
|
916
|
-
if (hint) hint.textContent = r.message || 'that did not go through';
|
|
917
|
-
var input = spot.querySelector('input');
|
|
918
|
-
if (input) { input.readOnly = false; dimOthers(spot); input.focus(); }
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// the card the owner is looking at, named with every answer and every
|
|
923
|
-
// word said, so the server acts on that card or on nothing
|
|
924
|
-
function shownCard() { return bet.last && bet.last.id ? bet.last.id : ''; }
|
|
925
|
-
|
|
926
|
-
// Words said on a card hold their spot as chosen while the server reads
|
|
927
|
-
// them. An answer or an instruction brings the next card; a question
|
|
928
|
-
// back or a second tap it needs turns in as the orb's reply, in the
|
|
929
|
-
// title's spot. If it did not send, the words stay where they were.
|
|
930
|
-
// One id per typed send: made when these words are first sent from this
|
|
931
|
-
// spot, the same on every retry of them, and new when the words change,
|
|
932
|
-
// 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
|
|
933
911
|
// works on the plain http of the wifi link, where randomUUID does not.
|
|
934
|
-
|
|
935
|
-
|
|
912
|
+
var sent = { key: null, id: null };
|
|
913
|
+
function sendId(key) {
|
|
914
|
+
if (sent.key !== key) {
|
|
936
915
|
var n = new Uint32Array(2);
|
|
937
916
|
crypto.getRandomValues(n);
|
|
938
|
-
|
|
939
|
-
|
|
917
|
+
sent.key = key;
|
|
918
|
+
sent.id = ('000000' + n[0].toString(36)).slice(-7) + ('000000' + n[1].toString(36)).slice(-7);
|
|
940
919
|
}
|
|
941
|
-
return
|
|
920
|
+
return sent.id;
|
|
942
921
|
}
|
|
943
922
|
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
if (
|
|
954
|
-
var
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
function say(spot, body) {
|
|
959
|
-
body.card = shownCard();
|
|
960
|
-
body.send = sendId(spot, body.text);
|
|
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(' | '));
|
|
961
936
|
var heldAt = hold(spot);
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
// It did not send: the card is as it was, the words still in the box with
|
|
971
|
-
// the cursor in it, and one short line under it says why, so Enter (or
|
|
972
|
-
// the same tap) tries again. Left empty, the box turns back into the
|
|
973
|
-
// button and the line goes with it.
|
|
974
|
-
function unsent(spot, why) {
|
|
975
|
-
doneReading(spot);
|
|
976
|
-
unhold();
|
|
977
|
-
var note = spot.nextElementSibling;
|
|
978
|
-
var input = spot.querySelector('input');
|
|
979
|
-
if (!note || !note.classList.contains('unsent')) {
|
|
980
|
-
note = el('p', 'links unsent');
|
|
981
|
-
spot.after(note);
|
|
982
|
-
if (input) input.addEventListener('blur', function () { if (!spot.isConnected) note.remove(); });
|
|
983
|
-
}
|
|
984
|
-
note.textContent = why;
|
|
985
|
-
if (input) { input.readOnly = false; dimOthers(spot); input.focus(); }
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// one reply to tap: it holds as chosen and is said
|
|
989
|
-
function sayChoice(label, body) {
|
|
990
|
-
var b = el('button', 'choice', label);
|
|
991
|
-
b.type = 'button';
|
|
992
|
-
b.addEventListener('click', function () {
|
|
993
|
-
if (bet.busy) return;
|
|
994
|
-
buzz();
|
|
995
|
-
say(b, body);
|
|
996
|
-
});
|
|
997
|
-
return b;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
// the orb's reply is the title, with the card's own answer right under it
|
|
1001
|
-
// (do it, yes, nice, or the answers), then up to three suggested replies;
|
|
1002
|
-
// starting a member who is not running takes one more tap, or it is kept
|
|
1003
|
-
// as a note. The say line takes anything else.
|
|
1004
|
-
function drawReply(r, said) {
|
|
1005
|
-
var box = document.getElementById('bet');
|
|
1006
|
-
// a reply never takes the card's own answer away: the card is drawn
|
|
1007
|
-
// once more, and its row is kept, buttons and all
|
|
1008
|
-
var own = null;
|
|
1009
|
-
if (bet.last) { drawBetCard(bet.last); own = box.querySelector(':scope > .act, :scope > .options'); }
|
|
1010
|
-
box.textContent = '';
|
|
1011
|
-
bet.drawn = null;
|
|
1012
|
-
var title = el('h1', null, r.stay ? r.reply : (r.message || 'that did not go through'));
|
|
1013
|
-
box.appendChild(title);
|
|
1014
|
-
// more than one line reads at the body size, in the same spot
|
|
1015
|
-
var lines = document.createRange();
|
|
1016
|
-
lines.selectNodeContents(title);
|
|
1017
|
-
if (lines.getClientRects().length > 1) title.classList.add('said');
|
|
1018
|
-
if (own) box.appendChild(own);
|
|
1019
|
-
var act = el('div', 'act answers');
|
|
1020
|
-
var have = own ? Array.prototype.map.call(own.querySelectorAll('button'), function (b) { return b.textContent.trim().toLowerCase(); }) : [];
|
|
1021
|
-
if (r.stay) (r.suggestions || []).forEach(function (sug) {
|
|
1022
|
-
if (have.indexOf(String(sug).trim().toLowerCase()) < 0) act.appendChild(sayChoice(sug, { text: sug }));
|
|
1023
|
-
});
|
|
1024
|
-
if (r.reason === 'needs_confirm') {
|
|
1025
|
-
act.appendChild(sayChoice('start it', { text: said, confirm: true }));
|
|
1026
|
-
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;
|
|
1027
945
|
}
|
|
1028
|
-
|
|
1029
|
-
if (!own || !own.querySelector('.other')) box.appendChild(sayBox());
|
|
946
|
+
afterHold(heldAt).then(function () { flow(answer, from, body.text, option); });
|
|
1030
947
|
}
|
|
1031
948
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
var
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
input.value = '';
|
|
1052
|
-
input.placeholder = r.message || 'that did not go through';
|
|
1053
|
-
});
|
|
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);
|
|
1054
968
|
};
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
// a quiet way out: moves on, saves nothing, and is not asked again today
|
|
1060
|
-
var links = el('div', 'links');
|
|
1061
|
-
var later = el('button', null, 'not now');
|
|
1062
|
-
later.type = 'button';
|
|
1063
|
-
later.addEventListener('click', function () { if (bet.busy) return; buzz(); think(false, 0); });
|
|
1064
|
-
links.appendChild(later);
|
|
1065
|
-
act.appendChild(links);
|
|
1066
|
-
box.appendChild(act);
|
|
1067
|
-
box.appendChild(sayBox());
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// A conversation in one box. While the words are read the reply spot shows
|
|
1071
|
-
// a quiet ellipsis; a question back is answered in place, with the turns so
|
|
1072
|
-
// far above it and up to three suggested replies to tap. Starting a member
|
|
1073
|
-
// who is not running takes a second tap, or it is kept as a note.
|
|
1074
|
-
// post(body) sends it; done(ack) runs when something real came of it.
|
|
1075
|
-
function convo(post, done, placeholder) {
|
|
1076
|
-
var wrap = el('div', 'links sayline');
|
|
1077
|
-
var thread = el('div', 'thread');
|
|
1078
|
-
var reply = el('p', 'why reply');
|
|
1079
|
-
reply.hidden = true;
|
|
1080
|
-
var input = el('input', 'say');
|
|
1081
|
-
input.type = 'text';
|
|
1082
|
-
input.maxLength = 280;
|
|
1083
|
-
input.setAttribute('aria-label', placeholder);
|
|
1084
|
-
input.placeholder = placeholder;
|
|
1085
|
-
var send = el('button', 'pill', 'send');
|
|
1086
|
-
send.type = 'button';
|
|
1087
|
-
var extra = el('div', 'links');
|
|
1088
|
-
var pending = null;
|
|
1089
|
-
// an armed second tap belongs to the words it was asked about: any edit clears it
|
|
1090
|
-
function disarm() {
|
|
1091
|
-
pending = null;
|
|
1092
|
-
send.textContent = 'send';
|
|
1093
|
-
send.className = 'pill';
|
|
1094
|
-
extra.textContent = '';
|
|
1095
|
-
reply.textContent = '';
|
|
1096
|
-
reply.hidden = true;
|
|
1097
|
-
}
|
|
1098
|
-
function drawThread(turns) {
|
|
1099
|
-
thread.textContent = '';
|
|
1100
|
-
turns.forEach(function (t) { thread.appendChild(el('p', 'turn ' + (t.who === 'owner' ? 'you' : 'orb'), t.text)); });
|
|
1101
|
-
}
|
|
1102
|
-
function go(more, said) {
|
|
1103
|
-
var text = said || input.value.trim();
|
|
1104
|
-
if (!text) return;
|
|
1105
|
-
send.disabled = true;
|
|
1106
|
-
reply.hidden = false;
|
|
1107
|
-
reply.textContent = '\u2026';
|
|
1108
|
-
extra.textContent = '';
|
|
1109
|
-
// the same words carry the same id on every try, so a retry never sends twice
|
|
1110
|
-
var body = { text: text, send: sendId(wrap, text) };
|
|
1111
|
-
if (more) Object.keys(more).forEach(function (k) { body[k] = more[k]; });
|
|
1112
|
-
post(body).catch(offline).then(function (r) {
|
|
1113
|
-
if (r.ok && !r.stay) { done(r.ack); return; }
|
|
1114
|
-
send.disabled = false;
|
|
1115
|
-
if (r.stay) { reply.textContent = r.reply;
|
|
1116
|
-
input.value = '';
|
|
1117
|
-
drawThread(r.thread.slice(0, -1));
|
|
1118
|
-
(r.suggestions || []).forEach(function (sug) {
|
|
1119
|
-
var b = el('button', 'quiet', sug);
|
|
1120
|
-
b.type = 'button';
|
|
1121
|
-
b.addEventListener('click', function () { buzz(); go(null, sug); });
|
|
1122
|
-
extra.appendChild(b);
|
|
1123
|
-
});
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
if (r.reason === 'needs_confirm') {
|
|
1127
|
-
pending = text;
|
|
1128
|
-
reply.textContent = r.message;
|
|
1129
|
-
send.textContent = 'tap again';
|
|
1130
|
-
send.className = 'pill armed';
|
|
1131
|
-
var keep = el('button', 'quiet', 'keep it as a note instead');
|
|
1132
|
-
keep.type = 'button';
|
|
1133
|
-
keep.addEventListener('click', function () { disarm(); go({ note: true }); });
|
|
1134
|
-
extra.appendChild(keep);
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
// it did not send: the words are in the box, send works again, and
|
|
1138
|
-
// one short line says so
|
|
1139
|
-
if (said && !input.value) input.value = said;
|
|
1140
|
-
reply.textContent = r.message || 'that did not send. tap send to try again';
|
|
1141
|
-
input.focus();
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
send.addEventListener('click', function () {
|
|
1145
|
-
buzz();
|
|
1146
|
-
if (pending) { disarm(); go({ confirm: true }); return; }
|
|
1147
|
-
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();
|
|
1148
973
|
});
|
|
1149
|
-
|
|
1150
|
-
input.addEventListener('input', function () { if (pending) disarm(); });
|
|
1151
|
-
wrap.appendChild(thread);
|
|
1152
|
-
wrap.appendChild(reply);
|
|
1153
|
-
wrap.appendChild(input);
|
|
1154
|
-
wrap.appendChild(send);
|
|
1155
|
-
wrap.appendChild(extra);
|
|
1156
|
-
return wrap;
|
|
974
|
+
leave(function () { sphere = true; land(); }, true);
|
|
1157
975
|
}
|
|
1158
976
|
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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);
|
|
1173
993
|
}
|
|
1174
994
|
|
|
1175
|
-
//
|
|
1176
|
-
function
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
b.appendChild(el('span', 't', o.title));
|
|
1185
|
-
b.appendChild(el('span', 'w', o.why));
|
|
1186
|
-
b.addEventListener('click', function () {
|
|
1187
|
-
if (bet.busy) return;
|
|
1188
|
-
buzz();
|
|
1189
|
-
var heldAt = hold(b);
|
|
1190
|
-
api('/bet/pick', { id: o.id }).then(function (r) {
|
|
1191
|
-
if (r.http === 200 && r.bet) { afterHold(heldAt).then(function () { turn(r); }); return; }
|
|
1192
|
-
think(false, heldAt);
|
|
1193
|
-
}, function () { think(false, heldAt); });
|
|
1194
|
-
});
|
|
1195
|
-
list.appendChild(b);
|
|
1196
|
-
});
|
|
1197
|
-
box.appendChild(list);
|
|
1198
|
-
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);
|
|
1199
1004
|
}
|
|
1200
1005
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1206
|
-
var
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
//
|
|
1211
|
-
box.
|
|
1212
|
-
if (
|
|
1213
|
-
if (
|
|
1214
|
-
var
|
|
1215
|
-
|
|
1216
|
-
go.type = 'button';
|
|
1217
|
-
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');
|
|
1218
|
-
go.addEventListener('click', function () {
|
|
1219
|
-
buzz();
|
|
1220
|
-
if (!bet.armed) {
|
|
1221
|
-
bet.armed = true; drawBetCard(s);
|
|
1222
|
-
setTimeout(function () { if (bet.armed) { bet.armed = false; if (bet.last) drawBetCard(bet.last); } }, 5000);
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
bet.armed = false;
|
|
1226
|
-
s.status = 'starting'; s.unstarted = null; drawBetCard(s);
|
|
1227
|
-
api('/bet/do', { confirm: true }).catch(offline).then(function (r) {
|
|
1228
|
-
if (!r || !r.ok) {
|
|
1229
|
-
// it did not get going: the card comes back as it was, do it and
|
|
1230
|
-
// all, with one line why, and the page follows this card again
|
|
1231
|
-
s.status = 'ready';
|
|
1232
|
-
s.unstarted = (r && r.message) || 'that did not start. tap do it to try again';
|
|
1233
|
-
bet.drawn = s.id;
|
|
1234
|
-
drawBetCard(s);
|
|
1235
|
-
refresh();
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
loadBet(); refresh();
|
|
1239
|
-
setTimeout(function () { tapOrb(false); }, AFTER_MS);
|
|
1240
|
-
});
|
|
1241
|
-
});
|
|
1242
|
-
act.appendChild(go);
|
|
1243
|
-
if (s.unstarted) act.appendChild(el('p', 'links unsent', s.unstarted));
|
|
1244
|
-
var links = el('div', 'links');
|
|
1245
|
-
var sharp = el('button', null, 'sharper idea');
|
|
1246
|
-
sharp.type = 'button';
|
|
1247
|
-
sharp.addEventListener('click', function () { tapOrb(true); });
|
|
1248
|
-
var skip = el('button', null, 'not now');
|
|
1249
|
-
skip.type = 'button';
|
|
1250
|
-
skip.addEventListener('click', function () {
|
|
1251
|
-
buzz();
|
|
1252
|
-
api('/bet/skip', {}).then(function () {
|
|
1253
|
-
dissolve(function () { bet.status = 'skipped'; line.set('not now. tap when you want another', false); });
|
|
1254
|
-
});
|
|
1255
|
-
});
|
|
1256
|
-
links.appendChild(sharp);
|
|
1257
|
-
links.appendChild(document.createTextNode(' · '));
|
|
1258
|
-
links.appendChild(skip);
|
|
1259
|
-
act.appendChild(links);
|
|
1260
|
-
box.appendChild(act);
|
|
1261
|
-
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;
|
|
1262
1021
|
}
|
|
1263
1022
|
|
|
1023
|
+
// ---- showing a card ---------------------------------------------------------------------
|
|
1264
1024
|
// the time from the tap to the card fully on screen, kept on the page so
|
|
1265
1025
|
// the speed can be measured
|
|
1266
1026
|
function measured() {
|
|
@@ -1268,544 +1028,261 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1268
1028
|
bet.tapAt = 0;
|
|
1269
1029
|
}
|
|
1270
1030
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
bet.
|
|
1275
|
-
|
|
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
|
|
1276
1042
|
document.body.classList.add('has-card');
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
if (!animate) {
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
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'); });
|
|
1286
1051
|
}
|
|
1287
1052
|
|
|
1288
1053
|
// where the sphere sits once the card is gone, read before anything moves,
|
|
1289
1054
|
// in page coordinates (the screen's once the page is back at the top)
|
|
1290
1055
|
function sphereSpot() {
|
|
1291
|
-
var
|
|
1292
|
-
var y0 = window.scrollY, was =
|
|
1293
|
-
document.body.classList.remove('has-card');
|
|
1294
|
-
|
|
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;
|
|
1295
1060
|
var r = document.getElementById('orb').getBoundingClientRect();
|
|
1296
1061
|
var spot = { x: r.left + r.width / 2, y: r.top + window.scrollY + r.height / 2 };
|
|
1297
1062
|
document.body.classList.add('has-card');
|
|
1298
|
-
|
|
1063
|
+
if (wide) document.body.classList.add('wide');
|
|
1064
|
+
b.hidden = was;
|
|
1299
1065
|
if (window.scrollY !== y0) window.scrollTo(0, y0);
|
|
1300
1066
|
return spot;
|
|
1301
1067
|
}
|
|
1302
1068
|
|
|
1303
|
-
//
|
|
1304
|
-
// the
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
var to = sphereSpot();
|
|
1308
|
-
box.classList.remove('quick', 'up');
|
|
1309
|
-
box.classList.add('leave');
|
|
1310
|
-
var swap = function () {
|
|
1311
|
-
box.style.minHeight = '';
|
|
1312
|
-
box.hidden = true;
|
|
1313
|
-
box.classList.remove('leave');
|
|
1314
|
-
document.body.classList.remove('has-card');
|
|
1315
|
-
window.scrollTo(0, 0);
|
|
1316
|
-
};
|
|
1317
|
-
if (reduced) { setTimeout(function () { orb.melt(then, to, swap); }, orb.leaveMs); return; }
|
|
1318
|
-
orb.melt(then, to, swap);
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
// what the page does with the server's answer once the sphere is ready.
|
|
1322
|
-
// Every tap comes back with a card, so the orb plays straight into it.
|
|
1323
|
-
function reveal(s, fast) {
|
|
1324
|
-
bet.status = s.status;
|
|
1325
|
-
if ((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
1326
|
-
line.set('', false);
|
|
1327
|
-
orb.play(function (phase) { if (phase === 'settled') { bet.busy = false; showCard(s, true, fast); } }, fast);
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
bet.busy = false;
|
|
1331
|
-
orb.relax();
|
|
1332
|
-
line.set('tap to think', false);
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
function isCard(s) {
|
|
1336
|
-
return Boolean((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting'));
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// Card to card: a crossfade in place. The small orb stays where it is.
|
|
1340
|
-
// A still copy of the old card fades over the box while the new card,
|
|
1341
|
-
// drawn under it, fades in from DRIFT below over TURN_IN_MS, starting
|
|
1342
|
-
// 80ms in, so its title lands where the old title was. The box eases to
|
|
1343
|
-
// 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.
|
|
1344
1073
|
var TURN_IN_MS = 320, DRIFT = 4;
|
|
1345
|
-
function
|
|
1346
|
-
|
|
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(); });
|
|
1347
1079
|
}
|
|
1348
1080
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
var
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
// one card turns into the next in its own spot: a still copy of the old
|
|
1357
|
-
// one fades over it while the new one drifts in, and the box eases to
|
|
1358
|
-
// the new height. after() runs once it is at rest.
|
|
1359
|
-
function turnInPlace(box, draw, after) {
|
|
1360
|
-
if (reduced) { box.style.minHeight = ''; draw(); after(); return; }
|
|
1361
|
-
var from = box.offsetHeight;
|
|
1362
|
-
box.style.height = from + 'px';
|
|
1363
|
-
box.style.minHeight = '';
|
|
1364
|
-
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);
|
|
1365
1087
|
ghost.removeAttribute('id');
|
|
1366
|
-
ghost.classList.remove('quick', 'turning', 'turn-in');
|
|
1088
|
+
ghost.classList.remove('quick', 'acted', 'turning', 'turn-in');
|
|
1367
1089
|
ghost.classList.add('up', 'ghost');
|
|
1368
1090
|
ghost.setAttribute('aria-hidden', 'true');
|
|
1369
1091
|
ghost.inert = true;
|
|
1370
|
-
var r =
|
|
1092
|
+
var r = b.getBoundingClientRect(), p = b.offsetParent.getBoundingClientRect();
|
|
1371
1093
|
ghost.style.top = (r.top - p.top) + 'px';
|
|
1372
1094
|
ghost.style.left = (r.left - p.left) + 'px';
|
|
1373
1095
|
ghost.style.width = r.width + 'px';
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
var to =
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
void
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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';
|
|
1385
1107
|
ghost.classList.add('gone');
|
|
1386
1108
|
setTimeout(function () {
|
|
1387
1109
|
ghost.remove();
|
|
1388
|
-
|
|
1389
|
-
|
|
1110
|
+
b.classList.remove('turning');
|
|
1111
|
+
b.style.height = '';
|
|
1390
1112
|
after();
|
|
1391
1113
|
}, 80 + TURN_IN_MS + 20);
|
|
1392
1114
|
}
|
|
1393
1115
|
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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);
|
|
1400
1143
|
}
|
|
1401
1144
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
//
|
|
1405
|
-
|
|
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();
|
|
1406
1153
|
bet.busy = true;
|
|
1407
|
-
bet.
|
|
1408
|
-
bet.tapAt = heldAt || performance.now();
|
|
1154
|
+
bet.tapAt = performance.now();
|
|
1409
1155
|
document.body.removeAttribute('data-reveal-ms');
|
|
1410
|
-
var onCard = document.body.classList.contains('has-card');
|
|
1411
1156
|
var failed = function () {
|
|
1412
1157
|
bet.busy = false;
|
|
1413
|
-
document.getElementById('bet').classList.remove('holding');
|
|
1414
1158
|
orb.relax();
|
|
1415
1159
|
line.set('that did not go through. tap to try again', true);
|
|
1416
1160
|
};
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
bet.
|
|
1421
|
-
if (onCard && isCard(s)) { turn(s); return; }
|
|
1422
|
-
if (onCard) { dissolve(function () { reveal(s, true); }); return; }
|
|
1423
|
-
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);
|
|
1424
1165
|
}, failed);
|
|
1425
1166
|
}
|
|
1426
1167
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
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(); }
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function poll() {
|
|
1436
|
-
if (bet.polling) return;
|
|
1437
|
-
bet.polling = true;
|
|
1438
|
-
setTimeout(function () { bet.polling = false; loadBet(); }, 400);
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// first look: a card already on screen shows settled, anything else is the sphere
|
|
1442
|
-
function firstLook() {
|
|
1443
|
-
api('/bet').then(function (s) {
|
|
1444
|
-
if (s.http !== 200) return;
|
|
1445
|
-
bet.status = s.status;
|
|
1446
|
-
if ((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting')) {
|
|
1447
|
-
orb.settled();
|
|
1448
|
-
showCard(s, false);
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
line.set(s.status === 'skipped' ? 'not now. tap when you want another' : 'tap to think', false);
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// The small orb on a card goes back to the big dotted orb, the same calm
|
|
1456
|
-
// way the page goes home before a new idea: the card fades, the small orb
|
|
1457
|
-
// glides down and the dots form again, idle under the pointer. The card
|
|
1458
|
-
// 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.
|
|
1459
1172
|
function goHome() {
|
|
1460
1173
|
if (bet.busy) return;
|
|
1461
1174
|
buzz();
|
|
1462
1175
|
bet.busy = true;
|
|
1463
|
-
|
|
1464
|
-
|
|
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; });
|
|
1465
1180
|
}
|
|
1466
1181
|
|
|
1467
|
-
// The big orb: the next idea, with the spiral, as on page open. An idea
|
|
1468
|
-
// that landed behind the calm line is the next one, so it is never passed.
|
|
1469
|
-
function tapBig() { tapOrb(false); }
|
|
1470
|
-
|
|
1471
1182
|
function tapSmallOrb() {
|
|
1472
|
-
if (document.body.classList.contains('has-card')) goHome(); else
|
|
1183
|
+
if (document.body.classList.contains('has-card')) goHome(); else think();
|
|
1473
1184
|
}
|
|
1474
1185
|
|
|
1475
1186
|
var canvas = document.getElementById('orb');
|
|
1476
1187
|
var lavaTap = document.getElementById('lava');
|
|
1477
|
-
[[canvas,
|
|
1188
|
+
[[canvas, think], [lavaTap, tapSmallOrb]].forEach(function (pair) {
|
|
1478
1189
|
pair[0].addEventListener('click', pair[1]);
|
|
1479
1190
|
pair[0].addEventListener('keydown', function (e) {
|
|
1480
1191
|
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pair[1](); }
|
|
1481
1192
|
});
|
|
1482
1193
|
});
|
|
1483
1194
|
|
|
1484
|
-
//
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
// every redraw of the team box makes a new view; an answer that comes
|
|
1492
|
-
// back for an older view is dropped (a closed box is hidden, and the next
|
|
1493
|
-
// open is a new view)
|
|
1494
|
-
var teamView = 0;
|
|
1495
|
-
|
|
1496
|
-
function closeTeam() { teamBox.hidden = true; }
|
|
1497
|
-
|
|
1498
|
-
function teamHead(title) {
|
|
1499
|
-
teamView += 1;
|
|
1500
|
-
teamInner.textContent = '';
|
|
1501
|
-
teamInner.className = 'inner';
|
|
1502
|
-
teamBack = closeTeam;
|
|
1503
|
-
var head = el('div', 'head');
|
|
1504
|
-
head.appendChild(el('h1', null, title));
|
|
1505
|
-
var close = el('button', 'quiet', 'close');
|
|
1506
|
-
close.type = 'button';
|
|
1507
|
-
close.addEventListener('click', closeTeam);
|
|
1508
|
-
head.appendChild(close);
|
|
1509
|
-
teamInner.appendChild(head);
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
function openTeam() {
|
|
1513
|
-
teamBox.hidden = false;
|
|
1514
|
-
teamHead('the team');
|
|
1515
|
-
var view = teamView;
|
|
1516
|
-
api('/bet/team').then(function (s) { if (s.http === 200 && view === teamView) drawTeam(s.rows || []); });
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
// one row per member, five columns; a tap anywhere on a row opens its talk
|
|
1520
|
-
// card, and the name is a button so the keyboard reaches it too
|
|
1521
|
-
var TEAM_COLUMNS = ['member', 'role', 'ovr', 'engine', 'status'];
|
|
1522
|
-
var TEAM_CELLS = ['who', 'role', 'ovr', 'eng', 'st'];
|
|
1523
|
-
|
|
1524
|
-
// note is the one plain line a swap just said, shown over the table
|
|
1525
|
-
function drawTeam(rows, note) {
|
|
1526
|
-
teamHead('the team');
|
|
1527
|
-
if (note) teamInner.appendChild(el('p', 'done', note));
|
|
1528
|
-
if (!rows.length) { teamInner.appendChild(el('p', 'why', 'nobody is on the team yet.')); return; }
|
|
1529
|
-
teamInner.className = 'inner wide';
|
|
1530
|
-
deckEntry(rows);
|
|
1531
|
-
var grid = el('div', 'grid');
|
|
1532
|
-
var table = el('table', 'roster');
|
|
1533
|
-
var head = el('tr');
|
|
1534
|
-
TEAM_COLUMNS.forEach(function (name, i) { head.appendChild(el('th', TEAM_CELLS[i], name)).scope = 'col'; });
|
|
1535
|
-
table.appendChild(el('thead')).appendChild(head);
|
|
1536
|
-
var body = table.appendChild(el('tbody'));
|
|
1537
|
-
rows.forEach(function (r) {
|
|
1538
|
-
var tr = el('tr', 'row');
|
|
1539
|
-
var who = el('td', 'who');
|
|
1540
|
-
var open = who.appendChild(el('button', null, r.member));
|
|
1541
|
-
open.type = 'button';
|
|
1542
|
-
open.setAttribute('aria-label', 'talk to ' + r.member);
|
|
1543
|
-
tr.appendChild(who);
|
|
1544
|
-
tr.appendChild(el('td', 'role', r.role));
|
|
1545
|
-
tr.appendChild(el('td', 'ovr', r.ovr));
|
|
1546
|
-
// the engine cell is its own button: a tap opens the engine card, not the talk card
|
|
1547
|
-
var eng = tr.appendChild(el('td', 'eng'));
|
|
1548
|
-
if (r.engine === 'person') eng.textContent = r.engine;
|
|
1549
|
-
else {
|
|
1550
|
-
var swap = eng.appendChild(el('button', null, r.engine));
|
|
1551
|
-
swap.type = 'button';
|
|
1552
|
-
swap.setAttribute('aria-label', 'change what ' + r.member + ' runs on');
|
|
1553
|
-
swap.addEventListener('click', function (e) { e.stopPropagation(); drawEngine(r); });
|
|
1554
|
-
}
|
|
1555
|
-
tr.appendChild(el('td', 'st ' + r.status.replace(/ /g, '-'), r.sentence));
|
|
1556
|
-
tr.addEventListener('click', function () { drawTalk(r); });
|
|
1557
|
-
body.appendChild(tr);
|
|
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);
|
|
1558
1201
|
});
|
|
1559
|
-
grid.appendChild(table);
|
|
1560
|
-
teamInner.appendChild(grid);
|
|
1561
1202
|
}
|
|
1562
1203
|
|
|
1563
|
-
// the
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
var back = el('button', 'quiet', 'back to the team');
|
|
1577
|
-
back.type = 'button';
|
|
1578
|
-
back.addEventListener('click', openTeam);
|
|
1579
|
-
var busy = false;
|
|
1580
|
-
api('/bet/engine', { member: member }).then(function (s) {
|
|
1581
|
-
if (!mine(s)) return;
|
|
1582
|
-
if (s.http !== 200 || !s.ok) { said.textContent = s.message || 'could not read the models on this machine.'; teamInner.appendChild(back); return; }
|
|
1583
|
-
said.textContent = s.line;
|
|
1584
|
-
var scope = 'member';
|
|
1585
|
-
var scopes = el('div', 'scopes');
|
|
1586
|
-
scopes.setAttribute('role', 'group');
|
|
1587
|
-
scopes.setAttribute('aria-label', 'who the swap is for');
|
|
1588
|
-
(s.scopes || []).forEach(function (sc) {
|
|
1589
|
-
var b = scopes.appendChild(el('button', sc.id === scope ? 'chosen' : null, sc.label));
|
|
1590
|
-
b.type = 'button';
|
|
1591
|
-
b.setAttribute('aria-pressed', sc.id === scope ? 'true' : 'false');
|
|
1592
|
-
b.addEventListener('click', function () {
|
|
1593
|
-
scope = sc.id;
|
|
1594
|
-
Array.prototype.forEach.call(scopes.children, function (x) { var on = x === b; x.classList.toggle('chosen', on); x.setAttribute('aria-pressed', on ? 'true' : 'false'); });
|
|
1595
|
-
});
|
|
1596
|
-
});
|
|
1597
|
-
if ((s.scopes || []).length > 1) teamInner.appendChild(scopes);
|
|
1598
|
-
(s.groups || []).forEach(function (g) {
|
|
1599
|
-
var box = teamInner.appendChild(el('section', 'models'));
|
|
1600
|
-
box.appendChild(el('h2', null, g.tool));
|
|
1601
|
-
g.models.forEach(function (m) {
|
|
1602
|
-
var b = box.appendChild(el('button', 'choice' + (m.current ? ' chosen' : '')));
|
|
1603
|
-
b.type = 'button';
|
|
1604
|
-
b.appendChild(el('span', 't', m.label));
|
|
1605
|
-
var w = m.current ? 'now' : m.record;
|
|
1606
|
-
if (w) b.appendChild(el('span', 'w', w));
|
|
1607
|
-
if (m.current) { b.disabled = true; return; }
|
|
1608
|
-
b.setAttribute('data-member', member);
|
|
1609
|
-
b.addEventListener('click', function () {
|
|
1610
|
-
if (busy || !mine()) return;
|
|
1611
|
-
busy = true;
|
|
1612
|
-
b.classList.add('chosen');
|
|
1613
|
-
api('/bet/swap', { member: b.getAttribute('data-member'), engine: m.engine, model: m.model, scope: scope }).then(function (out) {
|
|
1614
|
-
busy = false;
|
|
1615
|
-
if (!mine()) return;
|
|
1616
|
-
if (out.http === 200 && out.ok) { drawTeam(out.rows || [], out.ack); return; }
|
|
1617
|
-
b.classList.remove('chosen');
|
|
1618
|
-
said.textContent = out.message || 'that did not change. try again.';
|
|
1619
|
-
}, function () { busy = false; if (!mine()) return; b.classList.remove('chosen'); said.textContent = 'that did not reach the mac. try again.'; });
|
|
1620
|
-
});
|
|
1621
|
-
});
|
|
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);
|
|
1622
1217
|
});
|
|
1623
|
-
|
|
1624
|
-
}, function () { if (!mine()) return; said.textContent = 'that did not reach the mac. try again.'; teamInner.appendChild(back); });
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
// a talk card: what the member is on, and a word to steer them
|
|
1628
|
-
function drawTalk(r) {
|
|
1629
|
-
teamHead(r.member);
|
|
1630
|
-
teamBack = openTeam;
|
|
1631
|
-
teamInner.appendChild(el('p', 'why', r.sentence + '.'));
|
|
1632
|
-
var talkBox = convo(function (body) { body.member = r.member; return api('/bet/steer', body); }, function (ack) {
|
|
1633
|
-
talkBox.replaceWith(el('p', 'mission', ack));
|
|
1634
|
-
}, 'tell ' + r.member + ' something');
|
|
1635
|
-
var back = el('button', 'quiet', 'back to the team');
|
|
1636
|
-
back.type = 'button';
|
|
1637
|
-
back.addEventListener('click', openTeam);
|
|
1638
|
-
teamInner.appendChild(talkBox);
|
|
1639
|
-
teamInner.appendChild(back);
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
// ---- the job deck: every running job is one card, the ones that need
|
|
1643
|
-
// the owner first. The arrows or a sideways swipe flip one card at a
|
|
1644
|
-
// time, and it turns in its own spot the way the main card does. Esc
|
|
1645
|
-
// goes back to talking. A card that needs the owner is that job's yes
|
|
1646
|
-
// card, and under the table one box talks to the job's member and keeps
|
|
1647
|
-
// the turns, even across flips.
|
|
1648
|
-
var deckCards = [];
|
|
1649
|
-
var deckColumns = [];
|
|
1650
|
-
var deckAt = 0;
|
|
1651
|
-
var deckTurning = false;
|
|
1652
|
-
var deckTalk = {};
|
|
1653
|
-
var DECK_LIVE = ['waiting for your yes', 'working', 'waiting to start'];
|
|
1654
|
-
|
|
1655
|
-
// the way in from the team page, there only while a job is running
|
|
1656
|
-
function deckEntry(rows) {
|
|
1657
|
-
if (!rows.some(function (r) { return DECK_LIVE.indexOf(r.status) >= 0; })) return;
|
|
1658
|
-
var open = el('button', 'quiet', 'the jobs, one at a time');
|
|
1659
|
-
open.type = 'button';
|
|
1660
|
-
open.addEventListener('click', openDeck);
|
|
1661
|
-
teamInner.appendChild(open);
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
function openDeck() {
|
|
1665
|
-
teamBox.hidden = false;
|
|
1666
|
-
teamHead('the jobs');
|
|
1667
|
-
var waiting = teamInner.appendChild(el('div', 'deckspot'));
|
|
1668
|
-
api('/bet/jobs').then(function (s) {
|
|
1669
|
-
// the owner moved on while it loaded: nothing is drawn over it
|
|
1670
|
-
if (s.http !== 200 || !waiting.isConnected || teamBox.hidden) return;
|
|
1671
|
-
deckCards = s.cards || [];
|
|
1672
|
-
deckColumns = s.columns || [];
|
|
1673
|
-
if (!deckCards.length) { openTeam(); return; }
|
|
1674
|
-
deckAt = Math.min(deckAt, deckCards.length - 1);
|
|
1675
|
-
drawDeck();
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
function drawDeck() {
|
|
1680
|
-
teamHead('the jobs');
|
|
1681
|
-
teamInner.className = 'inner wide';
|
|
1682
|
-
var spot = teamInner.appendChild(el('div', 'deckspot'));
|
|
1683
|
-
// a card like the main one, so it turns the same way
|
|
1684
|
-
var card = spot.appendChild(el('article', 'bet'));
|
|
1685
|
-
card.classList.add('up', 'deckcard');
|
|
1686
|
-
fillDeckCard(card);
|
|
1687
|
-
var startX = null, startY = null;
|
|
1688
|
-
card.addEventListener('touchstart', function (e) { startX = e.touches[0].clientX; startY = e.touches[0].clientY; }, { passive: true });
|
|
1689
|
-
card.addEventListener('touchend', function (e) {
|
|
1690
|
-
if (startX === null) return;
|
|
1691
|
-
var dx = e.changedTouches[0].clientX - startX, dy = e.changedTouches[0].clientY - startY;
|
|
1692
|
-
startX = null;
|
|
1693
|
-
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) flipDeck(dx < 0 ? 1 : -1);
|
|
1694
|
-
});
|
|
1695
|
-
var bar = el('div', 'deckbar');
|
|
1696
|
-
if (deckCards.length > 1) {
|
|
1697
|
-
var prev = bar.appendChild(el('button', 'quiet', 'previous'));
|
|
1698
|
-
prev.type = 'button';
|
|
1699
|
-
prev.addEventListener('click', function () { flipDeck(-1); });
|
|
1700
|
-
}
|
|
1701
|
-
var whole = bar.appendChild(el('button', 'quiet', 'the whole team'));
|
|
1702
|
-
whole.type = 'button';
|
|
1703
|
-
whole.addEventListener('click', openTeam);
|
|
1704
|
-
if (deckCards.length > 1) {
|
|
1705
|
-
var next = bar.appendChild(el('button', 'quiet', 'next'));
|
|
1706
|
-
next.type = 'button';
|
|
1707
|
-
next.addEventListener('click', function () { flipDeck(1); });
|
|
1218
|
+
return;
|
|
1708
1219
|
}
|
|
1709
|
-
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
// one job: its place in the deck, the yes card when it needs the owner,
|
|
1713
|
-
// the divided table, how long, and the talk box for its member
|
|
1714
|
-
function fillDeckCard(card) {
|
|
1715
|
-
var c = deckCards[deckAt];
|
|
1716
|
-
card.textContent = '';
|
|
1717
|
-
card.appendChild(el('span', 'count', 'job ' + (deckAt + 1) + ' of ' + deckCards.length));
|
|
1718
|
-
card.appendChild(el('span', 'label ' + c.label.replace(/ /g, '-'), c.label));
|
|
1719
|
-
if (c.yes) {
|
|
1720
|
-
card.appendChild(el('h1', null, c.yes.what));
|
|
1721
|
-
if (c.yes.why) card.appendChild(el('p', 'why', c.yes.why));
|
|
1722
|
-
var act = card.appendChild(el('div', 'act'));
|
|
1723
|
-
act.appendChild(yesPill(function () { return api('/bet/yes', { confirm: true, key: c.yes.key, job: c.job_id }); }, function (said, r) {
|
|
1724
|
-
if (r.ok || r.reason === 'gone') { c.yes = null; c.said = said; act.replaceWith(el('p', 'mission', said)); return; }
|
|
1725
|
-
c.unsent = said;
|
|
1726
|
-
fillDeckCard(card);
|
|
1727
|
-
}));
|
|
1728
|
-
if (c.unsent) act.appendChild(el('p', 'links unsent', c.unsent));
|
|
1729
|
-
} else if (c.said) card.appendChild(el('p', 'mission', c.said));
|
|
1730
|
-
var table = el('table', 'jobs');
|
|
1731
|
-
var head = el('tr');
|
|
1732
|
-
deckColumns.forEach(function (k) { head.appendChild(el('th', k, k)).scope = 'col'; });
|
|
1733
|
-
table.appendChild(el('thead')).appendChild(head);
|
|
1734
|
-
var body = table.appendChild(el('tbody'));
|
|
1735
|
-
c.rows.forEach(function (r) {
|
|
1736
|
-
var tr = body.appendChild(el('tr'));
|
|
1737
|
-
deckColumns.forEach(function (k) { tr.appendChild(el('td', k, r[k] || '')); });
|
|
1738
|
-
});
|
|
1739
|
-
card.appendChild(el('div', 'jobs-wrap')).appendChild(table);
|
|
1740
|
-
if (c.time) card.appendChild(el('span', 'time', c.time));
|
|
1741
|
-
card.appendChild(deckTalk[c.member] || (deckTalk[c.member] = talkBox(c.member)));
|
|
1220
|
+
flow(answer, bet.shown, null, null);
|
|
1742
1221
|
}
|
|
1222
|
+
document.getElementById('working').addEventListener('click', function () { openFrom('team'); });
|
|
1223
|
+
document.getElementById('needs').addEventListener('click', function () { openFrom('deck'); });
|
|
1743
1224
|
|
|
1744
|
-
//
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
var holder = el('div', 'jobtalk');
|
|
1749
|
-
var fresh = function () {
|
|
1750
|
-
var box = convo(function (body) { body.member = member; return api('/bet/steer', body); }, function (ack) {
|
|
1751
|
-
holder.insertBefore(el('p', 'mission', ack), box);
|
|
1752
|
-
box.replaceWith(fresh());
|
|
1753
|
-
}, 'talk to this job');
|
|
1754
|
-
return box;
|
|
1755
|
-
};
|
|
1756
|
-
holder.appendChild(fresh());
|
|
1757
|
-
return holder;
|
|
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;
|
|
1758
1229
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
if (!card || deckTurning || deckCards.length < 2) return;
|
|
1763
|
-
deckAt = (deckAt + step + deckCards.length) % deckCards.length;
|
|
1764
|
-
deckTurning = true;
|
|
1765
|
-
turnInPlace(card, function () { fillDeckCard(card); }, function () { deckTurning = false; });
|
|
1230
|
+
function useOption(o) {
|
|
1231
|
+
var b = o && box().querySelector('[data-option="' + o.id + '"]');
|
|
1232
|
+
if (b && !bet.busy) b.click();
|
|
1766
1233
|
}
|
|
1767
1234
|
|
|
1768
|
-
//
|
|
1769
|
-
|
|
1770
|
-
var step = e.key === 'ArrowLeft' ? -1 : e.key === 'ArrowRight' ? 1 : 0;
|
|
1771
|
-
if (!step || e.isComposing || teamBox.hidden || !teamInner.querySelector('.deckcard')) return;
|
|
1772
|
-
var field = document.activeElement;
|
|
1773
|
-
if (field && (field.tagName === 'INPUT' || field.tagName === 'TEXTAREA')) return;
|
|
1774
|
-
e.preventDefault();
|
|
1775
|
-
flipDeck(step);
|
|
1776
|
-
});
|
|
1777
|
-
|
|
1778
|
-
document.getElementById('working').addEventListener('click', openTeam);
|
|
1779
|
-
|
|
1780
|
-
// Esc on the team page steps back one layer, the way its own buttons do:
|
|
1781
|
-
// words being typed are cancelled and the cursor leaves first, then the
|
|
1782
|
-
// talk card goes back to the team, then the team closes to the card.
|
|
1783
|
-
// On the card itself it does nothing. It listens on the way down, so the
|
|
1784
|
-
// 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.
|
|
1785
1237
|
document.addEventListener('keydown', function (e) {
|
|
1786
|
-
if (e.key !== 'Escape' || e.isComposing ||
|
|
1787
|
-
e.preventDefault();
|
|
1238
|
+
if (e.key !== 'Escape' || e.isComposing || !bet.shown || !document.body.classList.contains('has-card')) return;
|
|
1788
1239
|
var field = document.activeElement;
|
|
1789
|
-
if (field &&
|
|
1240
|
+
if (field && field.tagName === 'INPUT') {
|
|
1241
|
+
e.preventDefault();
|
|
1790
1242
|
field.value = '';
|
|
1791
|
-
// the field's own input listener drops a second tap armed for those words
|
|
1792
|
-
field.dispatchEvent(new Event('input'));
|
|
1793
1243
|
field.blur();
|
|
1794
1244
|
return;
|
|
1795
1245
|
}
|
|
1796
|
-
|
|
1246
|
+
var back = optionFor('back');
|
|
1247
|
+
if (back) { e.preventDefault(); useOption(back); }
|
|
1797
1248
|
}, true);
|
|
1798
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
|
+
|
|
1799
1269
|
// the footer: how many members are really working, or the team when none are
|
|
1800
1270
|
function drawWorking(n) {
|
|
1801
1271
|
var btn = document.getElementById('working');
|
|
1802
1272
|
btn.textContent = n ? n + ' working' : 'the team';
|
|
1803
1273
|
}
|
|
1804
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.
|
|
1805
1278
|
function refresh() {
|
|
1806
|
-
return api('/
|
|
1279
|
+
return api('/card').then(function (s) {
|
|
1807
1280
|
if (s.http !== 200) { line.set('this link stopped working. start gm phone again on the mac.', true); return; }
|
|
1808
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
|
+
}
|
|
1809
1286
|
}).catch(function () { /* the next refresh tries again */ });
|
|
1810
1287
|
}
|
|
1811
1288
|
|