@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.js
CHANGED
|
@@ -1975,7 +1975,7 @@ function platformApiBase(app) {
|
|
|
1975
1975
|
* This package's version, attached to help messages so the back office can
|
|
1976
1976
|
* tell which shell a report came from. Keep in step with package.json.
|
|
1977
1977
|
*/
|
|
1978
|
-
const SHELL_VERSION = "0.1.
|
|
1978
|
+
const SHELL_VERSION = "0.1.64";
|
|
1979
1979
|
|
|
1980
1980
|
const HELP_APP_LABEL = {
|
|
1981
1981
|
platform: "แพลตฟอร์ม",
|
|
@@ -4052,7 +4052,7 @@ function announceStep(d, suite, env) {
|
|
|
4052
4052
|
const step = mission.steps[s.index];
|
|
4053
4053
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
4054
4054
|
const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
|
|
4055
|
-
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : resolveText(step.why, ctx);
|
|
4055
|
+
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
|
|
4056
4056
|
d.effects.push({
|
|
4057
4057
|
type: "announce",
|
|
4058
4058
|
text: `ขั้นที่ ${s.index + 1} จาก ${mission.steps.length}. ${title}${why ? `. ${why}` : ""}`,
|
|
@@ -4214,6 +4214,7 @@ function startMission(d, suite, env, key, replay) {
|
|
|
4214
4214
|
const entry = d.state.progress.missions[key];
|
|
4215
4215
|
// A mission already done is only ever replayed; its done status stays.
|
|
4216
4216
|
const isReplay = replay || entry?.status === "done";
|
|
4217
|
+
const before = d.state.progress;
|
|
4217
4218
|
d.state = { ...d.state, panelOpen: false, justDone: null };
|
|
4218
4219
|
if (d.state.progress.welcome === "not_seen")
|
|
4219
4220
|
withProgress(d, { welcome: "started" });
|
|
@@ -4231,7 +4232,9 @@ function startMission(d, suite, env, key, replay) {
|
|
|
4231
4232
|
index = checkpointAtOrBefore(mission.steps, at);
|
|
4232
4233
|
}
|
|
4233
4234
|
enterStep(d, suite, env, mission, index, { replay: isReplay });
|
|
4234
|
-
|
|
4235
|
+
// A replay never writes progress; only what starting it changed (the
|
|
4236
|
+
// welcome, first seen through a replay) is saved.
|
|
4237
|
+
if (isReplay && d.state.progress !== before)
|
|
4235
4238
|
save(d);
|
|
4236
4239
|
}
|
|
4237
4240
|
function exitMission(d) {
|
|
@@ -4805,6 +4808,27 @@ function putBody(p, app) {
|
|
|
4805
4808
|
TERMINAL.includes(p.server_status),
|
|
4806
4809
|
};
|
|
4807
4810
|
}
|
|
4811
|
+
function stable(v) {
|
|
4812
|
+
if (Array.isArray(v))
|
|
4813
|
+
return `[${v.map(stable).join(",")}]`;
|
|
4814
|
+
if (isRecord(v)) {
|
|
4815
|
+
return `{${Object.keys(v)
|
|
4816
|
+
.sort()
|
|
4817
|
+
.map(k => `${JSON.stringify(k)}:${stable(v[k])}`)
|
|
4818
|
+
.join(",")}}`;
|
|
4819
|
+
}
|
|
4820
|
+
return JSON.stringify(v) ?? "null";
|
|
4821
|
+
}
|
|
4822
|
+
/**
|
|
4823
|
+
* What a PUT would store, without the rev it is checked against: two equal
|
|
4824
|
+
* keys mean the save would change nothing, and it is not sent (a replay
|
|
4825
|
+
* starting, a step entered again, never bump the row's rev). Key order does
|
|
4826
|
+
* not matter.
|
|
4827
|
+
*/
|
|
4828
|
+
function payloadKey(p, app) {
|
|
4829
|
+
const { rev: _rev, restart: _restart, ...rest } = putBody(p, app);
|
|
4830
|
+
return stable(rest);
|
|
4831
|
+
}
|
|
4808
4832
|
const MISSION_RANK = { todo: 0, skipped: 1, active: 1, done: 3 };
|
|
4809
4833
|
const WELCOME_RANK = { not_seen: 0, skipped: 1, started: 1 };
|
|
4810
4834
|
/**
|
|
@@ -4939,8 +4963,9 @@ function isPresent(el) {
|
|
|
4939
4963
|
if (r.width <= 0 || r.height <= 0)
|
|
4940
4964
|
return false;
|
|
4941
4965
|
// Slid off the side (a closed drawer); off the top or bottom is only
|
|
4942
|
-
// scrolled away, and the tour scrolls it back.
|
|
4943
|
-
|
|
4966
|
+
// scrolled away, and the tour scrolls it back. So is a tab scrolled out of
|
|
4967
|
+
// a row of tabs that scrolls sideways, as long as the row is on screen.
|
|
4968
|
+
if ((r.right <= 0 || r.left >= window.innerWidth) && !inSidewaysScroller(el))
|
|
4944
4969
|
return false;
|
|
4945
4970
|
if (el.getAttribute("data-state") === "closed")
|
|
4946
4971
|
return false;
|
|
@@ -5020,6 +5045,26 @@ function sameRect(a, b) {
|
|
|
5020
5045
|
const POPPER_SELECTOR = "[data-radix-popper-content-wrapper]";
|
|
5021
5046
|
/** Toasts stay usable above the tour. */
|
|
5022
5047
|
const TOASTER_SELECTOR = "[data-sonner-toaster]";
|
|
5048
|
+
/** Open dialogs (Radix dialog, alert dialog, sheet, popover content). */
|
|
5049
|
+
const OPEN_DIALOG_SELECTOR = '[role="dialog"][data-state="open"],[role="alertdialog"][data-state="open"]';
|
|
5050
|
+
/**
|
|
5051
|
+
* Whether an open dialog of the app's covers the target: the target is
|
|
5052
|
+
* neither in it nor around it (a confirm opened by the step's own button).
|
|
5053
|
+
* The ring and the hand would be drawn over the dialog, pointing at a button
|
|
5054
|
+
* behind it.
|
|
5055
|
+
*/
|
|
5056
|
+
function coveredByDialog(el) {
|
|
5057
|
+
if (!hasDocument() || !el)
|
|
5058
|
+
return false;
|
|
5059
|
+
for (const d of Array.from(document.querySelectorAll(OPEN_DIALOG_SELECTOR))) {
|
|
5060
|
+
if (inGuideLayer(d) || !isPresent(d))
|
|
5061
|
+
continue;
|
|
5062
|
+
if (d.contains(el) || el.contains(d))
|
|
5063
|
+
continue;
|
|
5064
|
+
return true;
|
|
5065
|
+
}
|
|
5066
|
+
return false;
|
|
5067
|
+
}
|
|
5023
5068
|
function openPoppers() {
|
|
5024
5069
|
if (!hasDocument())
|
|
5025
5070
|
return [];
|
|
@@ -5037,18 +5082,181 @@ function prefersReducedMotion() {
|
|
|
5037
5082
|
function isPhoneWidth() {
|
|
5038
5083
|
return typeof window !== "undefined" && window.innerWidth < 768;
|
|
5039
5084
|
}
|
|
5040
|
-
|
|
5085
|
+
/* ---------------- what is really on screen ---------------- */
|
|
5086
|
+
const INFINITE = 1e7;
|
|
5087
|
+
/** The overlap of two rectangles, or null when they do not meet. */
|
|
5088
|
+
function intersectRect(a, b) {
|
|
5089
|
+
const x = Math.max(a.x, b.x);
|
|
5090
|
+
const y = Math.max(a.y, b.y);
|
|
5091
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
5092
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
5093
|
+
if (right - x <= 0 || bottom - y <= 0)
|
|
5094
|
+
return null;
|
|
5095
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
5096
|
+
}
|
|
5097
|
+
/** The part of `rect` inside every clip (scroll boxes, the viewport); null when none is. */
|
|
5098
|
+
function visibleRect(rect, clips) {
|
|
5099
|
+
let r = rect;
|
|
5100
|
+
for (const c of clips) {
|
|
5101
|
+
if (!r)
|
|
5102
|
+
return null;
|
|
5103
|
+
r = intersectRect(r, c);
|
|
5104
|
+
}
|
|
5105
|
+
return r;
|
|
5106
|
+
}
|
|
5107
|
+
const clipsX = (v) => v !== "visible";
|
|
5108
|
+
const scrollsX = (v) => v === "auto" || v === "scroll";
|
|
5109
|
+
/**
|
|
5110
|
+
* The ancestors that cut the element off: anything with overflow other than
|
|
5111
|
+
* visible on an axis. The body and the root scroll the page and are covered by
|
|
5112
|
+
* the viewport. Read once per target; their rectangles are read every frame.
|
|
5113
|
+
*/
|
|
5114
|
+
function clipAncestors(el) {
|
|
5115
|
+
const out = [];
|
|
5116
|
+
if (typeof getComputedStyle !== "function")
|
|
5117
|
+
return out;
|
|
5118
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
5119
|
+
if (n.hasAttribute(GUIDE_LAYER_ATTRIBUTE))
|
|
5120
|
+
break;
|
|
5121
|
+
const s = getComputedStyle(n);
|
|
5122
|
+
const x = clipsX(s.overflowX);
|
|
5123
|
+
const y = clipsX(s.overflowY);
|
|
5124
|
+
if (x || y)
|
|
5125
|
+
out.push({ el: n, x, y });
|
|
5126
|
+
if (s.position === "fixed")
|
|
5127
|
+
break;
|
|
5128
|
+
}
|
|
5129
|
+
return out;
|
|
5130
|
+
}
|
|
5131
|
+
/** An ancestor's clip as a rectangle: unbounded on an axis it does not clip. */
|
|
5132
|
+
function clipRectOf(c) {
|
|
5133
|
+
const r = c.el.getBoundingClientRect();
|
|
5134
|
+
return {
|
|
5135
|
+
x: c.x ? r.left : -INFINITE,
|
|
5136
|
+
y: c.y ? r.top : -INFINITE,
|
|
5137
|
+
width: c.x ? r.width : INFINITE * 2,
|
|
5138
|
+
height: c.y ? r.height : INFINITE * 2,
|
|
5139
|
+
};
|
|
5140
|
+
}
|
|
5141
|
+
function viewportRect() {
|
|
5142
|
+
const vv = typeof window !== "undefined" ? window.visualViewport : undefined;
|
|
5143
|
+
return {
|
|
5144
|
+
x: 0,
|
|
5145
|
+
y: vv ? vv.offsetTop : 0,
|
|
5146
|
+
width: window.innerWidth,
|
|
5147
|
+
height: vv ? vv.height : window.innerHeight,
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
5150
|
+
/**
|
|
5151
|
+
* The part of the element a person can see: its box cut by every scroll box
|
|
5152
|
+
* around it and by the viewport. The spotlight, its hole, the ring and the
|
|
5153
|
+
* hand use this, so a tab half out of a sideways-scrolling row of tabs is not
|
|
5154
|
+
* spotlit where it cannot be pressed. Null when none of it is on screen.
|
|
5155
|
+
*/
|
|
5156
|
+
function visibleRectOf(el, ancestors = clipAncestors(el)) {
|
|
5157
|
+
return visibleRect(rectOf(el), [...ancestors.map(clipRectOf), viewportRect()]);
|
|
5158
|
+
}
|
|
5159
|
+
/** Inside a box that scrolls sideways (with somewhere to scroll) that is itself on screen. */
|
|
5160
|
+
function inSidewaysScroller(el) {
|
|
5161
|
+
if (typeof getComputedStyle !== "function")
|
|
5162
|
+
return false;
|
|
5163
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
5164
|
+
const s = getComputedStyle(n);
|
|
5165
|
+
if (scrollsX(s.overflowX) && n.scrollWidth > n.clientWidth + 1) {
|
|
5166
|
+
const r = n.getBoundingClientRect();
|
|
5167
|
+
return r.width > 0 && r.right > 0 && r.left < window.innerWidth;
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
return false;
|
|
5171
|
+
}
|
|
5172
|
+
/**
|
|
5173
|
+
* How to bring a target into view, or null when it already is: to the middle
|
|
5174
|
+
* when it is above or below the comfortable band (under the header, at the
|
|
5175
|
+
* bottom edge), the nearest edge when only a side is cut off (a tab out of a
|
|
5176
|
+
* sideways-scrolling row, on a phone).
|
|
5177
|
+
*/
|
|
5178
|
+
function revealPlan(rect, visible, viewportHeight) {
|
|
5179
|
+
const offVertically = rect.y < 70 || rect.y + rect.height > viewportHeight - 20;
|
|
5180
|
+
const cutSideways = !visible || visible.width < rect.width - 1;
|
|
5181
|
+
if (!offVertically && !cutSideways)
|
|
5182
|
+
return null;
|
|
5183
|
+
return { block: offVertically ? "center" : "nearest", inline: "nearest" };
|
|
5184
|
+
}
|
|
5185
|
+
/** Scrolls the target into view (instant under reduced motion). */
|
|
5041
5186
|
function revealTarget(el) {
|
|
5042
|
-
const r = el.getBoundingClientRect();
|
|
5043
5187
|
const vh = window.visualViewport?.height ?? window.innerHeight;
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5188
|
+
const plan = revealPlan(rectOf(el), visibleRectOf(el), vh);
|
|
5189
|
+
if (!plan)
|
|
5190
|
+
return;
|
|
5191
|
+
try {
|
|
5192
|
+
el.scrollIntoView({ ...plan, behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
5193
|
+
}
|
|
5194
|
+
catch {
|
|
5195
|
+
el.scrollIntoView();
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
/* ---------------- the scrim's holes, as one union ---------------- */
|
|
5199
|
+
/**
|
|
5200
|
+
* The scrim's blocking area: the screen (with a 10 px margin) minus the union
|
|
5201
|
+
* of the holes, as rectangles that do not overlap. Cutting each hole out with
|
|
5202
|
+
* an even-odd fill made the place where two holes overlap (a field and the
|
|
5203
|
+
* select list open over it) solid again, and the option there could not be
|
|
5204
|
+
* pressed.
|
|
5205
|
+
*
|
|
5206
|
+
* Every hole edge splits the screen into a grid; a cell is blocked when no
|
|
5207
|
+
* hole covers it. Cells in a row are joined, and identical rows stacked.
|
|
5208
|
+
*/
|
|
5209
|
+
function blockedRects(holes, width, height) {
|
|
5210
|
+
const x0 = -10;
|
|
5211
|
+
const y0 = -10;
|
|
5212
|
+
const x1 = width + 10;
|
|
5213
|
+
const y1 = height + 10;
|
|
5214
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
5215
|
+
const hs = holes
|
|
5216
|
+
.filter(h => h.width > 0 && h.height > 0)
|
|
5217
|
+
.map(h => ({
|
|
5218
|
+
l: clamp(h.x, x0, x1),
|
|
5219
|
+
t: clamp(h.y, y0, y1),
|
|
5220
|
+
r: clamp(h.x + h.width, x0, x1),
|
|
5221
|
+
b: clamp(h.y + h.height, y0, y1),
|
|
5222
|
+
}))
|
|
5223
|
+
.filter(h => h.r > h.l && h.b > h.t);
|
|
5224
|
+
if (!hs.length)
|
|
5225
|
+
return [{ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }];
|
|
5226
|
+
const uniq = (v) => Array.from(new Set(v)).sort((a, b) => a - b);
|
|
5227
|
+
const xs = uniq([x0, x1, ...hs.flatMap(h => [h.l, h.r])]);
|
|
5228
|
+
const ys = uniq([y0, y1, ...hs.flatMap(h => [h.t, h.b])]);
|
|
5229
|
+
const out = [];
|
|
5230
|
+
let prev = null;
|
|
5231
|
+
for (let j = 0; j < ys.length - 1; j++) {
|
|
5232
|
+
const top = ys[j];
|
|
5233
|
+
const bottom = ys[j + 1];
|
|
5234
|
+
const cy = (top + bottom) / 2;
|
|
5235
|
+
const row = [];
|
|
5236
|
+
for (let i = 0; i < xs.length - 1; i++) {
|
|
5237
|
+
const left = xs[i];
|
|
5238
|
+
const right = xs[i + 1];
|
|
5239
|
+
const cx = (left + right) / 2;
|
|
5240
|
+
const covered = hs.some(h => cx > h.l && cx < h.r && cy > h.t && cy < h.b);
|
|
5241
|
+
if (covered)
|
|
5242
|
+
continue;
|
|
5243
|
+
const last = row[row.length - 1];
|
|
5244
|
+
if (last && last.x + last.width === left)
|
|
5245
|
+
last.width = right - last.x;
|
|
5246
|
+
else
|
|
5247
|
+
row.push({ x: left, y: top, width: right - left, height: bottom - top });
|
|
5047
5248
|
}
|
|
5048
|
-
|
|
5049
|
-
|
|
5249
|
+
const key = row.map(r => `${r.x},${r.width}`).join("|");
|
|
5250
|
+
if (prev && prev.key === key) {
|
|
5251
|
+
for (const r of prev.rects)
|
|
5252
|
+
r.height += bottom - top;
|
|
5253
|
+
}
|
|
5254
|
+
else {
|
|
5255
|
+
out.push(...row);
|
|
5256
|
+
prev = { key, rects: row };
|
|
5050
5257
|
}
|
|
5051
5258
|
}
|
|
5259
|
+
return out;
|
|
5052
5260
|
}
|
|
5053
5261
|
|
|
5054
5262
|
/**
|
|
@@ -5070,7 +5278,8 @@ const GUIDE_CSS = `
|
|
|
5070
5278
|
.eg-i{pointer-events:auto}
|
|
5071
5279
|
.eg-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
5072
5280
|
.eg-scrim-svg{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}
|
|
5073
|
-
.eg-scrim-svg
|
|
5281
|
+
.eg-scrim-svg .eg-scrim-fill{fill:var(--eg-scrim);pointer-events:none}
|
|
5282
|
+
.eg-scrim-svg .eg-scrim-hit{fill:transparent;pointer-events:all}
|
|
5074
5283
|
.eg-scrim{position:fixed;inset:0;background:var(--eg-scrim);pointer-events:auto}
|
|
5075
5284
|
.eg-scrim.eg-light{background:rgba(12,20,34,.45)}
|
|
5076
5285
|
.eg-scrim.eg-faint{background:rgba(12,20,34,.35)}
|
|
@@ -5179,6 +5388,7 @@ const GUIDE_CSS = `
|
|
|
5179
5388
|
.eg-q-item.eg-done .eg-t b{color:var(--eg-muted)}
|
|
5180
5389
|
.eg-q-item .eg-t small{display:block;font-size:12px;color:var(--eg-muted)}
|
|
5181
5390
|
.eg-q-item .eg-t small.eg-sk{color:#b06a00}
|
|
5391
|
+
.eg-q-item .eg-t small.eg-lock{color:var(--eg-fg);font-weight:500}
|
|
5182
5392
|
.eg-q-item .eg-btn{height:32px;padding:0 12px;font-size:13px}
|
|
5183
5393
|
.eg-q-acts{display:flex;flex-direction:column;align-items:flex-end;gap:2px}
|
|
5184
5394
|
.eg-q-acts .eg-link{height:22px;font-size:12px;padding:0 2px}
|
|
@@ -5243,15 +5453,51 @@ function roundRect(r, radius) {
|
|
|
5243
5453
|
`H${x + rad}A${rad} ${rad} 0 0 1 ${x} ${y + h - rad}` +
|
|
5244
5454
|
`V${y + rad}A${rad} ${rad} 0 0 1 ${x + rad} ${y}Z`);
|
|
5245
5455
|
}
|
|
5456
|
+
const rectPath = (r) => `M${r.x} ${r.y}H${r.x + r.width}V${r.y + r.height}H${r.x}Z`;
|
|
5246
5457
|
/**
|
|
5247
|
-
* The dim layer that eats every press outside the holes.
|
|
5248
|
-
*
|
|
5249
|
-
*
|
|
5458
|
+
* The dim layer that eats every press outside the holes. The holes are a
|
|
5459
|
+
* union: where two overlap (a field and the select list open over it) the
|
|
5460
|
+
* page stays open.
|
|
5461
|
+
*
|
|
5462
|
+
* - The look: one rectangle masked by the rounded holes (a mask adds holes
|
|
5463
|
+
* up, so an overlap is not filled back in).
|
|
5464
|
+
* - The presses: a transparent path made of the screen minus the holes, as
|
|
5465
|
+
* rectangles that do not overlap (blockedRects). A press inside a hole
|
|
5466
|
+
* reaches the page underneath; any other lands on the path (marked
|
|
5467
|
+
* data-eg-block) and is cancelled.
|
|
5250
5468
|
*/
|
|
5251
5469
|
function Scrim({ holes, width, height }) {
|
|
5252
|
-
const
|
|
5253
|
-
const
|
|
5254
|
-
return (jsxRuntime.
|
|
5470
|
+
const maskId = "eg-scrim-mask-" + React__namespace.useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
5471
|
+
const block = blockedRects(holes, width, height).map(rectPath).join("");
|
|
5472
|
+
return (jsxRuntime.jsxs("svg", { className: "eg-scrim-svg", width: width, height: height, "aria-hidden": "true", children: [holes.length ? (jsxRuntime.jsx("defs", { children: jsxRuntime.jsxs("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: -10, y: -10, width: width + 20, height: height + 20, children: [jsxRuntime.jsx("rect", { x: -10, y: -10, width: width + 20, height: height + 20, fill: "#fff" }), holes.map((h, i) => (jsxRuntime.jsx("path", { d: roundRect(h, 12), fill: "#000" }, i)))] }) })) : null, jsxRuntime.jsx("rect", { className: "eg-scrim-fill", x: -10, y: -10, width: width + 20, height: height + 20, mask: holes.length ? `url(#${maskId})` : undefined }), jsxRuntime.jsx("path", { className: "eg-scrim-hit", d: block, "data-eg-block": "" })] }));
|
|
5473
|
+
}
|
|
5474
|
+
/** Whether two rectangles overlap. */
|
|
5475
|
+
const meets = (a, b) => !!intersectRect(a, b);
|
|
5476
|
+
/** The smallest rectangle around all of them. */
|
|
5477
|
+
function boundingRect(rs) {
|
|
5478
|
+
const x = Math.min(...rs.map(r => r.x));
|
|
5479
|
+
const y = Math.min(...rs.map(r => r.y));
|
|
5480
|
+
const right = Math.max(...rs.map(r => r.x + r.width));
|
|
5481
|
+
const bottom = Math.max(...rs.map(r => r.y + r.height));
|
|
5482
|
+
return { x, y, width: right - x, height: bottom - y };
|
|
5483
|
+
}
|
|
5484
|
+
/** Where the bubble may go, in order of preference. */
|
|
5485
|
+
const BUBBLE_PLACEMENTS = ["right", "bottom", "top", "left"];
|
|
5486
|
+
/**
|
|
5487
|
+
* The first placement (in BUBBLE_PLACEMENTS order) whose bubble is on screen,
|
|
5488
|
+
* off the target (with its open list), and off everything to keep clear;
|
|
5489
|
+
* failing that, the first that is only on screen and off the target; -1 when
|
|
5490
|
+
* none is.
|
|
5491
|
+
*/
|
|
5492
|
+
function pickPlacement(results, bw, bh, anchor, clear, bounds) {
|
|
5493
|
+
const box = (r) => ({ x: r.x, y: r.y, width: bw, height: bh });
|
|
5494
|
+
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;
|
|
5495
|
+
const ok = results.map(r => {
|
|
5496
|
+
const b = box(r);
|
|
5497
|
+
return fits(b) && !meets(b, anchor);
|
|
5498
|
+
});
|
|
5499
|
+
const best = results.findIndex((r, i) => ok[i] && !clear.some(c => meets(box(r), c)));
|
|
5500
|
+
return best >= 0 ? best : ok.indexOf(true);
|
|
5255
5501
|
}
|
|
5256
5502
|
const BUBBLE_TITLE_ID = "eg-bubble-title";
|
|
5257
5503
|
const BUBBLE_WHY_ID = "eg-bubble-why";
|
|
@@ -5291,12 +5537,21 @@ function StepLayer(p) {
|
|
|
5291
5537
|
const gap = p.showHand ? 34 : 14;
|
|
5292
5538
|
const spotKey = spot ? `${spot.x}|${spot.y}|${spot.width}|${spot.height}` : "";
|
|
5293
5539
|
const contentKey = `${p.bubble.title}|${p.bubble.why}|${p.bubble.error?.title ?? ""}|${p.bubble.showNext}`;
|
|
5540
|
+
// What the bubble sits beside: the target, with an open list over it (the
|
|
5541
|
+
// list is what the person is choosing from now). What it must not cover:
|
|
5542
|
+
// open lists, and the other anchors the step allows (a map step's
|
|
5543
|
+
// coordinate fields).
|
|
5544
|
+
const popoverList = p.popovers ?? [];
|
|
5545
|
+
const anchor = spot ? boundingRect([spot, ...popoverList.filter(o => meets(o, spot))]) : null;
|
|
5546
|
+
const clear = [...popoverList, ...p.holes];
|
|
5547
|
+
const anchorKey = anchor ? `${anchor.x}|${anchor.y}|${anchor.width}|${anchor.height}` : "";
|
|
5548
|
+
const clearKey = clear.map(r => `${r.x},${r.y},${r.width},${r.height}`).join(";");
|
|
5294
5549
|
React.useLayoutEffect(() => {
|
|
5295
5550
|
const el = bubbleRef.current;
|
|
5296
5551
|
if (!el || !showBubble)
|
|
5297
5552
|
return;
|
|
5298
5553
|
const m = 16;
|
|
5299
|
-
if (!spot) {
|
|
5554
|
+
if (!spot || !anchor) {
|
|
5300
5555
|
// Nothing to point at: the middle of the screen (the bottom on a phone).
|
|
5301
5556
|
const bw = el.offsetWidth;
|
|
5302
5557
|
const bh = el.offsetHeight;
|
|
@@ -5307,13 +5562,13 @@ function StepLayer(p) {
|
|
|
5307
5562
|
}
|
|
5308
5563
|
if (phone) {
|
|
5309
5564
|
// Docked to the top or the bottom of the screen, whichever covers less
|
|
5310
|
-
// of the target (target in the lower half: top).
|
|
5565
|
+
// of the target and its open list (target in the lower half: top).
|
|
5311
5566
|
const vh = viewport.height;
|
|
5312
5567
|
const oy = viewport.offsetTop;
|
|
5313
5568
|
const bh = el.offsetHeight;
|
|
5314
5569
|
const topT = oy + m + 4;
|
|
5315
5570
|
const botT = oy + vh - bh - m;
|
|
5316
|
-
const { y, height: h } =
|
|
5571
|
+
const { y, height: h } = anchor;
|
|
5317
5572
|
const overlapTop = Math.max(0, topT + bh + 8 - y);
|
|
5318
5573
|
const overlapBot = Math.max(0, y + h + gap - botT);
|
|
5319
5574
|
const top = y + h / 2 > vh / 2
|
|
@@ -5327,40 +5582,42 @@ function StepLayer(p) {
|
|
|
5327
5582
|
return;
|
|
5328
5583
|
}
|
|
5329
5584
|
let cancelled = false;
|
|
5585
|
+
const a = anchor;
|
|
5330
5586
|
const ref = {
|
|
5331
5587
|
getBoundingClientRect: () => ({
|
|
5332
|
-
x:
|
|
5333
|
-
y:
|
|
5334
|
-
left:
|
|
5335
|
-
top:
|
|
5336
|
-
width:
|
|
5337
|
-
height:
|
|
5338
|
-
right:
|
|
5339
|
-
bottom:
|
|
5588
|
+
x: a.x,
|
|
5589
|
+
y: a.y,
|
|
5590
|
+
left: a.x,
|
|
5591
|
+
top: a.y,
|
|
5592
|
+
width: a.width,
|
|
5593
|
+
height: a.height,
|
|
5594
|
+
right: a.x + a.width,
|
|
5595
|
+
bottom: a.y + a.height,
|
|
5340
5596
|
}),
|
|
5341
5597
|
};
|
|
5342
|
-
dom.computePosition(ref, el, {
|
|
5598
|
+
const at = (placement) => dom.computePosition(ref, el, {
|
|
5343
5599
|
strategy: "fixed",
|
|
5344
|
-
placement
|
|
5600
|
+
placement,
|
|
5345
5601
|
middleware: [
|
|
5346
|
-
dom.offset(
|
|
5347
|
-
dom.flip({ fallbackPlacements: ["bottom", "top", "left"], padding: m }),
|
|
5602
|
+
dom.offset(placement.startsWith("bottom") ? gap : 14),
|
|
5348
5603
|
dom.shift({ padding: m }),
|
|
5349
5604
|
...(arrowRef.current ? [dom.arrow({ element: arrowRef.current, padding: 14 })] : []),
|
|
5350
5605
|
],
|
|
5351
|
-
})
|
|
5352
|
-
|
|
5606
|
+
});
|
|
5607
|
+
Promise.all(BUBBLE_PLACEMENTS.map(at))
|
|
5608
|
+
.then(results => {
|
|
5353
5609
|
if (cancelled)
|
|
5354
5610
|
return;
|
|
5355
5611
|
const bw = el.offsetWidth;
|
|
5356
5612
|
const bh = el.offsetHeight;
|
|
5357
|
-
const
|
|
5358
|
-
const
|
|
5359
|
-
if (
|
|
5613
|
+
const bounds = { x: m, y: viewport.offsetTop + m, width: window.innerWidth - 2 * m, height: viewport.height - 2 * m };
|
|
5614
|
+
const i = pickPlacement(results, bw, bh, a, clear, bounds);
|
|
5615
|
+
if (i < 0) {
|
|
5360
5616
|
// Nowhere beside it: the bottom-right corner.
|
|
5361
5617
|
setPos({ left: window.innerWidth - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
|
|
5362
5618
|
return;
|
|
5363
5619
|
}
|
|
5620
|
+
const { x, y, placement, middlewareData } = results[i];
|
|
5364
5621
|
const side = placement.split("-")[0];
|
|
5365
5622
|
setPos({ left: x, top: y, side, ax: middlewareData.arrow?.x, ay: middlewareData.arrow?.y });
|
|
5366
5623
|
})
|
|
@@ -5371,11 +5628,14 @@ function StepLayer(p) {
|
|
|
5371
5628
|
return () => {
|
|
5372
5629
|
cancelled = true;
|
|
5373
5630
|
};
|
|
5374
|
-
//
|
|
5631
|
+
// The keys stand for spot, the lists, the holes and the bubble's size.
|
|
5375
5632
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5376
|
-
}, [spotKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
5377
|
-
const
|
|
5378
|
-
const
|
|
5633
|
+
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
5634
|
+
const popovers = p.popovers ?? [];
|
|
5635
|
+
const covered = !!p.covered;
|
|
5636
|
+
const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
|
|
5637
|
+
const ringShown = !!spot && p.phase === "ready" && !covered && !popovers.some(o => meets(o, spot));
|
|
5638
|
+
const hand = spot && p.showHand && p.phase === "ready" && !covered && !popovers.length ? handPosition(spot, viewport.height) : null;
|
|
5379
5639
|
let arrowStyle;
|
|
5380
5640
|
if (pos && pos.side !== "dock") {
|
|
5381
5641
|
if (pos.side === "right")
|
|
@@ -5387,7 +5647,7 @@ function StepLayer(p) {
|
|
|
5387
5647
|
else
|
|
5388
5648
|
arrowStyle = { bottom: -7, left: pos.ax };
|
|
5389
5649
|
}
|
|
5390
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Scrim, { holes: p.phase === "ready" ? holes : [], width: viewport.width, height: Math.max(viewport.height + viewport.offsetTop, 0) }), spot &&
|
|
5650
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(Scrim, { holes: p.phase === "ready" ? holes : [], width: viewport.width, height: Math.max(viewport.height + viewport.offsetTop, 0) }), spot && ringShown ? (jsxRuntime.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 ? (jsxRuntime.jsx("div", { className: "eg-hand" + (hand.flip ? " eg-flip" : ""), style: { left: hand.left, top: hand.top }, "aria-hidden": "true", children: jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { d: HAND_PATH }) }) })) : null, showBubble ? (jsxRuntime.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: {
|
|
5391
5651
|
left: pos?.left ?? 0,
|
|
5392
5652
|
top: pos?.top ?? 0,
|
|
5393
5653
|
width: phone ? viewport.width - 32 : undefined,
|
|
@@ -5398,7 +5658,7 @@ function BubbleBody(p) {
|
|
|
5398
5658
|
const b = p.bubble;
|
|
5399
5659
|
const title = b.error ? b.error.title : b.title;
|
|
5400
5660
|
const why = b.error ? (b.error.detail ?? "") : b.why;
|
|
5401
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "eg-b-top", children: [jsxRuntime.jsxs("span", { className: "eg-mis", children: ["\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08 ", b.missionNo, " \u00B7 ", b.missionTitle, b.replay ? " (ทบทวน)" : ""] }), jsxRuntime.jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", b.index + 1, "/", b.total] })] }), jsxRuntime.jsx("div", { className: "eg-pips", "aria-hidden": "true", children: Array.from({ length: b.total }, (_, i) => (jsxRuntime.jsx("i", { className: i < b.index ? "eg-d" : i === b.index ? "eg-c" : undefined }, i))) }), jsxRuntime.jsx("p", { className: "eg-b-t", id: BUBBLE_TITLE_ID, children: jsxRuntime.jsx(GuideTitle, { text: title }) }), jsxRuntime.jsx("p", { className: "eg-b-w", id: BUBBLE_WHY_ID, hidden: !why, children: why }), jsxRuntime.jsxs("div", { className: "eg-b-f", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: b.replay ? "จบการทบทวน" : "ออกจากไกด์" }), jsxRuntime.jsxs("span", { style: { display: "flex", gap: 6 }, children: [b.showEndWithoutSave ? (jsxRuntime.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 ? (jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onNext, children: "\u0E16\u0E31\u0E14\u0E44\u0E1B" })) : null] })] })] }));
|
|
5661
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("div", { className: "eg-b-top", children: [jsxRuntime.jsxs("span", { className: "eg-mis", children: ["\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08 ", b.missionNo, " \u00B7 ", b.missionTitle, b.replay ? " (ทบทวน)" : ""] }), jsxRuntime.jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", b.index + 1, "/", b.total] })] }), jsxRuntime.jsx("div", { className: "eg-pips", "aria-hidden": "true", children: Array.from({ length: b.total }, (_, i) => (jsxRuntime.jsx("i", { className: i < b.index ? "eg-d" : i === b.index ? "eg-c" : undefined }, i))) }), jsxRuntime.jsx("p", { className: "eg-b-t", id: BUBBLE_TITLE_ID, children: jsxRuntime.jsx(GuideTitle, { text: title }) }), jsxRuntime.jsx("p", { className: "eg-b-w", id: BUBBLE_WHY_ID, hidden: !why, children: jsxRuntime.jsx(GuideTitle, { text: why }) }), jsxRuntime.jsxs("div", { className: "eg-b-f", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: b.replay ? "จบการทบทวน" : "ออกจากไกด์" }), jsxRuntime.jsxs("span", { style: { display: "flex", gap: 6 }, children: [b.showEndWithoutSave ? (jsxRuntime.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 ? (jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onNext, children: "\u0E16\u0E31\u0E14\u0E44\u0E1B" })) : null] })] })] }));
|
|
5402
5662
|
}
|
|
5403
5663
|
/** The fingertip at ≈62% across and down the target; upside down near the bottom. */
|
|
5404
5664
|
function handPosition(spot, viewportHeight) {
|
|
@@ -5511,6 +5771,9 @@ function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
|
5511
5771
|
meta = jsxRuntime.jsx("span", { children: "\u0E02\u0E49\u0E32\u0E21\u0E44\u0E27\u0E49" });
|
|
5512
5772
|
else if (v === "waiting" && gate)
|
|
5513
5773
|
meta = gate.pending;
|
|
5774
|
+
// Locked: what to do first gets its own line under the title, so the title
|
|
5775
|
+
// keeps the row's width and the button stays short.
|
|
5776
|
+
const lockHint = v === "locked" && item.lockedBy ? `ทำ ${item.lockedBy} ก่อน` : null;
|
|
5514
5777
|
let button;
|
|
5515
5778
|
if (v === "done")
|
|
5516
5779
|
button = jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onStart(item.key, true), children: "\u0E17\u0E33\u0E0B\u0E49\u0E33" });
|
|
@@ -5519,11 +5782,11 @@ function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
|
|
|
5519
5782
|
else if (v === "waiting" && gate)
|
|
5520
5783
|
button = (jsxRuntime.jsx("a", { className: "eg-btn eg-ghost", href: gate.href, style: { textDecoration: "none" }, children: gate.label }));
|
|
5521
5784
|
else if (v === "locked")
|
|
5522
|
-
button = (jsxRuntime.
|
|
5785
|
+
button = (jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", disabled: true, "aria-label": lockHint ? `เริ่ม (${lockHint})` : undefined, children: "\u0E40\u0E23\u0E34\u0E48\u0E21" }));
|
|
5523
5786
|
else
|
|
5524
5787
|
button = (jsxRuntime.jsx("button", { type: "button", className: "eg-btn " + (v === "next" ? "eg-primary" : "eg-ghost"), onClick: () => onStart(item.key, false), children: v === "skipped" || v === "active" ? "ทำต่อ" : "เริ่ม" }));
|
|
5525
5788
|
const dismissable = item.optional && (v === "todo" || v === "next");
|
|
5526
|
-
return (jsxRuntime.jsxs("li", { className: cls, children: [jsxRuntime.jsx("span", { className: "eg-n", children: ticked ? jsxRuntime.jsx(IconCheck, {}) : n }), jsxRuntime.jsxs("div", { className: "eg-t", children: [jsxRuntime.jsx("b", { children: item.title }), jsxRuntime.jsx("small", { className: v === "skipped" ? "eg-sk" : undefined, children: meta })] }), dismissable ? (jsxRuntime.jsxs("span", { className: "eg-q-acts", children: [button, jsxRuntime.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)] }));
|
|
5789
|
+
return (jsxRuntime.jsxs("li", { className: cls, children: [jsxRuntime.jsx("span", { className: "eg-n", children: ticked ? jsxRuntime.jsx(IconCheck, {}) : n }), jsxRuntime.jsxs("div", { className: "eg-t", children: [jsxRuntime.jsx("b", { children: item.title }), lockHint ? jsxRuntime.jsx("small", { className: "eg-lock", children: lockHint }) : null, jsxRuntime.jsx("small", { className: v === "skipped" ? "eg-sk" : undefined, children: meta })] }), dismissable ? (jsxRuntime.jsxs("span", { className: "eg-q-acts", children: [button, jsxRuntime.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)] }));
|
|
5527
5790
|
}
|
|
5528
5791
|
function GuideToast({ text }) {
|
|
5529
5792
|
return (jsxRuntime.jsx("div", { className: "eg-toast", role: "status", children: text }));
|
|
@@ -5711,6 +5974,9 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5711
5974
|
/* ---------------- machine ---------------- */
|
|
5712
5975
|
const [state, setState] = React.useState(null);
|
|
5713
5976
|
const stateRef = React.useRef(null);
|
|
5977
|
+
// What the server holds (payloadKey), so a save that would change nothing
|
|
5978
|
+
// is not sent and does not bump its rev.
|
|
5979
|
+
const savedKey = React.useRef(null);
|
|
5714
5980
|
React.useEffect(() => {
|
|
5715
5981
|
if (!answer || answer.audience === "none" || answer.newerOnServer) {
|
|
5716
5982
|
stateRef.current = null;
|
|
@@ -5719,6 +5985,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5719
5985
|
}
|
|
5720
5986
|
if (stateRef.current)
|
|
5721
5987
|
return;
|
|
5988
|
+
savedKey.current = answer.progress && answer.progress.rev > 0 ? payloadKey(answer.progress, suite.app) : null;
|
|
5722
5989
|
// Resume from whichever copy is further along: the server's, or this
|
|
5723
5990
|
// tab's session copy from before a refresh (its rev is the server's).
|
|
5724
5991
|
const mirror = readMirror(organizationId, suite);
|
|
@@ -5775,7 +6042,11 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5775
6042
|
const cur = stateRef.current;
|
|
5776
6043
|
if (!cur)
|
|
5777
6044
|
return;
|
|
6045
|
+
if (payloadKey(cur.progress, suite.app) === savedKey.current)
|
|
6046
|
+
continue;
|
|
5778
6047
|
const row = await saveGuides(organizationId, suite, cur.progress);
|
|
6048
|
+
if (row)
|
|
6049
|
+
savedKey.current = payloadKey(row, suite.app);
|
|
5779
6050
|
if (row && stateRef.current) {
|
|
5780
6051
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
5781
6052
|
dispatchRef.current({ type: "serverRow", progress: row });
|
|
@@ -5952,7 +6223,9 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5952
6223
|
scrolling, dialogs animating in, the drawer sliding, the keyboard. */
|
|
5953
6224
|
const [layout, setLayout] = React.useState({
|
|
5954
6225
|
spot: null,
|
|
6226
|
+
covered: false,
|
|
5955
6227
|
holes: [],
|
|
6228
|
+
popovers: [],
|
|
5956
6229
|
vw: 0,
|
|
5957
6230
|
vh: 0,
|
|
5958
6231
|
oy: 0,
|
|
@@ -5964,9 +6237,22 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5964
6237
|
}
|
|
5965
6238
|
let raf = 0;
|
|
5966
6239
|
let lostSince = null;
|
|
5967
|
-
let last = { spot: null, holes: [], vw: 0, vh: 0, oy: 0 };
|
|
6240
|
+
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0 };
|
|
5968
6241
|
targetElRef.current = target ? findAnchor(target.anchor, target.id) : null;
|
|
5969
6242
|
const allow = stepInfo?.step.allow ?? [];
|
|
6243
|
+
// The spotlight is the part of the target a person can see: cut by the
|
|
6244
|
+
// scroll boxes around it (a row of tabs that scrolls sideways) and the
|
|
6245
|
+
// viewport. Wholly out of view (scrolled away up or down), its own box,
|
|
6246
|
+
// and the tour scrolls it back.
|
|
6247
|
+
let clipFor = null;
|
|
6248
|
+
let clips = [];
|
|
6249
|
+
const spotOf = (el) => {
|
|
6250
|
+
if (el !== clipFor) {
|
|
6251
|
+
clipFor = el;
|
|
6252
|
+
clips = clipAncestors(el);
|
|
6253
|
+
}
|
|
6254
|
+
return padRect(visibleRectOf(el, clips) ?? rectOf(el), TARGET_PAD);
|
|
6255
|
+
};
|
|
5970
6256
|
const tick = () => {
|
|
5971
6257
|
raf = requestAnimationFrame(tick);
|
|
5972
6258
|
const vv = window.visualViewport;
|
|
@@ -5981,14 +6267,14 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
5981
6267
|
let spot = null;
|
|
5982
6268
|
if (target && el && isPresent(el)) {
|
|
5983
6269
|
lostSince = null;
|
|
5984
|
-
spot =
|
|
6270
|
+
spot = spotOf(el);
|
|
5985
6271
|
}
|
|
5986
6272
|
else if (target) {
|
|
5987
6273
|
// Maybe re-rendered under another node.
|
|
5988
6274
|
const again = findAnchor(target.anchor, target.id);
|
|
5989
6275
|
if (again) {
|
|
5990
6276
|
targetElRef.current = again;
|
|
5991
|
-
spot =
|
|
6277
|
+
spot = spotOf(again);
|
|
5992
6278
|
lostSince = null;
|
|
5993
6279
|
}
|
|
5994
6280
|
else if (phase === "ready") {
|
|
@@ -6001,21 +6287,25 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6001
6287
|
}
|
|
6002
6288
|
if (phase === "waiting" && spot)
|
|
6003
6289
|
dispatchRef.current({ type: "sync" });
|
|
6004
|
-
const
|
|
6290
|
+
const popovers = [];
|
|
6005
6291
|
for (const p of openPoppers())
|
|
6006
|
-
|
|
6292
|
+
popovers.push(padRect(rectOf(p), 2));
|
|
6293
|
+
const holes = [];
|
|
6007
6294
|
for (const a of allow) {
|
|
6008
6295
|
const ael = findAnchor(a);
|
|
6009
6296
|
if (ael)
|
|
6010
6297
|
holes.push(padRect(rectOf(ael), TARGET_PAD));
|
|
6011
6298
|
}
|
|
6012
|
-
const
|
|
6299
|
+
const covered = !!spot && coveredByDialog(targetElRef.current);
|
|
6300
|
+
const next = { spot, covered, holes, popovers, vw, vh, oy };
|
|
6301
|
+
const sameList = (a, b) => a.length === b.length && a.every((h, i) => sameRect(h, b[i]));
|
|
6013
6302
|
if (!sameRect(next.spot, last.spot) ||
|
|
6303
|
+
next.covered !== last.covered ||
|
|
6014
6304
|
next.vw !== last.vw ||
|
|
6015
6305
|
next.vh !== last.vh ||
|
|
6016
6306
|
next.oy !== last.oy ||
|
|
6017
|
-
next.holes
|
|
6018
|
-
next.
|
|
6307
|
+
!sameList(next.holes, last.holes) ||
|
|
6308
|
+
!sameList(next.popovers, last.popovers)) {
|
|
6019
6309
|
last = next;
|
|
6020
6310
|
setLayout(next);
|
|
6021
6311
|
}
|
|
@@ -6399,7 +6689,7 @@ function GuideHostInner({ config, organizationId, code }) {
|
|
|
6399
6689
|
const { mission, step, missionNo } = stepInfo;
|
|
6400
6690
|
const kind = stepKind(step);
|
|
6401
6691
|
const bridge = screen.bridge;
|
|
6402
|
-
content = (jsxRuntime.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: {
|
|
6692
|
+
content = (jsxRuntime.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: {
|
|
6403
6693
|
missionNo,
|
|
6404
6694
|
missionTitle: mission.title,
|
|
6405
6695
|
replay: screen.replay,
|