atris 3.73.0 → 3.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/member.js +141 -7
- package/commands/mission.js +156 -65
- package/commands/slop.js +8 -2
- package/lib/falsifier-probe.js +2 -1
- package/lib/gm-bet.js +84 -25
- package/lib/gm-phone-page.js +386 -98
- package/lib/gm-phone.js +27 -3
- package/lib/handoff-wake.js +180 -0
- package/lib/member-up.js +18 -3
- package/lib/mission-protected-lane.js +38 -6
- package/lib/spawn-env.js +92 -3
- package/lib/task-proof.js +2 -1
- package/lib/task-receipt.js +3 -2
- package/lib/yes-lock.js +633 -81
- package/lib/zen-fleet.js +2 -2
- package/lib/zen-workspace.js +6 -1
- package/lib/zen.js +9 -0
- package/package.json +2 -1
package/lib/gm-phone-page.js
CHANGED
|
@@ -86,6 +86,8 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
|
86
86
|
.team .time { grid-column: 1 / -1; font-size: 17px; color: var(--text-2); }
|
|
87
87
|
.sayline { margin: 10px 0 0; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
88
88
|
.sayline .thread, .sayline .reply { width: 100%; text-align: left; margin: 0; }
|
|
89
|
+
.unsent { margin: 0; text-align: center; }
|
|
90
|
+
.act > .links:empty { display: none; }
|
|
89
91
|
.turn { margin: 0 0 6px; font-size: 18px; line-height: 1.4; }
|
|
90
92
|
.turn.you { color: var(--text-2); }
|
|
91
93
|
.turn.orb { color: var(--text); }
|
|
@@ -127,12 +129,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
127
129
|
.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); }
|
|
128
130
|
.bet.up { opacity: 1; transform: none; }
|
|
129
131
|
.bet.quick { transition-duration: 0.4s, 0.5s; }
|
|
130
|
-
/* card to card:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
.bet.
|
|
135
|
-
.bet.
|
|
132
|
+
/* card to card: a crossfade in place. the old card is a still copy that
|
|
133
|
+
fades over the box while the new card fades in under it from a small
|
|
134
|
+
drift, its title landing where the old title was; the box eases to the
|
|
135
|
+
new height */
|
|
136
|
+
.bet.ghost { position: absolute; z-index: 1; margin: 0; pointer-events: none; transition: opacity 0.2s ease-out; }
|
|
137
|
+
.bet.ghost.gone { opacity: 0; }
|
|
138
|
+
.bet.turning { overflow: hidden; transition: opacity 0.32s ease-out 0.08s, transform 0.32s cubic-bezier(0.22, 1, 0.36, 1) 0.08s, height 0.36s cubic-bezier(0.4, 0, 0.2, 1); }
|
|
139
|
+
.bet.turn-in { opacity: 0; transform: translateY(4px); transition: none; }
|
|
136
140
|
/* leaving: a soft fade in place, never a cut */
|
|
137
141
|
.bet.leave { opacity: 0; transform: none; transition: opacity 0.4s ease-out; }
|
|
138
142
|
.bet h1 { margin: 0; font-size: clamp(23px, 6.2vw, 30px); line-height: 1.12; font-weight: 700; letter-spacing: -0.03em; text-align: left; }
|
|
@@ -159,6 +163,20 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
159
163
|
/* an answer to a question card */
|
|
160
164
|
.choice { width: 100%; min-height: 52px; text-align: left; padding: 12px 16px; border-radius: 14px; border: 1px solid var(--line); color: var(--text); font-size: 19px; }
|
|
161
165
|
|
|
166
|
+
/* typing in place: the tapped answer becomes this box at its own size and
|
|
167
|
+
spot, the cursor in it, and the rest of the card dims and stays put */
|
|
168
|
+
.field { position: relative; display: block; width: 100%; padding: 0; border: 1px solid var(--line); border-radius: 14px; color: var(--text); }
|
|
169
|
+
.field input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0 76px 0 16px; border: 0; border-radius: 14px; background: transparent; color: var(--text); font: inherit; outline: none; }
|
|
170
|
+
.field input::placeholder { color: var(--text-2); opacity: 1; }
|
|
171
|
+
.field:focus-within { border-color: var(--text-2); }
|
|
172
|
+
.field .send { position: absolute; top: 0; right: 0; bottom: 0; min-width: 64px; padding: 0 14px; color: var(--text-2); font-size: 17px; text-decoration: none; }
|
|
173
|
+
.sayline .field input { padding: 0 76px; text-align: center; }
|
|
174
|
+
/* a chosen answer holds its spot with an amber edge, and the card takes no taps */
|
|
175
|
+
.choice.chosen, .option.chosen, .field.chosen { border-color: var(--amber); background: rgba(255, 140, 66, 0.1); }
|
|
176
|
+
.chosen .send { visibility: hidden; }
|
|
177
|
+
.bet.holding { pointer-events: none; }
|
|
178
|
+
.muted { opacity: 0.35; transition: opacity 0.2s ease; }
|
|
179
|
+
|
|
162
180
|
@media (prefers-reduced-motion: reduce) {
|
|
163
181
|
.lava .tx, .lava .ty, .lava .blob { animation: none !important; }
|
|
164
182
|
.bet { transform: none; transition: opacity 0.2s linear; }
|
|
@@ -175,7 +193,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
175
193
|
<article class="bet" id="bet" hidden></article>
|
|
176
194
|
<div class="foot" id="foot"><button type="button" class="quiet" id="working" aria-haspopup="dialog">the team</button></div>
|
|
177
195
|
</main>
|
|
178
|
-
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap
|
|
196
|
+
<div class="lava" id="lava" role="button" tabindex="-1" aria-label="the atris orb. tap to go back to the big orb">
|
|
179
197
|
<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>
|
|
180
198
|
<div class="tx" style-vars="18%,12.6s,-7.2s"><div class="ty" style-vars="15%,7.2s,-4.7s"><div class="blob b2 rev" spin-vars="48s,45deg"></div></div></div>
|
|
181
199
|
<div class="tx" style-vars="12%,7.2s,-6.4s"><div class="ty" style-vars="18%,10.1s,-9.9s"><div class="blob b3" spin-vars="57.6s,90deg"></div></div></div>
|
|
@@ -624,7 +642,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
624
642
|
})();
|
|
625
643
|
|
|
626
644
|
// ---- the bet ------------------------------------------------------------------
|
|
627
|
-
var bet = { status: null, polling: false, armed: false, drawn: null, busy: false, last: null };
|
|
645
|
+
var bet = { status: null, polling: false, armed: false, drawn: null, busy: false, last: null, watching: null };
|
|
628
646
|
// how long "builder has it" (or any ack) stays before the next card
|
|
629
647
|
var AFTER_MS = 1500;
|
|
630
648
|
|
|
@@ -668,7 +686,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
668
686
|
box.textContent = '';
|
|
669
687
|
var j = s.job;
|
|
670
688
|
box.appendChild(el('h1', null, j.what));
|
|
671
|
-
box.appendChild(el('p', 'why', j.why));
|
|
689
|
+
if (j.why) box.appendChild(el('p', 'why', j.why));
|
|
672
690
|
var act = el('div', 'act');
|
|
673
691
|
if (s.kind === 'yes') {
|
|
674
692
|
var yes = el('button', 'pill', 'yes');
|
|
@@ -677,9 +695,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
677
695
|
api('/bet/yes', { confirm: true }).then(function (r) { moveOn(r.ack || r.message || 'sent'); });
|
|
678
696
|
});
|
|
679
697
|
act.appendChild(yes);
|
|
680
|
-
act.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
681
698
|
} else if (s.kind === 'wait') {
|
|
682
|
-
|
|
699
|
+
// one calm line: the idea turns in on its own once it is written
|
|
700
|
+
watchWait();
|
|
701
|
+
box.appendChild(sayBox());
|
|
702
|
+
return;
|
|
683
703
|
} else {
|
|
684
704
|
var ok = el('button', 'pill', 'nice');
|
|
685
705
|
ok.type = 'button';
|
|
@@ -693,6 +713,31 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
693
713
|
box.appendChild(sayBox());
|
|
694
714
|
}
|
|
695
715
|
|
|
716
|
+
// While the calm line shows, look every WAIT_LOOK_MS for the idea; once the
|
|
717
|
+
// server has one ready, it is asked for the way a tap does and turns in
|
|
718
|
+
// with one crossfade. No tap, and nothing moves on the page while it
|
|
719
|
+
// waits. It also waits while the owner types there (the box has the
|
|
720
|
+
// cursor or any words), so it comes in after they send, or once they
|
|
721
|
+
// clear the box and leave it; and on the big orb, it waits for their tap.
|
|
722
|
+
var WAIT_LOOK_MS = 1500;
|
|
723
|
+
function typingOnCard() {
|
|
724
|
+
var input = document.getElementById('bet').querySelector('.field input');
|
|
725
|
+
return Boolean(input && (input === document.activeElement || input.value.trim()));
|
|
726
|
+
}
|
|
727
|
+
function watchWait() {
|
|
728
|
+
if (bet.watching) return;
|
|
729
|
+
var quiet = function () { return !bet.busy && document.body.classList.contains('has-card') && !typingOnCard(); };
|
|
730
|
+
bet.watching = setInterval(function () {
|
|
731
|
+
if (!bet.last || bet.last.kind !== 'wait') { clearInterval(bet.watching); bet.watching = null; return; }
|
|
732
|
+
if (!quiet()) return;
|
|
733
|
+
api('/bet').then(function (s) {
|
|
734
|
+
if (s.http !== 200 || !quiet() || !bet.last || bet.last.kind !== 'wait') return;
|
|
735
|
+
if (s.id !== bet.last.id && s.kind !== 'wait' && isCard(s)) { bet.status = s.status; turn(s); return; }
|
|
736
|
+
if (s.kind === 'wait' && s.queue > 0) think(false, 0);
|
|
737
|
+
}).catch(function () { /* the next look tries again */ });
|
|
738
|
+
}, WAIT_LOOK_MS);
|
|
739
|
+
}
|
|
740
|
+
|
|
696
741
|
// no bet was ready: one question, then what is going on and why the answer matters
|
|
697
742
|
function drawQuestionCard(s) {
|
|
698
743
|
var box = document.getElementById('bet');
|
|
@@ -705,48 +750,207 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
705
750
|
var b = el('button', 'choice', c.label);
|
|
706
751
|
b.type = 'button';
|
|
707
752
|
b.addEventListener('click', function () {
|
|
753
|
+
if (bet.busy) return;
|
|
708
754
|
buzz();
|
|
709
|
-
if (c.other) {
|
|
710
|
-
|
|
711
|
-
api('/bet/answer', { choice: c.id }).then(function (r) {
|
|
712
|
-
if (r.ok) { moveOn(r.ack); return; }
|
|
713
|
-
act.replaceWith(el('p', 'mission', r.message || 'that did not go through'));
|
|
714
|
-
});
|
|
755
|
+
if (c.other) { typeHere(b, c.label, 60, function (text, field) { answer(field, { choice: 'other', text: text }); }); return; }
|
|
756
|
+
answer(b, { choice: c.id });
|
|
715
757
|
});
|
|
716
758
|
act.appendChild(b);
|
|
717
759
|
});
|
|
718
|
-
|
|
760
|
+
// a quiet line under the answers, empty until an answer does not go through
|
|
761
|
+
act.appendChild(el('div', 'links'));
|
|
719
762
|
box.appendChild(act);
|
|
720
763
|
box.appendChild(sayBox());
|
|
721
764
|
}
|
|
722
765
|
|
|
723
|
-
//
|
|
724
|
-
|
|
725
|
-
|
|
766
|
+
// ---- answering in place: the eyes stay in one spot ----------------------------
|
|
767
|
+
// A tapped answer, or the words typed in its place, holds that spot as
|
|
768
|
+
// chosen for HOLD_MS while the rest of the card dims and stays put. The
|
|
769
|
+
// next card is asked for during the hold and turns in once it is over.
|
|
770
|
+
var HOLD_MS = 400;
|
|
771
|
+
|
|
772
|
+
// a failed fetch reads like a refusal, so a hold never hangs
|
|
773
|
+
function offline() { return { ok: false, http: 0 }; }
|
|
774
|
+
|
|
775
|
+
// every answer row of the card dims but the one in use; no spot, none
|
|
776
|
+
function dimOthers(spot) {
|
|
777
|
+
var box = document.getElementById('bet');
|
|
778
|
+
box.querySelectorAll('.act > *, .options > *, .sayline, :scope > .links').forEach(function (n) {
|
|
779
|
+
n.classList.toggle('muted', Boolean(spot) && n !== spot && !n.contains(spot));
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// the spot holds as chosen and the card takes no taps; returns when it
|
|
784
|
+
// started, which is also where the time to the next card counts from
|
|
785
|
+
function hold(spot) {
|
|
786
|
+
var box = document.getElementById('bet');
|
|
787
|
+
dimOthers(spot);
|
|
788
|
+
spot.classList.add('chosen');
|
|
789
|
+
box.classList.add('holding');
|
|
790
|
+
var input = spot.querySelector('input');
|
|
791
|
+
if (input) { input.readOnly = true; input.blur(); }
|
|
792
|
+
bet.busy = true;
|
|
793
|
+
bet.armed = false;
|
|
794
|
+
bet.tapAt = performance.now();
|
|
795
|
+
document.body.removeAttribute('data-reveal-ms');
|
|
796
|
+
return bet.tapAt;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// it did not go through: the card is as it was
|
|
800
|
+
function unhold() {
|
|
801
|
+
var box = document.getElementById('bet');
|
|
802
|
+
box.classList.remove('holding');
|
|
803
|
+
dimOthers(null);
|
|
804
|
+
box.querySelectorAll('.chosen').forEach(function (n) { n.classList.remove('chosen'); });
|
|
805
|
+
bet.busy = false;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// resolves once the hold that started at heldAt has run HOLD_MS
|
|
809
|
+
function afterHold(heldAt) {
|
|
810
|
+
var left = heldAt ? heldAt + HOLD_MS - performance.now() : 0;
|
|
811
|
+
return new Promise(function (resolve) { setTimeout(resolve, Math.max(0, left)); });
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// The tapped button becomes the field at its own size and spot, with the
|
|
815
|
+
// cursor already in it, and the other answers dim. Enter or send hands
|
|
816
|
+
// the words to sent(text, field). Left empty, it turns back into the button.
|
|
817
|
+
function typeHere(btn, placeholder, maxLength, sent) {
|
|
818
|
+
var field = el('div', btn.classList.contains('choice') ? 'choice field' : 'field');
|
|
819
|
+
// its exact height, to the subpixel, so nothing under it moves
|
|
820
|
+
field.style.height = btn.getBoundingClientRect().height + 'px';
|
|
821
|
+
var input = el('input');
|
|
726
822
|
input.type = 'text';
|
|
727
|
-
input.maxLength =
|
|
728
|
-
input.
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
var
|
|
823
|
+
input.maxLength = maxLength;
|
|
824
|
+
input.placeholder = placeholder;
|
|
825
|
+
input.enterKeyHint = 'send';
|
|
826
|
+
input.setAttribute('aria-label', placeholder);
|
|
827
|
+
var send = el('button', 'send', 'send');
|
|
828
|
+
send.type = 'button';
|
|
829
|
+
var submit = function () {
|
|
732
830
|
var text = input.value.trim();
|
|
733
|
-
if (!text)
|
|
734
|
-
save.disabled = true;
|
|
735
|
-
api('/bet/answer', { choice: 'other', text: text }).then(function (r) {
|
|
736
|
-
if (r.ok) { moveOn(r.ack); return; }
|
|
737
|
-
save.disabled = false;
|
|
738
|
-
input.value = '';
|
|
739
|
-
input.placeholder = r.message || 'that did not go through';
|
|
740
|
-
});
|
|
831
|
+
if (text && !bet.busy) sent(text, field);
|
|
741
832
|
};
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
833
|
+
// a press on send keeps the cursor in the field, so it never closes first
|
|
834
|
+
send.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
|
835
|
+
send.addEventListener('click', function () { buzz(); submit(); });
|
|
836
|
+
input.addEventListener('keydown', function (e) {
|
|
837
|
+
if (e.key === 'Enter') { e.preventDefault(); submit(); }
|
|
838
|
+
if (e.key === 'Escape') { input.value = ''; input.blur(); }
|
|
839
|
+
});
|
|
840
|
+
input.addEventListener('blur', function () {
|
|
841
|
+
if (input.value.trim() || field.classList.contains('chosen') || !field.isConnected) return;
|
|
842
|
+
field.replaceWith(btn);
|
|
843
|
+
dimOthers(null);
|
|
844
|
+
});
|
|
845
|
+
field.appendChild(input);
|
|
846
|
+
field.appendChild(send);
|
|
847
|
+
btn.replaceWith(field);
|
|
848
|
+
dimOthers(field);
|
|
747
849
|
input.focus();
|
|
748
850
|
}
|
|
749
851
|
|
|
852
|
+
// An answer to a question holds as chosen while it saves. If it does not
|
|
853
|
+
// go through, the card comes back as it was, the words still in the
|
|
854
|
+
// field, and why takes the hint's line under the answers.
|
|
855
|
+
function answer(spot, body) {
|
|
856
|
+
body.card = shownCard();
|
|
857
|
+
var heldAt = hold(spot);
|
|
858
|
+
api('/bet/answer', body).catch(offline).then(function (r) {
|
|
859
|
+
if (r.ok) { think(false, heldAt); return; }
|
|
860
|
+
unhold();
|
|
861
|
+
var hint = document.getElementById('bet').querySelector('.act .links');
|
|
862
|
+
if (hint) hint.textContent = r.message || 'that did not go through';
|
|
863
|
+
var input = spot.querySelector('input');
|
|
864
|
+
if (input) { input.readOnly = false; dimOthers(spot); input.focus(); }
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// the card the owner is looking at, named with every answer and every
|
|
869
|
+
// word said, so the server acts on that card or on nothing
|
|
870
|
+
function shownCard() { return bet.last && bet.last.id ? bet.last.id : ''; }
|
|
871
|
+
|
|
872
|
+
// Words said on a card hold their spot as chosen while the server reads
|
|
873
|
+
// them. An answer or an instruction brings the next card; a question
|
|
874
|
+
// back or a second tap it needs turns in as the orb's reply, in the
|
|
875
|
+
// title's spot. If it did not send, the words stay where they were.
|
|
876
|
+
// One id per typed send: made when these words are first sent from this
|
|
877
|
+
// spot, the same on every retry of them, and new when the words change,
|
|
878
|
+
// so a send whose reply was lost is never sent twice. getRandomValues
|
|
879
|
+
// works on the plain http of the wifi link, where randomUUID does not.
|
|
880
|
+
function sendId(spot, text) {
|
|
881
|
+
if (spot.sendText !== text) {
|
|
882
|
+
var n = new Uint32Array(2);
|
|
883
|
+
crypto.getRandomValues(n);
|
|
884
|
+
spot.sendText = text;
|
|
885
|
+
spot.sendId = ('000000' + n[0].toString(36)).slice(-7) + ('000000' + n[1].toString(36)).slice(-7);
|
|
886
|
+
}
|
|
887
|
+
return spot.sendId;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function say(spot, body) {
|
|
891
|
+
body.card = shownCard();
|
|
892
|
+
body.send = sendId(spot, body.text);
|
|
893
|
+
var heldAt = hold(spot);
|
|
894
|
+
api('/bet/say', body).catch(offline).then(function (r) {
|
|
895
|
+
if (r.ok && !r.stay) { think(false, heldAt); return; }
|
|
896
|
+
if (!r.ok && r.reason !== 'needs_confirm') { unsent(spot, r.message || 'that did not send. tap send to try again'); return; }
|
|
897
|
+
afterHold(heldAt).then(function () { crossfade(function () { drawReply(r, body.text); }); });
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// It did not send: the card is as it was, the words still in the box with
|
|
902
|
+
// the cursor in it, and one short line under it says why, so Enter (or
|
|
903
|
+
// the same tap) tries again. Left empty, the box turns back into the
|
|
904
|
+
// button and the line goes with it.
|
|
905
|
+
function unsent(spot, why) {
|
|
906
|
+
unhold();
|
|
907
|
+
var note = spot.nextElementSibling;
|
|
908
|
+
var input = spot.querySelector('input');
|
|
909
|
+
if (!note || !note.classList.contains('unsent')) {
|
|
910
|
+
note = el('p', 'links unsent');
|
|
911
|
+
spot.after(note);
|
|
912
|
+
if (input) input.addEventListener('blur', function () { if (!spot.isConnected) note.remove(); });
|
|
913
|
+
}
|
|
914
|
+
note.textContent = why;
|
|
915
|
+
if (input) { input.readOnly = false; dimOthers(spot); input.focus(); }
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// one reply to tap: it holds as chosen and is said
|
|
919
|
+
function sayChoice(label, body) {
|
|
920
|
+
var b = el('button', 'choice', label);
|
|
921
|
+
b.type = 'button';
|
|
922
|
+
b.addEventListener('click', function () {
|
|
923
|
+
if (bet.busy) return;
|
|
924
|
+
buzz();
|
|
925
|
+
say(b, body);
|
|
926
|
+
});
|
|
927
|
+
return b;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// the orb's reply is the title, with the card's own answer right under it
|
|
931
|
+
// (do it, yes, nice, or the answers), then up to three suggested replies;
|
|
932
|
+
// starting a member who is not running takes one more tap, or it is kept
|
|
933
|
+
// as a note. The say line takes anything else.
|
|
934
|
+
function drawReply(r, said) {
|
|
935
|
+
var box = document.getElementById('bet');
|
|
936
|
+
// a reply never takes the card's own answer away: the card is drawn
|
|
937
|
+
// once more, and its row is kept, buttons and all
|
|
938
|
+
var own = null;
|
|
939
|
+
if (bet.last) { drawBetCard(bet.last); own = box.querySelector(':scope > .act, :scope > .options'); }
|
|
940
|
+
box.textContent = '';
|
|
941
|
+
bet.drawn = null;
|
|
942
|
+
box.appendChild(el('h1', null, r.stay ? r.reply : (r.message || 'that did not go through')));
|
|
943
|
+
if (own) box.appendChild(own);
|
|
944
|
+
var act = el('div', 'act answers');
|
|
945
|
+
if (r.stay) (r.suggestions || []).forEach(function (sug) { act.appendChild(sayChoice(sug, { text: sug })); });
|
|
946
|
+
if (r.reason === 'needs_confirm') {
|
|
947
|
+
act.appendChild(sayChoice('start it', { text: said, confirm: true }));
|
|
948
|
+
act.appendChild(sayChoice('keep it as a note instead', { text: said, note: true }));
|
|
949
|
+
}
|
|
950
|
+
box.appendChild(act);
|
|
951
|
+
box.appendChild(sayBox());
|
|
952
|
+
}
|
|
953
|
+
|
|
750
954
|
// the owner's own number, once a week: one box and one tap to save
|
|
751
955
|
function drawReportCard(s) {
|
|
752
956
|
var box = document.getElementById('bet');
|
|
@@ -774,7 +978,6 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
774
978
|
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
|
|
775
979
|
act.appendChild(input);
|
|
776
980
|
act.appendChild(save);
|
|
777
|
-
act.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
778
981
|
box.appendChild(act);
|
|
779
982
|
}
|
|
780
983
|
|
|
@@ -817,9 +1020,10 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
817
1020
|
reply.hidden = false;
|
|
818
1021
|
reply.textContent = '\u2026';
|
|
819
1022
|
extra.textContent = '';
|
|
820
|
-
|
|
1023
|
+
// the same words carry the same id on every try, so a retry never sends twice
|
|
1024
|
+
var body = { text: text, send: sendId(wrap, text) };
|
|
821
1025
|
if (more) Object.keys(more).forEach(function (k) { body[k] = more[k]; });
|
|
822
|
-
post(body).then(function (r) {
|
|
1026
|
+
post(body).catch(offline).then(function (r) {
|
|
823
1027
|
if (r.ok && !r.stay) { done(r.ack); return; }
|
|
824
1028
|
send.disabled = false;
|
|
825
1029
|
if (r.stay) { reply.textContent = r.reply;
|
|
@@ -844,7 +1048,11 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
844
1048
|
extra.appendChild(keep);
|
|
845
1049
|
return;
|
|
846
1050
|
}
|
|
847
|
-
|
|
1051
|
+
// it did not send: the words are in the box, send works again, and
|
|
1052
|
+
// one short line says so
|
|
1053
|
+
if (said && !input.value) input.value = said;
|
|
1054
|
+
reply.textContent = r.message || 'that did not send. tap send to try again';
|
|
1055
|
+
input.focus();
|
|
848
1056
|
});
|
|
849
1057
|
}
|
|
850
1058
|
send.addEventListener('click', function () {
|
|
@@ -862,26 +1070,22 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
862
1070
|
return wrap;
|
|
863
1071
|
}
|
|
864
1072
|
|
|
865
|
-
// a quiet line under every card: tap it
|
|
866
|
-
//
|
|
867
|
-
//
|
|
1073
|
+
// a quiet line under every card: tap it and the line becomes the box, in
|
|
1074
|
+
// its own spot. on a question it is the answer, on an idea a note to
|
|
1075
|
+
// rework it, on a yes or a finished job a word to that member; the server
|
|
1076
|
+
// reads it first, and a question back is answered
|
|
868
1077
|
function sayBox() {
|
|
869
1078
|
var wrap = el('div', 'links sayline');
|
|
870
1079
|
var open = el('button', 'quiet', 'or say something');
|
|
871
1080
|
open.type = 'button';
|
|
872
1081
|
open.addEventListener('click', function () {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
box.querySelector('input').focus();
|
|
1082
|
+
if (bet.busy) return;
|
|
1083
|
+
typeHere(open, 'say something', 280, function (text, field) { say(field, { text: text }); });
|
|
876
1084
|
});
|
|
877
1085
|
wrap.appendChild(open);
|
|
878
1086
|
return wrap;
|
|
879
1087
|
}
|
|
880
1088
|
|
|
881
|
-
function sayConvo() {
|
|
882
|
-
return convo(function (body) { return api('/bet/say', body); }, moveOn, 'say something');
|
|
883
|
-
}
|
|
884
|
-
|
|
885
1089
|
// two or three ideas side by side: one tap picks one and opens its full card
|
|
886
1090
|
function drawChoiceCard(s) {
|
|
887
1091
|
var box = document.getElementById('bet');
|
|
@@ -894,17 +1098,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
894
1098
|
b.appendChild(el('span', 't', o.title));
|
|
895
1099
|
b.appendChild(el('span', 'w', o.why));
|
|
896
1100
|
b.addEventListener('click', function () {
|
|
1101
|
+
if (bet.busy) return;
|
|
897
1102
|
buzz();
|
|
898
|
-
|
|
1103
|
+
var heldAt = hold(b);
|
|
899
1104
|
api('/bet/pick', { id: o.id }).then(function (r) {
|
|
900
|
-
if (r.http === 200 && r.bet) {
|
|
901
|
-
|
|
902
|
-
});
|
|
1105
|
+
if (r.http === 200 && r.bet) { afterHold(heldAt).then(function () { turn(r); }); return; }
|
|
1106
|
+
think(false, heldAt);
|
|
1107
|
+
}, function () { think(false, heldAt); });
|
|
903
1108
|
});
|
|
904
1109
|
list.appendChild(b);
|
|
905
1110
|
});
|
|
906
1111
|
box.appendChild(list);
|
|
907
|
-
box.appendChild(el('div', 'links', 'or tap the orb for the next one'));
|
|
908
1112
|
box.appendChild(sayBox());
|
|
909
1113
|
}
|
|
910
1114
|
|
|
@@ -933,13 +1137,24 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
933
1137
|
return;
|
|
934
1138
|
}
|
|
935
1139
|
bet.armed = false;
|
|
936
|
-
s.status = 'starting'; drawBetCard(s);
|
|
937
|
-
api('/bet/do', { confirm: true }).then(function (r) {
|
|
1140
|
+
s.status = 'starting'; s.unstarted = null; drawBetCard(s);
|
|
1141
|
+
api('/bet/do', { confirm: true }).catch(offline).then(function (r) {
|
|
1142
|
+
if (!r || !r.ok) {
|
|
1143
|
+
// it did not get going: the card comes back as it was, do it and
|
|
1144
|
+
// all, with one line why, and the page follows this card again
|
|
1145
|
+
s.status = 'ready';
|
|
1146
|
+
s.unstarted = (r && r.message) || 'that did not start. tap do it to try again';
|
|
1147
|
+
bet.drawn = s.id;
|
|
1148
|
+
drawBetCard(s);
|
|
1149
|
+
refresh();
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
938
1152
|
loadBet(); refresh();
|
|
939
|
-
|
|
1153
|
+
setTimeout(function () { tapOrb(false); }, AFTER_MS);
|
|
940
1154
|
});
|
|
941
1155
|
});
|
|
942
1156
|
act.appendChild(go);
|
|
1157
|
+
if (s.unstarted) act.appendChild(el('p', 'links unsent', s.unstarted));
|
|
943
1158
|
var links = el('div', 'links');
|
|
944
1159
|
var sharp = el('button', null, 'sharper idea');
|
|
945
1160
|
sharp.type = 'button';
|
|
@@ -974,7 +1189,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
974
1189
|
drawBetCard(s);
|
|
975
1190
|
document.body.classList.add('has-card');
|
|
976
1191
|
box.hidden = false;
|
|
977
|
-
box.classList.remove('leave');
|
|
1192
|
+
box.classList.remove('leave', 'holding');
|
|
978
1193
|
box.classList.toggle('quick', Boolean(fast));
|
|
979
1194
|
if (!animate) { box.classList.add('up'); measured(); return; }
|
|
980
1195
|
box.classList.remove('up');
|
|
@@ -1035,39 +1250,53 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1035
1250
|
return Boolean((s.bet || s.job || s.question || s.choice || s.report) && (s.status === 'ready' || s.status === 'started' || s.status === 'starting'));
|
|
1036
1251
|
}
|
|
1037
1252
|
|
|
1038
|
-
// Card to card:
|
|
1039
|
-
//
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
|
|
1253
|
+
// Card to card: a crossfade in place. The small orb stays where it is.
|
|
1254
|
+
// A still copy of the old card fades over the box while the new card,
|
|
1255
|
+
// drawn under it, fades in from DRIFT below over TURN_IN_MS, starting
|
|
1256
|
+
// 80ms in, so its title lands where the old title was. The box eases to
|
|
1257
|
+
// its new height. No layout jump.
|
|
1258
|
+
var TURN_IN_MS = 320, DRIFT = 4;
|
|
1043
1259
|
function turn(s) {
|
|
1260
|
+
crossfade(function () { bet.last = s; bet.drawn = s.id; drawBetCard(s); });
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// draw() puts the next card in the box
|
|
1264
|
+
function crossfade(draw) {
|
|
1044
1265
|
var box = document.getElementById('bet');
|
|
1045
|
-
|
|
1266
|
+
box.classList.remove('holding');
|
|
1267
|
+
if (reduced) { box.style.minHeight = ''; draw(); bet.busy = false; measured(); return; }
|
|
1046
1268
|
var from = box.offsetHeight;
|
|
1047
1269
|
box.style.height = from + 'px';
|
|
1048
1270
|
box.style.minHeight = '';
|
|
1049
|
-
box.
|
|
1050
|
-
|
|
1271
|
+
var ghost = box.cloneNode(true);
|
|
1272
|
+
ghost.removeAttribute('id');
|
|
1273
|
+
ghost.classList.remove('quick', 'turning', 'turn-in');
|
|
1274
|
+
ghost.classList.add('up', 'ghost');
|
|
1275
|
+
ghost.setAttribute('aria-hidden', 'true');
|
|
1276
|
+
ghost.inert = true;
|
|
1277
|
+
var r = box.getBoundingClientRect(), p = box.offsetParent.getBoundingClientRect();
|
|
1278
|
+
ghost.style.top = (r.top - p.top) + 'px';
|
|
1279
|
+
ghost.style.left = (r.left - p.left) + 'px';
|
|
1280
|
+
ghost.style.width = r.width + 'px';
|
|
1281
|
+
box.after(ghost);
|
|
1282
|
+
box.classList.remove('quick', 'turning');
|
|
1283
|
+
draw();
|
|
1284
|
+
box.style.height = 'auto';
|
|
1285
|
+
var to = box.offsetHeight;
|
|
1286
|
+
box.style.height = from + 'px';
|
|
1287
|
+
box.classList.add('turn-in');
|
|
1288
|
+
void box.offsetHeight;
|
|
1289
|
+
box.classList.add('turning');
|
|
1290
|
+
box.classList.remove('turn-in');
|
|
1291
|
+
box.style.height = to + 'px';
|
|
1292
|
+
ghost.classList.add('gone');
|
|
1051
1293
|
setTimeout(function () {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
box.classList.remove('turning', 'turn-out');
|
|
1059
|
-
box.classList.add('turn-in');
|
|
1060
|
-
void box.offsetHeight;
|
|
1061
|
-
box.classList.add('turning', 'settle');
|
|
1062
|
-
box.classList.remove('turn-in');
|
|
1063
|
-
box.style.height = to + 'px';
|
|
1064
|
-
setTimeout(function () {
|
|
1065
|
-
box.classList.remove('turning', 'settle');
|
|
1066
|
-
box.style.height = '';
|
|
1067
|
-
bet.busy = false;
|
|
1068
|
-
measured();
|
|
1069
|
-
}, TURN_IN_MS + 20);
|
|
1070
|
-
}, TURN_OUT_MS);
|
|
1294
|
+
ghost.remove();
|
|
1295
|
+
box.classList.remove('turning');
|
|
1296
|
+
box.style.height = '';
|
|
1297
|
+
bet.busy = false;
|
|
1298
|
+
measured();
|
|
1299
|
+
}, 80 + TURN_IN_MS + 20);
|
|
1071
1300
|
}
|
|
1072
1301
|
|
|
1073
1302
|
// one tap: buzz at once and ask the server for the next card. From the
|
|
@@ -1075,18 +1304,32 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1075
1304
|
function tapOrb(sharper) {
|
|
1076
1305
|
if (bet.busy) return;
|
|
1077
1306
|
buzz();
|
|
1307
|
+
think(sharper, 0);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Ask for the next card. heldAt: an answer has held as chosen since then,
|
|
1311
|
+
// so the card is asked for during the hold and turns in once it is over,
|
|
1312
|
+
// and the time to the card counts from that answer.
|
|
1313
|
+
function think(sharper, heldAt) {
|
|
1078
1314
|
bet.busy = true;
|
|
1079
1315
|
bet.armed = false;
|
|
1080
|
-
bet.tapAt = performance.now();
|
|
1316
|
+
bet.tapAt = heldAt || performance.now();
|
|
1081
1317
|
document.body.removeAttribute('data-reveal-ms');
|
|
1082
1318
|
var onCard = document.body.classList.contains('has-card');
|
|
1083
|
-
|
|
1084
|
-
|
|
1319
|
+
var failed = function () {
|
|
1320
|
+
bet.busy = false;
|
|
1321
|
+
document.getElementById('bet').classList.remove('holding');
|
|
1322
|
+
orb.relax();
|
|
1323
|
+
line.set('that did not go through. tap to try again', true);
|
|
1324
|
+
};
|
|
1325
|
+
Promise.all([api('/bet/think', { sharper: Boolean(sharper) }), afterHold(heldAt)]).then(function (both) {
|
|
1326
|
+
var s = both[0];
|
|
1327
|
+
if (s.http !== 200) { failed(); return; }
|
|
1085
1328
|
bet.status = s.status;
|
|
1086
1329
|
if (onCard && isCard(s)) { turn(s); return; }
|
|
1087
1330
|
if (onCard) { dissolve(function () { reveal(s, true); }); return; }
|
|
1088
1331
|
reveal(s, true);
|
|
1089
|
-
});
|
|
1332
|
+
}, failed);
|
|
1090
1333
|
}
|
|
1091
1334
|
|
|
1092
1335
|
function loadBet() {
|
|
@@ -1117,12 +1360,32 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1117
1360
|
});
|
|
1118
1361
|
}
|
|
1119
1362
|
|
|
1363
|
+
// The small orb on a card goes back to the big dotted orb, the same calm
|
|
1364
|
+
// way the page goes home before a new idea: the card fades, the small orb
|
|
1365
|
+
// glides down and the dots form again, idle under the pointer. The card
|
|
1366
|
+
// stays where it was on the server, so nothing is passed yet.
|
|
1367
|
+
function goHome() {
|
|
1368
|
+
if (bet.busy) return;
|
|
1369
|
+
buzz();
|
|
1370
|
+
bet.busy = true;
|
|
1371
|
+
bet.armed = false;
|
|
1372
|
+
dissolve(function () { bet.busy = false; line.set('tap to think', false); });
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// The big orb: the next idea, with the spiral, as on page open. An idea
|
|
1376
|
+
// that landed behind the calm line is the next one, so it is never passed.
|
|
1377
|
+
function tapBig() { tapOrb(false); }
|
|
1378
|
+
|
|
1379
|
+
function tapSmallOrb() {
|
|
1380
|
+
if (document.body.classList.contains('has-card')) goHome(); else tapBig();
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1120
1383
|
var canvas = document.getElementById('orb');
|
|
1121
1384
|
var lavaTap = document.getElementById('lava');
|
|
1122
|
-
[canvas, lavaTap].forEach(function (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault();
|
|
1385
|
+
[[canvas, tapBig], [lavaTap, tapSmallOrb]].forEach(function (pair) {
|
|
1386
|
+
pair[0].addEventListener('click', pair[1]);
|
|
1387
|
+
pair[0].addEventListener('keydown', function (e) {
|
|
1388
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pair[1](); }
|
|
1126
1389
|
});
|
|
1127
1390
|
});
|
|
1128
1391
|
|
|
@@ -1130,14 +1393,19 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1130
1393
|
|
|
1131
1394
|
var teamBox = document.getElementById('team');
|
|
1132
1395
|
var teamInner = document.getElementById('team-inner');
|
|
1396
|
+
// the way back from what is on top: the team closes, a talk card goes back to the team
|
|
1397
|
+
var teamBack = null;
|
|
1398
|
+
|
|
1399
|
+
function closeTeam() { teamBox.hidden = true; }
|
|
1133
1400
|
|
|
1134
1401
|
function teamHead(title) {
|
|
1135
1402
|
teamInner.textContent = '';
|
|
1403
|
+
teamBack = closeTeam;
|
|
1136
1404
|
var head = el('div', 'head');
|
|
1137
1405
|
head.appendChild(el('h1', null, title));
|
|
1138
1406
|
var close = el('button', 'quiet', 'close');
|
|
1139
1407
|
close.type = 'button';
|
|
1140
|
-
close.addEventListener('click',
|
|
1408
|
+
close.addEventListener('click', closeTeam);
|
|
1141
1409
|
head.appendChild(close);
|
|
1142
1410
|
teamInner.appendChild(head);
|
|
1143
1411
|
}
|
|
@@ -1166,6 +1434,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1166
1434
|
// a talk card: what the member is on, and a word to steer them
|
|
1167
1435
|
function drawTalk(r) {
|
|
1168
1436
|
teamHead(r.member);
|
|
1437
|
+
teamBack = openTeam;
|
|
1169
1438
|
teamInner.appendChild(el('p', 'why', r.status + ': ' + r.on + '. ' + r.time + '.'));
|
|
1170
1439
|
var talkBox = convo(function (body) { body.member = r.member; return api('/bet/steer', body); }, function (ack) {
|
|
1171
1440
|
talkBox.replaceWith(el('p', 'mission', ack));
|
|
@@ -1179,6 +1448,25 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1179
1448
|
|
|
1180
1449
|
document.getElementById('working').addEventListener('click', openTeam);
|
|
1181
1450
|
|
|
1451
|
+
// Esc on the team page steps back one layer, the way its own buttons do:
|
|
1452
|
+
// words being typed are cancelled and the cursor leaves first, then the
|
|
1453
|
+
// talk card goes back to the team, then the team closes to the card.
|
|
1454
|
+
// On the card itself it does nothing. It listens on the way down, so the
|
|
1455
|
+
// focused element is still the field the key was pressed in.
|
|
1456
|
+
document.addEventListener('keydown', function (e) {
|
|
1457
|
+
if (e.key !== 'Escape' || e.isComposing || teamBox.hidden) return;
|
|
1458
|
+
e.preventDefault();
|
|
1459
|
+
var field = document.activeElement;
|
|
1460
|
+
if (field && (field.tagName === 'INPUT' || field.tagName === 'TEXTAREA')) {
|
|
1461
|
+
field.value = '';
|
|
1462
|
+
// the field's own input listener drops a second tap armed for those words
|
|
1463
|
+
field.dispatchEvent(new Event('input'));
|
|
1464
|
+
field.blur();
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
teamBack();
|
|
1468
|
+
}, true);
|
|
1469
|
+
|
|
1182
1470
|
// the footer: how many members are really working, or the team when none are
|
|
1183
1471
|
function drawWorking(n) {
|
|
1184
1472
|
var btn = document.getElementById('working');
|