@eintrek/erp-shell 0.1.63 → 0.1.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/guide/dom.d.ts +67 -1
- package/dist/guide/guide-styles.d.ts +1 -1
- package/dist/guide/guide-ui.d.ts +38 -5
- package/dist/guide/progress.d.ts +7 -0
- package/dist/index.esm.js +347 -57
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +346 -56
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -14,7 +14,7 @@ import { createPortal } from 'react-dom';
|
|
|
14
14
|
import { useTheme, ThemeProvider as ThemeProvider$1 } from 'next-themes';
|
|
15
15
|
import axios, { isAxiosError, AxiosHeaders } from 'axios';
|
|
16
16
|
export { isAxiosError } from 'axios';
|
|
17
|
-
import { computePosition, offset,
|
|
17
|
+
import { computePosition, offset, shift, arrow } from '@floating-ui/dom';
|
|
18
18
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -1956,7 +1956,7 @@ function platformApiBase(app) {
|
|
|
1956
1956
|
* This package's version, attached to help messages so the back office can
|
|
1957
1957
|
* tell which shell a report came from. Keep in step with package.json.
|
|
1958
1958
|
*/
|
|
1959
|
-
const SHELL_VERSION = "0.1.
|
|
1959
|
+
const SHELL_VERSION = "0.1.64";
|
|
1960
1960
|
|
|
1961
1961
|
const HELP_APP_LABEL = {
|
|
1962
1962
|
platform: "แพลตฟอร์ม",
|
|
@@ -4033,7 +4033,7 @@ function announceStep(d, suite, env) {
|
|
|
4033
4033
|
const step = mission.steps[s.index];
|
|
4034
4034
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4035
4035
|
const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
|
|
4036
|
-
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : resolveText(step.why, ctx);
|
|
4036
|
+
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
|
|
4037
4037
|
d.effects.push({
|
|
4038
4038
|
type: "announce",
|
|
4039
4039
|
text: `ขั้นที่ ${s.index + 1} จาก ${mission.steps.length}. ${title}${why ? `. ${why}` : ""}`,
|
|
@@ -4195,6 +4195,7 @@ function startMission(d, suite, env, key, replay) {
|
|
|
4195
4195
|
const entry = d.state.progress.missions[key];
|
|
4196
4196
|
// A mission already done is only ever replayed; its done status stays.
|
|
4197
4197
|
const isReplay = replay || entry?.status === "done";
|
|
4198
|
+
const before = d.state.progress;
|
|
4198
4199
|
d.state = { ...d.state, panelOpen: false, justDone: null };
|
|
4199
4200
|
if (d.state.progress.welcome === "not_seen")
|
|
4200
4201
|
withProgress(d, { welcome: "started" });
|
|
@@ -4212,7 +4213,9 @@ function startMission(d, suite, env, key, replay) {
|
|
|
4212
4213
|
index = checkpointAtOrBefore(mission.steps, at);
|
|
4213
4214
|
}
|
|
4214
4215
|
enterStep(d, suite, env, mission, index, { replay: isReplay });
|
|
4215
|
-
|
|
4216
|
+
// A replay never writes progress; only what starting it changed (the
|
|
4217
|
+
// welcome, first seen through a replay) is saved.
|
|
4218
|
+
if (isReplay && d.state.progress !== before)
|
|
4216
4219
|
save(d);
|
|
4217
4220
|
}
|
|
4218
4221
|
function exitMission(d) {
|
|
@@ -4786,6 +4789,27 @@ function putBody(p, app) {
|
|
|
4786
4789
|
TERMINAL.includes(p.server_status),
|
|
4787
4790
|
};
|
|
4788
4791
|
}
|
|
4792
|
+
function stable(v) {
|
|
4793
|
+
if (Array.isArray(v))
|
|
4794
|
+
return `[${v.map(stable).join(",")}]`;
|
|
4795
|
+
if (isRecord(v)) {
|
|
4796
|
+
return `{${Object.keys(v)
|
|
4797
|
+
.sort()
|
|
4798
|
+
.map(k => `${JSON.stringify(k)}:${stable(v[k])}`)
|
|
4799
|
+
.join(",")}}`;
|
|
4800
|
+
}
|
|
4801
|
+
return JSON.stringify(v) ?? "null";
|
|
4802
|
+
}
|
|
4803
|
+
/**
|
|
4804
|
+
* What a PUT would store, without the rev it is checked against: two equal
|
|
4805
|
+
* keys mean the save would change nothing, and it is not sent (a replay
|
|
4806
|
+
* starting, a step entered again, never bump the row's rev). Key order does
|
|
4807
|
+
* not matter.
|
|
4808
|
+
*/
|
|
4809
|
+
function payloadKey(p, app) {
|
|
4810
|
+
const { rev: _rev, restart: _restart, ...rest } = putBody(p, app);
|
|
4811
|
+
return stable(rest);
|
|
4812
|
+
}
|
|
4789
4813
|
const MISSION_RANK = { todo: 0, skipped: 1, active: 1, done: 3 };
|
|
4790
4814
|
const WELCOME_RANK = { not_seen: 0, skipped: 1, started: 1 };
|
|
4791
4815
|
/**
|
|
@@ -4920,8 +4944,9 @@ function isPresent(el) {
|
|
|
4920
4944
|
if (r.width <= 0 || r.height <= 0)
|
|
4921
4945
|
return false;
|
|
4922
4946
|
// Slid off the side (a closed drawer); off the top or bottom is only
|
|
4923
|
-
// scrolled away, and the tour scrolls it back.
|
|
4924
|
-
|
|
4947
|
+
// scrolled away, and the tour scrolls it back. So is a tab scrolled out of
|
|
4948
|
+
// a row of tabs that scrolls sideways, as long as the row is on screen.
|
|
4949
|
+
if ((r.right <= 0 || r.left >= window.innerWidth) && !inSidewaysScroller(el))
|
|
4925
4950
|
return false;
|
|
4926
4951
|
if (el.getAttribute("data-state") === "closed")
|
|
4927
4952
|
return false;
|
|
@@ -5001,6 +5026,26 @@ function sameRect(a, b) {
|
|
|
5001
5026
|
const POPPER_SELECTOR = "[data-radix-popper-content-wrapper]";
|
|
5002
5027
|
/** Toasts stay usable above the tour. */
|
|
5003
5028
|
const TOASTER_SELECTOR = "[data-sonner-toaster]";
|
|
5029
|
+
/** Open dialogs (Radix dialog, alert dialog, sheet, popover content). */
|
|
5030
|
+
const OPEN_DIALOG_SELECTOR = '[role="dialog"][data-state="open"],[role="alertdialog"][data-state="open"]';
|
|
5031
|
+
/**
|
|
5032
|
+
* Whether an open dialog of the app's covers the target: the target is
|
|
5033
|
+
* neither in it nor around it (a confirm opened by the step's own button).
|
|
5034
|
+
* The ring and the hand would be drawn over the dialog, pointing at a button
|
|
5035
|
+
* behind it.
|
|
5036
|
+
*/
|
|
5037
|
+
function coveredByDialog(el) {
|
|
5038
|
+
if (!hasDocument() || !el)
|
|
5039
|
+
return false;
|
|
5040
|
+
for (const d of Array.from(document.querySelectorAll(OPEN_DIALOG_SELECTOR))) {
|
|
5041
|
+
if (inGuideLayer(d) || !isPresent(d))
|
|
5042
|
+
continue;
|
|
5043
|
+
if (d.contains(el) || el.contains(d))
|
|
5044
|
+
continue;
|
|
5045
|
+
return true;
|
|
5046
|
+
}
|
|
5047
|
+
return false;
|
|
5048
|
+
}
|
|
5004
5049
|
function openPoppers() {
|
|
5005
5050
|
if (!hasDocument())
|
|
5006
5051
|
return [];
|
|
@@ -5018,18 +5063,181 @@ function prefersReducedMotion() {
|
|
|
5018
5063
|
function isPhoneWidth() {
|
|
5019
5064
|
return typeof window !== "undefined" && window.innerWidth < 768;
|
|
5020
5065
|
}
|
|
5021
|
-
|
|
5066
|
+
/* ---------------- what is really on screen ---------------- */
|
|
5067
|
+
const INFINITE = 1e7;
|
|
5068
|
+
/** The overlap of two rectangles, or null when they do not meet. */
|
|
5069
|
+
function intersectRect(a, b) {
|
|
5070
|
+
const x = Math.max(a.x, b.x);
|
|
5071
|
+
const y = Math.max(a.y, b.y);
|
|
5072
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
5073
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
5074
|
+
if (right - x <= 0 || bottom - y <= 0)
|
|
5075
|
+
return null;
|
|
5076
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
5077
|
+
}
|
|
5078
|
+
/** The part of `rect` inside every clip (scroll boxes, the viewport); null when none is. */
|
|
5079
|
+
function visibleRect(rect, clips) {
|
|
5080
|
+
let r = rect;
|
|
5081
|
+
for (const c of clips) {
|
|
5082
|
+
if (!r)
|
|
5083
|
+
return null;
|
|
5084
|
+
r = intersectRect(r, c);
|
|
5085
|
+
}
|
|
5086
|
+
return r;
|
|
5087
|
+
}
|
|
5088
|
+
const clipsX = (v) => v !== "visible";
|
|
5089
|
+
const scrollsX = (v) => v === "auto" || v === "scroll";
|
|
5090
|
+
/**
|
|
5091
|
+
* The ancestors that cut the element off: anything with overflow other than
|
|
5092
|
+
* visible on an axis. The body and the root scroll the page and are covered by
|
|
5093
|
+
* the viewport. Read once per target; their rectangles are read every frame.
|
|
5094
|
+
*/
|
|
5095
|
+
function clipAncestors(el) {
|
|
5096
|
+
const out = [];
|
|
5097
|
+
if (typeof getComputedStyle !== "function")
|
|
5098
|
+
return out;
|
|
5099
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
5100
|
+
if (n.hasAttribute(GUIDE_LAYER_ATTRIBUTE))
|
|
5101
|
+
break;
|
|
5102
|
+
const s = getComputedStyle(n);
|
|
5103
|
+
const x = clipsX(s.overflowX);
|
|
5104
|
+
const y = clipsX(s.overflowY);
|
|
5105
|
+
if (x || y)
|
|
5106
|
+
out.push({ el: n, x, y });
|
|
5107
|
+
if (s.position === "fixed")
|
|
5108
|
+
break;
|
|
5109
|
+
}
|
|
5110
|
+
return out;
|
|
5111
|
+
}
|
|
5112
|
+
/** An ancestor's clip as a rectangle: unbounded on an axis it does not clip. */
|
|
5113
|
+
function clipRectOf(c) {
|
|
5114
|
+
const r = c.el.getBoundingClientRect();
|
|
5115
|
+
return {
|
|
5116
|
+
x: c.x ? r.left : -INFINITE,
|
|
5117
|
+
y: c.y ? r.top : -INFINITE,
|
|
5118
|
+
width: c.x ? r.width : INFINITE * 2,
|
|
5119
|
+
height: c.y ? r.height : INFINITE * 2,
|
|
5120
|
+
};
|
|
5121
|
+
}
|
|
5122
|
+
function viewportRect() {
|
|
5123
|
+
const vv = typeof window !== "undefined" ? window.visualViewport : undefined;
|
|
5124
|
+
return {
|
|
5125
|
+
x: 0,
|
|
5126
|
+
y: vv ? vv.offsetTop : 0,
|
|
5127
|
+
width: window.innerWidth,
|
|
5128
|
+
height: vv ? vv.height : window.innerHeight,
|
|
5129
|
+
};
|
|
5130
|
+
}
|
|
5131
|
+
/**
|
|
5132
|
+
* The part of the element a person can see: its box cut by every scroll box
|
|
5133
|
+
* around it and by the viewport. The spotlight, its hole, the ring and the
|
|
5134
|
+
* hand use this, so a tab half out of a sideways-scrolling row of tabs is not
|
|
5135
|
+
* spotlit where it cannot be pressed. Null when none of it is on screen.
|
|
5136
|
+
*/
|
|
5137
|
+
function visibleRectOf(el, ancestors = clipAncestors(el)) {
|
|
5138
|
+
return visibleRect(rectOf(el), [...ancestors.map(clipRectOf), viewportRect()]);
|
|
5139
|
+
}
|
|
5140
|
+
/** Inside a box that scrolls sideways (with somewhere to scroll) that is itself on screen. */
|
|
5141
|
+
function inSidewaysScroller(el) {
|
|
5142
|
+
if (typeof getComputedStyle !== "function")
|
|
5143
|
+
return false;
|
|
5144
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
5145
|
+
const s = getComputedStyle(n);
|
|
5146
|
+
if (scrollsX(s.overflowX) && n.scrollWidth > n.clientWidth + 1) {
|
|
5147
|
+
const r = n.getBoundingClientRect();
|
|
5148
|
+
return r.width > 0 && r.right > 0 && r.left < window.innerWidth;
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
return false;
|
|
5152
|
+
}
|
|
5153
|
+
/**
|
|
5154
|
+
* How to bring a target into view, or null when it already is: to the middle
|
|
5155
|
+
* when it is above or below the comfortable band (under the header, at the
|
|
5156
|
+
* bottom edge), the nearest edge when only a side is cut off (a tab out of a
|
|
5157
|
+
* sideways-scrolling row, on a phone).
|
|
5158
|
+
*/
|
|
5159
|
+
function revealPlan(rect, visible, viewportHeight) {
|
|
5160
|
+
const offVertically = rect.y < 70 || rect.y + rect.height > viewportHeight - 20;
|
|
5161
|
+
const cutSideways = !visible || visible.width < rect.width - 1;
|
|
5162
|
+
if (!offVertically && !cutSideways)
|
|
5163
|
+
return null;
|
|
5164
|
+
return { block: offVertically ? "center" : "nearest", inline: "nearest" };
|
|
5165
|
+
}
|
|
5166
|
+
/** Scrolls the target into view (instant under reduced motion). */
|
|
5022
5167
|
function revealTarget(el) {
|
|
5023
|
-
const r = el.getBoundingClientRect();
|
|
5024
5168
|
const vh = window.visualViewport?.height ?? window.innerHeight;
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5169
|
+
const plan = revealPlan(rectOf(el), visibleRectOf(el), vh);
|
|
5170
|
+
if (!plan)
|
|
5171
|
+
return;
|
|
5172
|
+
try {
|
|
5173
|
+
el.scrollIntoView({ ...plan, behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
5174
|
+
}
|
|
5175
|
+
catch {
|
|
5176
|
+
el.scrollIntoView();
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
/* ---------------- the scrim's holes, as one union ---------------- */
|
|
5180
|
+
/**
|
|
5181
|
+
* The scrim's blocking area: the screen (with a 10 px margin) minus the union
|
|
5182
|
+
* of the holes, as rectangles that do not overlap. Cutting each hole out with
|
|
5183
|
+
* an even-odd fill made the place where two holes overlap (a field and the
|
|
5184
|
+
* select list open over it) solid again, and the option there could not be
|
|
5185
|
+
* pressed.
|
|
5186
|
+
*
|
|
5187
|
+
* Every hole edge splits the screen into a grid; a cell is blocked when no
|
|
5188
|
+
* hole covers it. Cells in a row are joined, and identical rows stacked.
|
|
5189
|
+
*/
|
|
5190
|
+
function blockedRects(holes, width, height) {
|
|
5191
|
+
const x0 = -10;
|
|
5192
|
+
const y0 = -10;
|
|
5193
|
+
const x1 = width + 10;
|
|
5194
|
+
const y1 = height + 10;
|
|
5195
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
5196
|
+
const hs = holes
|
|
5197
|
+
.filter(h => h.width > 0 && h.height > 0)
|
|
5198
|
+
.map(h => ({
|
|
5199
|
+
l: clamp(h.x, x0, x1),
|
|
5200
|
+
t: clamp(h.y, y0, y1),
|
|
5201
|
+
r: clamp(h.x + h.width, x0, x1),
|
|
5202
|
+
b: clamp(h.y + h.height, y0, y1),
|
|
5203
|
+
}))
|
|
5204
|
+
.filter(h => h.r > h.l && h.b > h.t);
|
|
5205
|
+
if (!hs.length)
|
|
5206
|
+
return [{ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }];
|
|
5207
|
+
const uniq = (v) => Array.from(new Set(v)).sort((a, b) => a - b);
|
|
5208
|
+
const xs = uniq([x0, x1, ...hs.flatMap(h => [h.l, h.r])]);
|
|
5209
|
+
const ys = uniq([y0, y1, ...hs.flatMap(h => [h.t, h.b])]);
|
|
5210
|
+
const out = [];
|
|
5211
|
+
let prev = null;
|
|
5212
|
+
for (let j = 0; j < ys.length - 1; j++) {
|
|
5213
|
+
const top = ys[j];
|
|
5214
|
+
const bottom = ys[j + 1];
|
|
5215
|
+
const cy = (top + bottom) / 2;
|
|
5216
|
+
const row = [];
|
|
5217
|
+
for (let i = 0; i < xs.length - 1; i++) {
|
|
5218
|
+
const left = xs[i];
|
|
5219
|
+
const right = xs[i + 1];
|
|
5220
|
+
const cx = (left + right) / 2;
|
|
5221
|
+
const covered = hs.some(h => cx > h.l && cx < h.r && cy > h.t && cy < h.b);
|
|
5222
|
+
if (covered)
|
|
5223
|
+
continue;
|
|
5224
|
+
const last = row[row.length - 1];
|
|
5225
|
+
if (last && last.x + last.width === left)
|
|
5226
|
+
last.width = right - last.x;
|
|
5227
|
+
else
|
|
5228
|
+
row.push({ x: left, y: top, width: right - left, height: bottom - top });
|
|
5028
5229
|
}
|
|
5029
|
-
|
|
5030
|
-
|
|
5230
|
+
const key = row.map(r => `${r.x},${r.width}`).join("|");
|
|
5231
|
+
if (prev && prev.key === key) {
|
|
5232
|
+
for (const r of prev.rects)
|
|
5233
|
+
r.height += bottom - top;
|
|
5234
|
+
}
|
|
5235
|
+
else {
|
|
5236
|
+
out.push(...row);
|
|
5237
|
+
prev = { key, rects: row };
|
|
5031
5238
|
}
|
|
5032
5239
|
}
|
|
5240
|
+
return out;
|
|
5033
5241
|
}
|
|
5034
5242
|
|
|
5035
5243
|
/**
|
|
@@ -5051,7 +5259,8 @@ const GUIDE_CSS = `
|
|
|
5051
5259
|
.eg-i{pointer-events:auto}
|
|
5052
5260
|
.eg-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
5053
5261
|
.eg-scrim-svg{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
|
|
5054
|
-
.eg-scrim-svg
|
|
5262
|
+
.eg-scrim-svg .eg-scrim-fill{fill:var(--eg-scrim);pointer-events:none}
|
|
5263
|
+
.eg-scrim-svg .eg-scrim-hit{fill:transparent;pointer-events:all}
|
|
5055
5264
|
.eg-scrim{position:fixed;inset:0;background:var(--eg-scrim);pointer-events:auto}
|
|
5056
5265
|
.eg-scrim.eg-light{background:rgba(12,20,34,.45)}
|
|
5057
5266
|
.eg-scrim.eg-faint{background:rgba(12,20,34,.35)}
|
|
@@ -5160,6 +5369,7 @@ const GUIDE_CSS = `
|
|
|
5160
5369
|
.eg-q-item.eg-done .eg-t b{color:var(--eg-muted)}
|
|
5161
5370
|
.eg-q-item .eg-t small{display:block;font-size:12px;color:var(--eg-muted)}
|
|
5162
5371
|
.eg-q-item .eg-t small.eg-sk{color:#b06a00}
|
|
5372
|
+
.eg-q-item .eg-t small.eg-lock{color:var(--eg-fg);font-weight:500}
|
|
5163
5373
|
.eg-q-item .eg-btn{height:32px;padding:0 12px;font-size:13px}
|
|
5164
5374
|
.eg-q-acts{display:flex;flex-direction:column;align-items:flex-end;gap:2px}
|
|
5165
5375
|
.eg-q-acts .eg-link{height:22px;font-size:12px;padding:0 2px}
|
|
@@ -5224,15 +5434,51 @@ function roundRect(r, radius) {
|
|
|
5224
5434
|
`H${x + rad}A${rad} ${rad} 0 0 1 ${x} ${y + h - rad}` +
|
|
5225
5435
|
`V${y + rad}A${rad} ${rad} 0 0 1 ${x + rad} ${y}Z`);
|
|
5226
5436
|
}
|
|
5437
|
+
const rectPath = (r) => `M${r.x} ${r.y}H${r.x + r.width}V${r.y + r.height}H${r.x}Z`;
|
|
5227
5438
|
/**
|
|
5228
|
-
* The dim layer that eats every press outside the holes.
|
|
5229
|
-
*
|
|
5230
|
-
*
|
|
5439
|
+
* The dim layer that eats every press outside the holes. The holes are a
|
|
5440
|
+
* union: where two overlap (a field and the select list open over it) the
|
|
5441
|
+
* page stays open.
|
|
5442
|
+
*
|
|
5443
|
+
* - The look: one rectangle masked by the rounded holes (a mask adds holes
|
|
5444
|
+
* up, so an overlap is not filled back in).
|
|
5445
|
+
* - The presses: a transparent path made of the screen minus the holes, as
|
|
5446
|
+
* rectangles that do not overlap (blockedRects). A press inside a hole
|
|
5447
|
+
* reaches the page underneath; any other lands on the path (marked
|
|
5448
|
+
* data-eg-block) and is cancelled.
|
|
5231
5449
|
*/
|
|
5232
5450
|
function Scrim({ holes, width, height }) {
|
|
5233
|
-
const
|
|
5234
|
-
const
|
|
5235
|
-
return (
|
|
5451
|
+
const maskId = "eg-scrim-mask-" + React.useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
5452
|
+
const block = blockedRects(holes, width, height).map(rectPath).join("");
|
|
5453
|
+
return (jsxs("svg", { className: "eg-scrim-svg", width: width, height: height, "aria-hidden": "true", children: [holes.length ? (jsx("defs", { children: jsxs("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: -10, y: -10, width: width + 20, height: height + 20, children: [jsx("rect", { x: -10, y: -10, width: width + 20, height: height + 20, fill: "#fff" }), holes.map((h, i) => (jsx("path", { d: roundRect(h, 12), fill: "#000" }, i)))] }) })) : null, jsx("rect", { className: "eg-scrim-fill", x: -10, y: -10, width: width + 20, height: height + 20, mask: holes.length ? `url(#${maskId})` : undefined }), jsx("path", { className: "eg-scrim-hit", d: block, "data-eg-block": "" })] }));
|
|
5454
|
+
}
|
|
5455
|
+
/** Whether two rectangles overlap. */
|
|
5456
|
+
const meets = (a, b) => !!intersectRect(a, b);
|
|
5457
|
+
/** The smallest rectangle around all of them. */
|
|
5458
|
+
function boundingRect(rs) {
|
|
5459
|
+
const x = Math.min(...rs.map(r => r.x));
|
|
5460
|
+
const y = Math.min(...rs.map(r => r.y));
|
|
5461
|
+
const right = Math.max(...rs.map(r => r.x + r.width));
|
|
5462
|
+
const bottom = Math.max(...rs.map(r => r.y + r.height));
|
|
5463
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
5464
|
+
}
|
|
5465
|
+
/** Where the bubble may go, in order of preference. */
|
|
5466
|
+
const BUBBLE_PLACEMENTS = ["right", "bottom", "top", "left"];
|
|
5467
|
+
/**
|
|
5468
|
+
* The first placement (in BUBBLE_PLACEMENTS order) whose bubble is on screen,
|
|
5469
|
+
* off the target (with its open list), and off everything to keep clear;
|
|
5470
|
+
* failing that, the first that is only on screen and off the target; -1 when
|
|
5471
|
+
* none is.
|
|
5472
|
+
*/
|
|
5473
|
+
function pickPlacement(results, bw, bh, anchor, clear, bounds) {
|
|
5474
|
+
const box = (r) => ({ x: r.x, y: r.y, width: bw, height: bh });
|
|
5475
|
+
const fits = (b) => b.x >= bounds.x - 1 && b.y >= bounds.y - 1 && b.x + b.width <= bounds.x + bounds.width + 1 && b.y + b.height <= bounds.y + bounds.height + 1;
|
|
5476
|
+
const ok = results.map(r => {
|
|
5477
|
+
const b = box(r);
|
|
5478
|
+
return fits(b) && !meets(b, anchor);
|
|
5479
|
+
});
|
|
5480
|
+
const best = results.findIndex((r, i) => ok[i] && !clear.some(c => meets(box(r), c)));
|
|
5481
|
+
return best >= 0 ? best : ok.indexOf(true);
|
|
5236
5482
|
}
|
|
5237
5483
|
const BUBBLE_TITLE_ID = "eg-bubble-title";
|
|
5238
5484
|
const BUBBLE_WHY_ID = "eg-bubble-why";
|
|
@@ -5272,12 +5518,21 @@ function StepLayer(p) {
|
|
|
5272
5518
|
const gap = p.showHand ? 34 : 14;
|
|
5273
5519
|
const spotKey = spot ? `${spot.x}|${spot.y}|${spot.width}|${spot.height}` : "";
|
|
5274
5520
|
const contentKey = `${p.bubble.title}|${p.bubble.why}|${p.bubble.error?.title ?? ""}|${p.bubble.showNext}`;
|
|
5521
|
+
// What the bubble sits beside: the target, with an open list over it (the
|
|
5522
|
+
// list is what the person is choosing from now). What it must not cover:
|
|
5523
|
+
// open lists, and the other anchors the step allows (a map step's
|
|
5524
|
+
// coordinate fields).
|
|
5525
|
+
const popoverList = p.popovers ?? [];
|
|
5526
|
+
const anchor = spot ? boundingRect([spot, ...popoverList.filter(o => meets(o, spot))]) : null;
|
|
5527
|
+
const clear = [...popoverList, ...p.holes];
|
|
5528
|
+
const anchorKey = anchor ? `${anchor.x}|${anchor.y}|${anchor.width}|${anchor.height}` : "";
|
|
5529
|
+
const clearKey = clear.map(r => `${r.x},${r.y},${r.width},${r.height}`).join(";");
|
|
5275
5530
|
useLayoutEffect(() => {
|
|
5276
5531
|
const el = bubbleRef.current;
|
|
5277
5532
|
if (!el || !showBubble)
|
|
5278
5533
|
return;
|
|
5279
5534
|
const m = 16;
|
|
5280
|
-
if (!spot) {
|
|
5535
|
+
if (!spot || !anchor) {
|
|
5281
5536
|
// Nothing to point at: the middle of the screen (the bottom on a phone).
|
|
5282
5537
|
const bw = el.offsetWidth;
|
|
5283
5538
|
const bh = el.offsetHeight;
|
|
@@ -5288,13 +5543,13 @@ function StepLayer(p) {
|
|
|
5288
5543
|
}
|
|
5289
5544
|
if (phone) {
|
|
5290
5545
|
// Docked to the top or the bottom of the screen, whichever covers less
|
|
5291
|
-
// of the target (target in the lower half: top).
|
|
5546
|
+
// of the target and its open list (target in the lower half: top).
|
|
5292
5547
|
const vh = viewport.height;
|
|
5293
5548
|
const oy = viewport.offsetTop;
|
|
5294
5549
|
const bh = el.offsetHeight;
|
|
5295
5550
|
const topT = oy + m + 4;
|
|
5296
5551
|
const botT = oy + vh - bh - m;
|
|
5297
|
-
const { y, height: h } =
|
|
5552
|
+
const { y, height: h } = anchor;
|
|
5298
5553
|
const overlapTop = Math.max(0, topT + bh + 8 - y);
|
|
5299
5554
|
const overlapBot = Math.max(0, y + h + gap - botT);
|
|
5300
5555
|
const top = y + h / 2 > vh / 2
|
|
@@ -5308,40 +5563,42 @@ function StepLayer(p) {
|
|
|
5308
5563
|
return;
|
|
5309
5564
|
}
|
|
5310
5565
|
let cancelled = false;
|
|
5566
|
+
const a = anchor;
|
|
5311
5567
|
const ref = {
|
|
5312
5568
|
getBoundingClientRect: () => ({
|
|
5313
|
-
x:
|
|
5314
|
-
y:
|
|
5315
|
-
left:
|
|
5316
|
-
top:
|
|
5317
|
-
width:
|
|
5318
|
-
height:
|
|
5319
|
-
right:
|
|
5320
|
-
bottom:
|
|
5569
|
+
x: a.x,
|
|
5570
|
+
y: a.y,
|
|
5571
|
+
left: a.x,
|
|
5572
|
+
top: a.y,
|
|
5573
|
+
width: a.width,
|
|
5574
|
+
height: a.height,
|
|
5575
|
+
right: a.x + a.width,
|
|
5576
|
+
bottom: a.y + a.height,
|
|
5321
5577
|
}),
|
|
5322
5578
|
};
|
|
5323
|
-
computePosition(ref, el, {
|
|
5579
|
+
const at = (placement) => computePosition(ref, el, {
|
|
5324
5580
|
strategy: "fixed",
|
|
5325
|
-
placement
|
|
5581
|
+
placement,
|
|
5326
5582
|
middleware: [
|
|
5327
|
-
offset(
|
|
5328
|
-
flip({ fallbackPlacements: ["bottom", "top", "left"], padding: m }),
|
|
5583
|
+
offset(placement.startsWith("bottom") ? gap : 14),
|
|
5329
5584
|
shift({ padding: m }),
|
|
5330
5585
|
...(arrowRef.current ? [arrow({ element: arrowRef.current, padding: 14 })] : []),
|
|
5331
5586
|
],
|
|
5332
|
-
})
|
|
5333
|
-
|
|
5587
|
+
});
|
|
5588
|
+
Promise.all(BUBBLE_PLACEMENTS.map(at))
|
|
5589
|
+
.then(results => {
|
|
5334
5590
|
if (cancelled)
|
|
5335
5591
|
return;
|
|
5336
5592
|
const bw = el.offsetWidth;
|
|
5337
5593
|
const bh = el.offsetHeight;
|
|
5338
|
-
const
|
|
5339
|
-
const
|
|
5340
|
-
if (
|
|
5594
|
+
const bounds = { x: m, y: viewport.offsetTop + m, width: window.innerWidth - 2 * m, height: viewport.height - 2 * m };
|
|
5595
|
+
const i = pickPlacement(results, bw, bh, a, clear, bounds);
|
|
5596
|
+
if (i < 0) {
|
|
5341
5597
|
// Nowhere beside it: the bottom-right corner.
|
|
5342
5598
|
setPos({ left: window.innerWidth - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
|
|
5343
5599
|
return;
|
|
5344
5600
|
}
|
|
5601
|
+
const { x, y, placement, middlewareData } = results[i];
|
|
5345
5602
|
const side = placement.split("-")[0];
|
|
5346
5603
|
setPos({ left: x, top: y, side, ax: middlewareData.arrow?.x, ay: middlewareData.arrow?.y });
|
|
5347
5604
|
})
|
|
@@ -5352,11 +5609,14 @@ function StepLayer(p) {
|
|
|
5352
5609
|
return () => {
|
|
5353
5610
|
cancelled = true;
|
|
5354
5611
|
};
|
|
5355
|
-
//
|
|
5612
|
+
// The keys stand for spot, the lists, the holes and the bubble's size.
|
|
5356
5613
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5357
|
-
}, [spotKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
5358
|
-
const
|
|
5359
|
-
const
|
|
5614
|
+
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
5615
|
+
const popovers = p.popovers ?? [];
|
|
5616
|
+
const covered = !!p.covered;
|
|
5617
|
+
const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
|
|
5618
|
+
const ringShown = !!spot && p.phase === "ready" && !covered && !popovers.some(o => meets(o, spot));
|
|
5619
|
+
const hand = spot && p.showHand && p.phase === "ready" && !covered && !popovers.length ? handPosition(spot, viewport.height) : null;
|
|
5360
5620
|
let arrowStyle;
|
|
5361
5621
|
if (pos && pos.side !== "dock") {
|
|
5362
5622
|
if (pos.side === "right")
|
|
@@ -5368,7 +5628,7 @@ function StepLayer(p) {
|
|
|
5368
5628
|
else
|
|
5369
5629
|
arrowStyle = { bottom: -7, left: pos.ax };
|
|
5370
5630
|
}
|
|
5371
|
-
return (jsxs(Fragment$1, { children: [jsx(Scrim, { holes: p.phase === "ready" ? holes : [], width: viewport.width, height: Math.max(viewport.height + viewport.offsetTop, 0) }), spot &&
|
|
5631
|
+
return (jsxs(Fragment$1, { children: [jsx(Scrim, { holes: p.phase === "ready" ? holes : [], width: viewport.width, height: Math.max(viewport.height + viewport.offsetTop, 0) }), spot && ringShown ? (jsx("div", { ref: ringRef, className: "eg-ring" + (p.moving ? " eg-move" : ""), style: { left: spot.x, top: spot.y, width: spot.width, height: spot.height }, "aria-hidden": "true" })) : null, hand ? (jsx("div", { className: "eg-hand" + (hand.flip ? " eg-flip" : ""), style: { left: hand.left, top: hand.top }, "aria-hidden": "true", children: jsx("svg", { viewBox: "0 0 24 24", children: jsx("path", { d: HAND_PATH }) }) })) : null, showBubble ? (jsxs("div", { ref: bubbleRef, className: "eg-bubble" + (p.bubble.error ? " eg-err" : ""), role: "dialog", "aria-modal": "false", "aria-labelledby": BUBBLE_TITLE_ID, "aria-describedby": BUBBLE_WHY_ID, tabIndex: -1, "data-eg-bubble": "", style: {
|
|
5372
5632
|
left: pos?.left ?? 0,
|
|
5373
5633
|
top: pos?.top ?? 0,
|
|
5374
5634
|
width: phone ? viewport.width - 32 : undefined,
|
|
@@ -5379,7 +5639,7 @@ function BubbleBody(p) {
|
|
|
5379
5639
|
const b = p.bubble;
|
|
5380
5640
|
const title = b.error ? b.error.title : b.title;
|
|
5381
5641
|
const why = b.error ? (b.error.detail ?? "") : b.why;
|
|
5382
|
-
return (jsxs(Fragment$1, { children: [jsxs("div", { className: "eg-b-top", children: [jsxs("span", { className: "eg-mis", children: ["\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08 ", b.missionNo, " \u00B7 ", b.missionTitle, b.replay ? " (ทบทวน)" : ""] }), jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", b.index + 1, "/", b.total] })] }), jsx("div", { className: "eg-pips", "aria-hidden": "true", children: Array.from({ length: b.total }, (_, i) => (jsx("i", { className: i < b.index ? "eg-d" : i === b.index ? "eg-c" : undefined }, i))) }), jsx("p", { className: "eg-b-t", id: BUBBLE_TITLE_ID, children: jsx(GuideTitle, { text: title }) }), jsx("p", { className: "eg-b-w", id: BUBBLE_WHY_ID, hidden: !why, children: why }), jsxs("div", { className: "eg-b-f", children: [jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: b.replay ? "จบการทบทวน" : "ออกจากไกด์" }), jsxs("span", { style: { display: "flex", gap: 6 }, children: [b.showEndWithoutSave ? (jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: p.onEndWithoutSave, children: "\u0E08\u0E1A\u0E42\u0E14\u0E22\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01" })) : null, b.showNext ? (jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onNext, children: "\u0E16\u0E31\u0E14\u0E44\u0E1B" })) : null] })] })] }));
|
|
5642
|
+
return (jsxs(Fragment$1, { children: [jsxs("div", { className: "eg-b-top", children: [jsxs("span", { className: "eg-mis", children: ["\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08 ", b.missionNo, " \u00B7 ", b.missionTitle, b.replay ? " (ทบทวน)" : ""] }), jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", b.index + 1, "/", b.total] })] }), jsx("div", { className: "eg-pips", "aria-hidden": "true", children: Array.from({ length: b.total }, (_, i) => (jsx("i", { className: i < b.index ? "eg-d" : i === b.index ? "eg-c" : undefined }, i))) }), jsx("p", { className: "eg-b-t", id: BUBBLE_TITLE_ID, children: jsx(GuideTitle, { text: title }) }), jsx("p", { className: "eg-b-w", id: BUBBLE_WHY_ID, hidden: !why, children: jsx(GuideTitle, { text: why }) }), jsxs("div", { className: "eg-b-f", children: [jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: b.replay ? "จบการทบทวน" : "ออกจากไกด์" }), jsxs("span", { style: { display: "flex", gap: 6 }, children: [b.showEndWithoutSave ? (jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: p.onEndWithoutSave, children: "\u0E08\u0E1A\u0E42\u0E14\u0E22\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01" })) : null, b.showNext ? (jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onNext, children: "\u0E16\u0E31\u0E14\u0E44\u0E1B" })) : null] })] })] }));
|
|
5383
5643
|
}
|
|
5384
5644
|
/** The fingertip at ≈62% across and down the target; upside down near the bottom. */
|
|
5385
5645
|
function handPosition(spot, viewportHeight) {
|
|
@@ -5492,6 +5752,9 @@ function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
|
5492
5752
|
meta = jsx("span", { children: "\u0E02\u0E49\u0E32\u0E21\u0E44\u0E27\u0E49" });
|
|
5493
5753
|
else if (v === "waiting" && gate)
|
|
5494
5754
|
meta = gate.pending;
|
|
5755
|
+
// Locked: what to do first gets its own line under the title, so the title
|
|
5756
|
+
// keeps the row's width and the button stays short.
|
|
5757
|
+
const lockHint = v === "locked" && item.lockedBy ? `ทำ ${item.lockedBy} ก่อน` : null;
|
|
5495
5758
|
let button;
|
|
5496
5759
|
if (v === "done")
|
|
5497
5760
|
button = jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onStart(item.key, true), children: "\u0E17\u0E33\u0E0B\u0E49\u0E33" });
|
|
@@ -5500,11 +5763,11 @@ function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
|
5500
5763
|
else if (v === "waiting" && gate)
|
|
5501
5764
|
button = (jsx("a", { className: "eg-btn eg-ghost", href: gate.href, style: { textDecoration: "none" }, children: gate.label }));
|
|
5502
5765
|
else if (v === "locked")
|
|
5503
|
-
button = (
|
|
5766
|
+
button = (jsx("button", { type: "button", className: "eg-btn eg-ghost", disabled: true, "aria-label": lockHint ? `เริ่ม (${lockHint})` : undefined, children: "\u0E40\u0E23\u0E34\u0E48\u0E21" }));
|
|
5504
5767
|
else
|
|
5505
5768
|
button = (jsx("button", { type: "button", className: "eg-btn " + (v === "next" ? "eg-primary" : "eg-ghost"), onClick: () => onStart(item.key, false), children: v === "skipped" || v === "active" ? "ทำต่อ" : "เริ่ม" }));
|
|
5506
5769
|
const dismissable = item.optional && (v === "todo" || v === "next");
|
|
5507
|
-
return (jsxs("li", { className: cls, children: [jsx("span", { className: "eg-n", children: ticked ? jsx(IconCheck, {}) : n }), jsxs("div", { className: "eg-t", children: [jsx("b", { children: item.title }), jsx("small", { className: v === "skipped" ? "eg-sk" : undefined, children: meta })] }), dismissable ? (jsxs("span", { className: "eg-q-acts", children: [button, jsx("button", { type: "button", className: "eg-btn eg-link", onClick: () => onDismiss(item.key), children: "\u0E44\u0E21\u0E48\u0E43\u0E0A\u0E49\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E19\u0E35\u0E49" })] })) : (button)] }));
|
|
5770
|
+
return (jsxs("li", { className: cls, children: [jsx("span", { className: "eg-n", children: ticked ? jsx(IconCheck, {}) : n }), jsxs("div", { className: "eg-t", children: [jsx("b", { children: item.title }), lockHint ? jsx("small", { className: "eg-lock", children: lockHint }) : null, jsx("small", { className: v === "skipped" ? "eg-sk" : undefined, children: meta })] }), dismissable ? (jsxs("span", { className: "eg-q-acts", children: [button, jsx("button", { type: "button", className: "eg-btn eg-link", onClick: () => onDismiss(item.key), children: "\u0E44\u0E21\u0E48\u0E43\u0E0A\u0E49\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E19\u0E35\u0E49" })] })) : (button)] }));
|
|
5508
5771
|
}
|
|
5509
5772
|
function GuideToast({ text }) {
|
|
5510
5773
|
return (jsx("div", { className: "eg-toast", role: "status", children: text }));
|
|
@@ -5692,6 +5955,9 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5692
5955
|
/* ---------------- machine ---------------- */
|
|
5693
5956
|
const [state, setState] = useState(null);
|
|
5694
5957
|
const stateRef = useRef(null);
|
|
5958
|
+
// What the server holds (payloadKey), so a save that would change nothing
|
|
5959
|
+
// is not sent and does not bump its rev.
|
|
5960
|
+
const savedKey = useRef(null);
|
|
5695
5961
|
useEffect(() => {
|
|
5696
5962
|
if (!answer || answer.audience === "none" || answer.newerOnServer) {
|
|
5697
5963
|
stateRef.current = null;
|
|
@@ -5700,6 +5966,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5700
5966
|
}
|
|
5701
5967
|
if (stateRef.current)
|
|
5702
5968
|
return;
|
|
5969
|
+
savedKey.current = answer.progress && answer.progress.rev > 0 ? payloadKey(answer.progress, suite.app) : null;
|
|
5703
5970
|
// Resume from whichever copy is further along: the server's, or this
|
|
5704
5971
|
// tab's session copy from before a refresh (its rev is the server's).
|
|
5705
5972
|
const mirror = readMirror(organizationId, suite);
|
|
@@ -5756,7 +6023,11 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5756
6023
|
const cur = stateRef.current;
|
|
5757
6024
|
if (!cur)
|
|
5758
6025
|
return;
|
|
6026
|
+
if (payloadKey(cur.progress, suite.app) === savedKey.current)
|
|
6027
|
+
continue;
|
|
5759
6028
|
const row = await saveGuides(organizationId, suite, cur.progress);
|
|
6029
|
+
if (row)
|
|
6030
|
+
savedKey.current = payloadKey(row, suite.app);
|
|
5760
6031
|
if (row && stateRef.current) {
|
|
5761
6032
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
5762
6033
|
dispatchRef.current({ type: "serverRow", progress: row });
|
|
@@ -5933,7 +6204,9 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5933
6204
|
scrolling, dialogs animating in, the drawer sliding, the keyboard. */
|
|
5934
6205
|
const [layout, setLayout] = useState({
|
|
5935
6206
|
spot: null,
|
|
6207
|
+
covered: false,
|
|
5936
6208
|
holes: [],
|
|
6209
|
+
popovers: [],
|
|
5937
6210
|
vw: 0,
|
|
5938
6211
|
vh: 0,
|
|
5939
6212
|
oy: 0,
|
|
@@ -5945,9 +6218,22 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5945
6218
|
}
|
|
5946
6219
|
let raf = 0;
|
|
5947
6220
|
let lostSince = null;
|
|
5948
|
-
let last = { spot: null, holes: [], vw: 0, vh: 0, oy: 0 };
|
|
6221
|
+
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0 };
|
|
5949
6222
|
targetElRef.current = target ? findAnchor(target.anchor, target.id) : null;
|
|
5950
6223
|
const allow = stepInfo?.step.allow ?? [];
|
|
6224
|
+
// The spotlight is the part of the target a person can see: cut by the
|
|
6225
|
+
// scroll boxes around it (a row of tabs that scrolls sideways) and the
|
|
6226
|
+
// viewport. Wholly out of view (scrolled away up or down), its own box,
|
|
6227
|
+
// and the tour scrolls it back.
|
|
6228
|
+
let clipFor = null;
|
|
6229
|
+
let clips = [];
|
|
6230
|
+
const spotOf = (el) => {
|
|
6231
|
+
if (el !== clipFor) {
|
|
6232
|
+
clipFor = el;
|
|
6233
|
+
clips = clipAncestors(el);
|
|
6234
|
+
}
|
|
6235
|
+
return padRect(visibleRectOf(el, clips) ?? rectOf(el), TARGET_PAD);
|
|
6236
|
+
};
|
|
5951
6237
|
const tick = () => {
|
|
5952
6238
|
raf = requestAnimationFrame(tick);
|
|
5953
6239
|
const vv = window.visualViewport;
|
|
@@ -5962,14 +6248,14 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5962
6248
|
let spot = null;
|
|
5963
6249
|
if (target && el && isPresent(el)) {
|
|
5964
6250
|
lostSince = null;
|
|
5965
|
-
spot =
|
|
6251
|
+
spot = spotOf(el);
|
|
5966
6252
|
}
|
|
5967
6253
|
else if (target) {
|
|
5968
6254
|
// Maybe re-rendered under another node.
|
|
5969
6255
|
const again = findAnchor(target.anchor, target.id);
|
|
5970
6256
|
if (again) {
|
|
5971
6257
|
targetElRef.current = again;
|
|
5972
|
-
spot =
|
|
6258
|
+
spot = spotOf(again);
|
|
5973
6259
|
lostSince = null;
|
|
5974
6260
|
}
|
|
5975
6261
|
else if (phase === "ready") {
|
|
@@ -5982,21 +6268,25 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5982
6268
|
}
|
|
5983
6269
|
if (phase === "waiting" && spot)
|
|
5984
6270
|
dispatchRef.current({ type: "sync" });
|
|
5985
|
-
const
|
|
6271
|
+
const popovers = [];
|
|
5986
6272
|
for (const p of openPoppers())
|
|
5987
|
-
|
|
6273
|
+
popovers.push(padRect(rectOf(p), 2));
|
|
6274
|
+
const holes = [];
|
|
5988
6275
|
for (const a of allow) {
|
|
5989
6276
|
const ael = findAnchor(a);
|
|
5990
6277
|
if (ael)
|
|
5991
6278
|
holes.push(padRect(rectOf(ael), TARGET_PAD));
|
|
5992
6279
|
}
|
|
5993
|
-
const
|
|
6280
|
+
const covered = !!spot && coveredByDialog(targetElRef.current);
|
|
6281
|
+
const next = { spot, covered, holes, popovers, vw, vh, oy };
|
|
6282
|
+
const sameList = (a, b) => a.length === b.length && a.every((h, i) => sameRect(h, b[i]));
|
|
5994
6283
|
if (!sameRect(next.spot, last.spot) ||
|
|
6284
|
+
next.covered !== last.covered ||
|
|
5995
6285
|
next.vw !== last.vw ||
|
|
5996
6286
|
next.vh !== last.vh ||
|
|
5997
6287
|
next.oy !== last.oy ||
|
|
5998
|
-
next.holes
|
|
5999
|
-
next.
|
|
6288
|
+
!sameList(next.holes, last.holes) ||
|
|
6289
|
+
!sameList(next.popovers, last.popovers)) {
|
|
6000
6290
|
last = next;
|
|
6001
6291
|
setLayout(next);
|
|
6002
6292
|
}
|
|
@@ -6380,7 +6670,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6380
6670
|
const { mission, step, missionNo } = stepInfo;
|
|
6381
6671
|
const kind = stepKind(step);
|
|
6382
6672
|
const bridge = screen.bridge;
|
|
6383
|
-
content = (jsx(StepLayer, { spot: layout.spot, holes: layout.holes, viewport: { width: layout.vw || window.innerWidth, height: layout.vh || window.innerHeight, offsetTop: layout.oy }, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
|
|
6673
|
+
content = (jsx(StepLayer, { spot: layout.spot, holes: layout.holes, popovers: layout.popovers, covered: layout.covered, viewport: { width: layout.vw || window.innerWidth, height: layout.vh || window.innerHeight, offsetTop: layout.oy }, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
|
|
6384
6674
|
missionNo,
|
|
6385
6675
|
missionTitle: mission.title,
|
|
6386
6676
|
replay: screen.replay,
|