@eintrek/erp-shell 0.1.76 → 0.1.77

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.
@@ -992,6 +992,7 @@ function initialGuideState(audience, progress, pages) {
992
992
  pageRun: null,
993
993
  panelSection: "all",
994
994
  autoPassed: null,
995
+ counter: null,
995
996
  audience,
996
997
  progress,
997
998
  screen: { name: "idle" },
@@ -1370,7 +1371,7 @@ function announceStep(d, suite, env) {
1370
1371
  const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
1371
1372
  const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
1372
1373
  const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
1373
- const pos = stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
1374
+ const pos = s.pos ?? stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
1374
1375
  d.effects.push({
1375
1376
  type: "announce",
1376
1377
  text: `ขั้นที่ ${pos.no} จาก ${pos.total}. ${title}${why ? `. ${why}` : ""}`,
@@ -1404,6 +1405,54 @@ function stepPosition(mission, index, env, context, autoPassed = []) {
1404
1405
  });
1405
1406
  return { no: Math.max(no, 1), total: Math.max(total, 1) };
1406
1407
  }
1408
+ /**
1409
+ * The steps after `index` this person will see, as far as can be known from
1410
+ * here: on this page, everything is read (requires, skipIf, a click whose
1411
+ * outcome already holds); on another page, a step whose showing depends on
1412
+ * what is on that page (`skipIf`, `onMissing: "skip"`) is not counted until
1413
+ * its page is reached.
1414
+ */
1415
+ function stepsAhead(mission, index, env, context) {
1416
+ let n = 0;
1417
+ for (let j = index + 1; j < mission.steps.length; j++) {
1418
+ const step = mission.steps[j];
1419
+ if (step.requires && !safe(() => env.can(step.requires), true))
1420
+ continue;
1421
+ const home = stepHomeRoute(mission.steps, j);
1422
+ const here = !home || !!matchRoute(home, env.pathname, env.search, env.code);
1423
+ if (here) {
1424
+ const ctx = guideContext(env, context, routeParamsFor(step, env));
1425
+ if (step.skipIf && safe(() => step.skipIf(ctx), false))
1426
+ continue;
1427
+ if (wouldAutoPass(step, env))
1428
+ continue;
1429
+ }
1430
+ else if (step.skipIf || step.onMissing === "skip") {
1431
+ continue;
1432
+ }
1433
+ n++;
1434
+ }
1435
+ return n;
1436
+ }
1437
+ /**
1438
+ * The counter as shown when entering step `index` (shell ≥ 0.1.77). `no`
1439
+ * counts the steps actually shown in this run up to this one. The total is
1440
+ * worked out on entering a step on a new page — the shown steps so far plus
1441
+ * stepsAhead — and kept while the run stays on that page; it never goes
1442
+ * down (the larger of the old and the new), and is never below `no`.
1443
+ */
1444
+ function countStep(d, env, mission, index) {
1445
+ const prev = d.state.counter?.mission === mission.key ? d.state.counter : null;
1446
+ const shown = prev ? (prev.shown.includes(index) ? prev.shown : [...prev.shown, index]) : [index];
1447
+ const no = Math.max(1, shown.filter(i => i <= index).length);
1448
+ let total;
1449
+ if (prev && prev.path === env.pathname)
1450
+ total = Math.max(prev.total, no);
1451
+ else
1452
+ total = Math.max(prev?.total ?? 0, no + stepsAhead(mission, index, env, d.state.progress.context), no);
1453
+ d.state = { ...d.state, counter: { mission: mission.key, shown, total, path: env.pathname } };
1454
+ return { no, total };
1455
+ }
1407
1456
  /** A step entered now would pass at once: a click or select step whose route or visible outcome holds. */
1408
1457
  function wouldAutoPass(step, env) {
1409
1458
  const kind = stepKind(step);
@@ -1473,6 +1522,7 @@ function enterStep(d, suite, env, mission, index, opts) {
1473
1522
  const away = !page && !section && !!where && !matchRoute(where, env.pathname, env.search, env.code);
1474
1523
  const bridge = !away && needsBridge(step, env, ctx);
1475
1524
  const nts = !away && !bridge && nothingToSaveHolds(step, env, ctx);
1525
+ const pos = countStep(d, env, mission, i);
1476
1526
  setScreen(d, {
1477
1527
  name: "step",
1478
1528
  mission: mission.key,
@@ -1490,6 +1540,7 @@ function enterStep(d, suite, env, mission, index, opts) {
1490
1540
  ...(page ? { page } : {}),
1491
1541
  ...(section ? { section } : {}),
1492
1542
  ...(flow ? { flow } : {}),
1543
+ pos,
1493
1544
  });
1494
1545
  if (!opts.replay) {
1495
1546
  const cp = checkpointAtOrBefore(steps, i);
@@ -1764,7 +1815,7 @@ function startMission(d, suite, env, key, replay) {
1764
1815
  const isReplay = replay || entry?.status === "done";
1765
1816
  const before = d.state.progress;
1766
1817
  // A new run: what passed by itself is counted afresh.
1767
- d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null };
1818
+ d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null, counter: null };
1768
1819
  if (d.state.progress.welcome === "not_seen")
1769
1820
  withProgress(d, { welcome: "started" });
1770
1821
  if (!isReplay && d.state.progress.mission_key !== key)
@@ -1914,24 +1965,33 @@ function pageStillHere(suite, key, env) {
1914
1965
  const page = findPage(suite, key);
1915
1966
  return !!page && pageMatchScore(page, whereOf(env)) !== null;
1916
1967
  }
1917
- /** Write a page guide's row (once per page and version): recorded here at once, saved by the host. */
1968
+ /**
1969
+ * Whether a row may be written now: none of this version yet, or — once —
1970
+ * a skipped one that the person has now finished (skipped → completed,
1971
+ * shell ≥ 0.1.77). A completed row is final; skipped again changes nothing.
1972
+ */
1973
+ function rowWritable(row, version, status) {
1974
+ if (!row || row.version < version)
1975
+ return true;
1976
+ return row.version === version && row.status === "skipped" && status === "completed";
1977
+ }
1978
+ /** Whether a run should keep its row to write at its end: no row yet, or only a skipped one. */
1979
+ function rowStillDue(row, version) {
1980
+ return rowWritable(row, version, "completed");
1981
+ }
1982
+ /** Write a page guide's row (once per page and version; skipped → completed once): recorded here at once, saved by the host. */
1918
1983
  function recordPage(d, suite, key, status) {
1919
1984
  const page = findPage(suite, key);
1920
- if (!page || pageRecorded(d.state, page))
1985
+ if (!page || !rowWritable(d.state.pages[key], page.version, status))
1921
1986
  return;
1922
1987
  const prev = d.state.pages[key];
1923
1988
  d.state = { ...d.state, pages: { ...d.state.pages, [key]: { version: page.version, status, rev: prev?.rev ?? 0 } } };
1924
1989
  d.effects.push({ type: "savePage", key, version: page.version, status });
1925
1990
  }
1926
- /** The section guide's row says this person has ended this version of it. */
1927
- function sectionRecorded(state, section) {
1928
- const row = state.sections?.[section.key];
1929
- return !!row && row.version >= section.version;
1930
- }
1931
1991
  /** Write a section guide's row (once per section and version). */
1932
1992
  function recordSection(d, suite, key, status) {
1933
1993
  const section = findSection(suite, key);
1934
- if (!section || sectionRecorded(d.state, section))
1994
+ if (!section || !rowWritable(d.state.sections?.[key], section.version, status))
1935
1995
  return;
1936
1996
  const prev = d.state.sections?.[key];
1937
1997
  d.state = { ...d.state, sections: { ...d.state.sections, [key]: { version: section.version, status, rev: prev?.rev ?? 0 } } };
@@ -1962,7 +2022,7 @@ function flowRecorded(state, flow) {
1962
2022
  /** Write a flow's row (once per flow and version). */
1963
2023
  function recordFlow(d, suite, key, status) {
1964
2024
  const flow = findFlow(suite, key);
1965
- if (!flow || flowRecorded(d.state, flow))
2025
+ if (!flow || !rowWritable(d.state.flows?.[key], flow.version, status))
1966
2026
  return;
1967
2027
  const prev = d.state.flows?.[key];
1968
2028
  d.state = { ...d.state, flows: { ...d.state.flows, [key]: { version: flow.version, status, rev: prev?.rev ?? 0 } } };
@@ -2047,6 +2107,7 @@ function startFlow(d, suite, env, key, write, at) {
2047
2107
  justDone: null,
2048
2108
  pageRun: { key, write, kind: "flow", context: saved },
2049
2109
  autoPassed: null,
2110
+ counter: null,
2050
2111
  replayPage: {
2051
2112
  mission: mission.key,
2052
2113
  anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
@@ -2077,6 +2138,7 @@ function startSection(d, suite, env, key, write) {
2077
2138
  justDone: null,
2078
2139
  pageRun: { key, write, kind: "section" },
2079
2140
  autoPassed: null,
2141
+ counter: null,
2080
2142
  replayPage: {
2081
2143
  mission: mission.key,
2082
2144
  anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
@@ -2096,6 +2158,7 @@ function startPage(d, suite, env, key, write) {
2096
2158
  justDone: null,
2097
2159
  pageRun: { key, write, kind: "page" },
2098
2160
  autoPassed: null,
2161
+ counter: null,
2099
2162
  // What is open now is the person's: ending the guide leaves it alone.
2100
2163
  replayPage: {
2101
2164
  mission: mission.key,
@@ -2139,6 +2202,28 @@ function backTarget(mission, index, env, context) {
2139
2202
  }
2140
2203
  return prev >= 0 ? { index: prev, close: false } : null;
2141
2204
  }
2205
+ /**
2206
+ * A flow's next step (the next one this person sees) when the person is on
2207
+ * its page and no longer on the current step's: they moved on by
2208
+ * themselves. -1 otherwise (still on this step's page, or elsewhere).
2209
+ */
2210
+ function flowStepArrivedAt(mission, index, env, context) {
2211
+ const here = (route) => !!route && !!matchRoute(route, env.pathname, env.search, env.code);
2212
+ const curHome = stepHomeRoute(mission.steps, index);
2213
+ if (!curHome || here(curHome))
2214
+ return -1;
2215
+ for (let j = index + 1; j < mission.steps.length; j++) {
2216
+ const st = mission.steps[j];
2217
+ const ctx = guideContext(env, context, routeParamsFor(st, env));
2218
+ if (st.requires && !safe(() => env.can(st.requires), true))
2219
+ continue;
2220
+ if (st.skipIf && safe(() => st.skipIf(ctx), false))
2221
+ continue;
2222
+ const where = st.route ?? stepHomeRoute(mission.steps, j);
2223
+ return here(where) ? j : -1;
2224
+ }
2225
+ return -1;
2226
+ }
2142
2227
  function reduceGuide(state, action, suite, env) {
2143
2228
  const d = { state, effects: [] };
2144
2229
  const s = state.screen;
@@ -2216,7 +2301,7 @@ function reduceGuide(state, action, suite, env) {
2216
2301
  const stepAt = mission.steps.findIndex(st => st.key === d.state.progress.step_key);
2217
2302
  const cpAt = mission.steps.findIndex(st => st.key === d.state.progress.checkpoint_key);
2218
2303
  const index = stepAt >= 0 ? checkpointAtOrBefore(mission.steps, stepAt) : Math.max(cpAt, 0);
2219
- d.state = { ...d.state, autoPassed: null };
2304
+ d.state = { ...d.state, autoPassed: null, counter: null };
2220
2305
  enterStep(d, suite, env, mission, index, { replay: false });
2221
2306
  break;
2222
2307
  }
@@ -2259,6 +2344,15 @@ function reduceGuide(state, action, suite, env) {
2259
2344
  break;
2260
2345
  const step = mission.steps[s.index];
2261
2346
  const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
2347
+ if (s.flow && !s.confirmExit) {
2348
+ // Within the chain, the person went on to the next step's page by
2349
+ // themselves: carry on there, no "ทำต่อ" (shell ≥ 0.1.77).
2350
+ const next = flowStepArrivedAt(mission, s.index, env, d.state.progress.context);
2351
+ if (next >= 0) {
2352
+ enterStep(d, suite, env, mission, next, { replay: s.replay });
2353
+ break;
2354
+ }
2355
+ }
2262
2356
  if (s.phase === "away") {
2263
2357
  const where = placeRoute(mission.steps, s.index);
2264
2358
  if (!where || matchRoute(where, env.pathname, env.search, env.code)) {
@@ -2653,7 +2747,7 @@ function reduceGuide(state, action, suite, env) {
2653
2747
  if (!flowsAvailable(suite, env).includes(action.flow))
2654
2748
  break;
2655
2749
  const flow = findFlow(suite, action.flow);
2656
- startFlow(d, suite, env, flow.key, !flowRecorded(state, flow));
2750
+ startFlow(d, suite, env, flow.key, rowStillDue(state.flows?.[flow.key], flow.version));
2657
2751
  break;
2658
2752
  }
2659
2753
  case "resumeFlow": {
@@ -2662,7 +2756,7 @@ function reduceGuide(state, action, suite, env) {
2662
2756
  const flow = findFlow(suite, action.flow);
2663
2757
  if (!flow || !flowShown(flow, guideContext(env, {})) || !inFlowChain(flow, whereOf(env)))
2664
2758
  break;
2665
- startFlow(d, suite, env, flow.key, action.write && !flowRecorded(state, flow), { step: action.step, context: action.context });
2759
+ startFlow(d, suite, env, flow.key, action.write && rowStillDue(state.flows?.[flow.key], flow.version), { step: action.step, context: action.context });
2666
2760
  break;
2667
2761
  }
2668
2762
  case "startPage": {
@@ -2674,7 +2768,7 @@ function reduceGuide(state, action, suite, env) {
2674
2768
  const page = pageHere(suite, env);
2675
2769
  if (!page || page.key !== action.page)
2676
2770
  break;
2677
- startPage(d, suite, env, page.key, !pageRecorded(state, page));
2771
+ startPage(d, suite, env, page.key, rowStillDue(state.pages[page.key], page.version));
2678
2772
  break;
2679
2773
  }
2680
2774
  case "back": {
@@ -2723,7 +2817,7 @@ function reduceGuide(state, action, suite, env) {
2723
2817
  if (!sectionsAvailable(suite, env).includes(action.section))
2724
2818
  break;
2725
2819
  const section = findSection(suite, action.section);
2726
- startSection(d, suite, env, section.key, !sectionRecorded(state, section));
2820
+ startSection(d, suite, env, section.key, rowStillDue(state.sections?.[section.key], section.version));
2727
2821
  break;
2728
2822
  }
2729
2823
  case "idleCelebrate": {
@@ -3169,7 +3263,10 @@ async function storePageRow(adapter, app, key, version, status, rev, kind = "pag
3169
3263
  }
3170
3264
  if (current.version > version)
3171
3265
  return null;
3172
- if (current.version === version && current.status !== "in_progress")
3266
+ // This version already recorded: kept, except a skipped row this run
3267
+ // finished (skipped → completed, once, shell ≥ 0.1.77).
3268
+ const finishing = status === "completed" && current.status === "skipped";
3269
+ if (current.version === version && current.status !== "in_progress" && !finishing)
3173
3270
  return current;
3174
3271
  at = current.rev;
3175
3272
  }
@@ -3836,6 +3933,9 @@ function useNudge(nudge, ref) {
3836
3933
  }
3837
3934
  function StepLayer(p) {
3838
3935
  const { spot, phone, viewport } = p;
3936
+ // Where the bubble may sit: the content column when the app has one.
3937
+ const col = p.bounds && p.bounds.width > 0 ? p.bounds : { x: 0, width: viewport.width || (typeof window !== "undefined" ? window.innerWidth : 0)};
3938
+ const colKey = `${col.x}|${col.width}`;
3839
3939
  const bubbleRef = useRef(null);
3840
3940
  const arrowRef = useRef(null);
3841
3941
  const ringRef = useRef(null);
@@ -3876,8 +3976,8 @@ function StepLayer(p) {
3876
3976
  const bw = el.offsetWidth;
3877
3977
  const bh = el.offsetHeight;
3878
3978
  setPos(phone
3879
- ? { left: m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
3880
- : { left: (window.innerWidth - bw) / 2, top: viewport.offsetTop + (viewport.height - bh) / 2, side: "dock" });
3979
+ ? { left: col.x + m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
3980
+ : { left: col.x + (col.width - bw) / 2, top: viewport.offsetTop + (viewport.height - bh) / 2, side: "dock" });
3881
3981
  return;
3882
3982
  }
3883
3983
  if (phone) {
@@ -3898,7 +3998,7 @@ function StepLayer(p) {
3898
3998
  : overlapBot <= overlapTop
3899
3999
  ? botT
3900
4000
  : topT;
3901
- setPos({ left: m, top, side: "dock" });
4001
+ setPos({ left: col.x + m, top, side: "dock" });
3902
4002
  return;
3903
4003
  }
3904
4004
  let cancelled = false;
@@ -3920,7 +4020,7 @@ function StepLayer(p) {
3920
4020
  placement,
3921
4021
  middleware: [
3922
4022
  offset(placement.startsWith("bottom") ? gap : 14),
3923
- shift({ padding: m }),
4023
+ shift({ padding: m, rootBoundary: { x: col.x, y: 0, width: col.width, height: window.innerHeight } }),
3924
4024
  ...(arrowRef.current ? [arrow({ element: arrowRef.current, padding: 14 })] : []),
3925
4025
  ],
3926
4026
  });
@@ -3930,11 +4030,11 @@ function StepLayer(p) {
3930
4030
  return;
3931
4031
  const bw = el.offsetWidth;
3932
4032
  const bh = el.offsetHeight;
3933
- const bounds = { x: m, y: viewport.offsetTop + m, width: window.innerWidth - 2 * m, height: viewport.height - 2 * m };
4033
+ const bounds = { x: col.x + m, y: viewport.offsetTop + m, width: col.width - 2 * m, height: viewport.height - 2 * m };
3934
4034
  const i = pickPlacement(results, bw, bh, a, clear, bounds);
3935
4035
  if (i < 0) {
3936
4036
  // Nowhere beside it: the bottom-right corner.
3937
- setPos({ left: window.innerWidth - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
4037
+ setPos({ left: col.x + col.width - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
3938
4038
  return;
3939
4039
  }
3940
4040
  const { x, y, placement, middlewareData } = results[i];
@@ -3950,7 +4050,7 @@ function StepLayer(p) {
3950
4050
  };
3951
4051
  // The keys stand for spot, the lists, the holes and the bubble's size.
3952
4052
  // eslint-disable-next-line react-hooks/exhaustive-deps
3953
- }, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
4053
+ }, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop, colKey]);
3954
4054
  const popovers = p.popovers ?? [];
3955
4055
  const covered = !!p.covered;
3956
4056
  const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
@@ -3971,7 +4071,7 @@ function StepLayer(p) {
3971
4071
  return (jsxs(Fragment, { children: [jsx(Scrim, { holes: p.phase === "ready" ? holes : [], open: p.phase === "ready" ? openHoles : [], 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: {
3972
4072
  left: pos?.left ?? 0,
3973
4073
  top: pos?.top ?? 0,
3974
- width: phone ? viewport.width - 32 : undefined,
4074
+ width: phone ? col.width - 32 : col.width < 352 ? col.width - 32 : undefined,
3975
4075
  visibility: pos ? "visible" : "hidden",
3976
4076
  }, children: [jsx(BubbleBody, { ...p }), jsx("span", { ref: arrowRef, className: "eg-arrow" + (pos?.side === "bottom" ? " eg-top" : ""), style: arrowStyle ?? { display: "none" }, "aria-hidden": "true" })] })) : null, p.phase === "waiting" ? (jsxs("div", { className: "eg-card", role: "status", "data-eg-card": "", children: [jsxs("div", { className: "eg-loading", children: [jsx("span", { className: "eg-spin", "aria-hidden": "true" }), "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14\u2026"] }), jsx("div", { className: "eg-row", children: jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }) })] })) : null, p.phase === "missing" ? (jsxs("div", { className: "eg-card eg-err", role: "alertdialog", "aria-labelledby": "eg-missing-t", "data-eg-card": "", tabIndex: -1, children: [jsx("p", { className: "eg-b-t", id: "eg-missing-t", children: "\u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E2A\u0E48\u0E27\u0E19\u0E19\u0E35\u0E49\u0E1A\u0E19\u0E2B\u0E19\u0E49\u0E32" }), jsxs("div", { className: "eg-row", children: [jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }), jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onRetry, children: "\u0E25\u0E2D\u0E07\u0E2D\u0E35\u0E01\u0E04\u0E23\u0E31\u0E49\u0E07" })] })] })) : null, p.phase === "away" ? (jsxs("div", { className: "eg-card", role: "dialog", "aria-modal": "false", "aria-labelledby": "eg-away-t", "data-eg-card": "", tabIndex: -1, children: [jsxs("div", { className: "eg-b-top", children: [jsx("span", { className: "eg-mis", children: headingOf(p.bubble) }), jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", p.bubble.index + 1, "/", p.bubble.total] })] }), jsx("p", { className: "eg-b-t", id: "eg-away-t", style: { marginTop: 8 }, children: jsx(GuideTitle, { text: p.bubble.title }) }), jsxs("div", { className: "eg-row", children: [jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }), jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onGoResume, children: "\u0E17\u0E33\u0E15\u0E48\u0E2D" })] })] })) : null, p.confirmExit ? (jsx(ConfirmModal, { title: "\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E44\u0E01\u0E14\u0E4C\u0E15\u0E2D\u0E19\u0E19\u0E35\u0E49?", line: p.reopenHint || REOPEN_HINT, leaveLabel: "\u0E2D\u0E2D\u0E01\u0E08\u0E32\u0E01\u0E44\u0E01\u0E14\u0E4C", stayLabel: "\u0E17\u0E33\u0E15\u0E48\u0E2D", onLeave: p.onExitConfirm, onStay: p.onExitCancel })) : null] }));
3977
4077
  }
@@ -4339,6 +4439,42 @@ function runAbort(actions) {
4339
4439
  }, ABORT_DELAY_MS + i * ABORT_GAP_MS);
4340
4440
  });
4341
4441
  }
4442
+ /** The app's content column (GuideConfig.contentRoot), cut to the viewport; null when none is declared or found. */
4443
+ function contentColumn(selector, vw) {
4444
+ if (!selector || typeof document === "undefined")
4445
+ return null;
4446
+ let el = null;
4447
+ try {
4448
+ el = document.querySelector(selector);
4449
+ }
4450
+ catch {
4451
+ return null;
4452
+ }
4453
+ if (!el)
4454
+ return null;
4455
+ const r = el.getBoundingClientRect();
4456
+ const x = Math.max(0, r.left);
4457
+ const right = Math.min(vw || window.innerWidth, r.right);
4458
+ if (right - x <= 0)
4459
+ return null;
4460
+ return { x, y: 0, width: right - x, height: 0 };
4461
+ }
4462
+ /** The control a form's implicit submit (Enter in a field) presses: its first submit button. */
4463
+ function defaultSubmitControl(form) {
4464
+ for (const el of Array.from(form.elements)) {
4465
+ if (el instanceof HTMLButtonElement && (el.type === "submit" || !el.getAttribute("type")))
4466
+ return el;
4467
+ if (el instanceof HTMLInputElement && (el.type === "submit" || el.type === "image"))
4468
+ return el;
4469
+ }
4470
+ return null;
4471
+ }
4472
+ /** A field where Enter submits its form (not a textarea, a button or a select). */
4473
+ function submitsOnEnter(el) {
4474
+ if (!(el instanceof HTMLInputElement))
4475
+ return false;
4476
+ return !["button", "submit", "reset", "image", "file", "hidden"].includes(el.type);
4477
+ }
4342
4478
  /** A step's own target takes presses: not on a "view" step, unless the step allows it. */
4343
4479
  function targetPressable(step, bridge) {
4344
4480
  if (!step || bridge)
@@ -4717,6 +4853,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4717
4853
  const stepId = screen.name === "step" ? `${screen.mission}:${screen.index}:${screen.bridge ? "b" : ""}${screen.nothingToSave ? "n" : ""}` : "";
4718
4854
  /* Measure every frame while a step is shown: the spotlight follows
4719
4855
  scrolling, dialogs animating in, the drawer sliding, the keyboard. */
4856
+ const contentRoot = config.contentRoot;
4720
4857
  const [layout, setLayout] = useState({
4721
4858
  spot: null,
4722
4859
  covered: false,
@@ -4725,6 +4862,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4725
4862
  vw: 0,
4726
4863
  vh: 0,
4727
4864
  oy: 0,
4865
+ col: null,
4728
4866
  });
4729
4867
  useEffect(() => {
4730
4868
  if (!inStep) {
@@ -4733,7 +4871,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4733
4871
  }
4734
4872
  let raf = 0;
4735
4873
  let lostSince = null;
4736
- let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0 };
4874
+ let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0, col: null };
4737
4875
  targetElRef.current = target ? findAnchor(target.anchor, target.id) : null;
4738
4876
  const allow = stepInfo?.step.allow ?? [];
4739
4877
  // The spotlight is the part of the target a person can see: cut by the
@@ -4793,13 +4931,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4793
4931
  holes.push(padRect(rectOf(ael), TARGET_PAD));
4794
4932
  }
4795
4933
  const covered = !!spot && coveredByDialog(targetElRef.current);
4796
- const next = { spot, covered, holes, popovers, vw, vh, oy };
4934
+ const col = contentColumn(contentRoot, vw);
4935
+ const next = { spot, covered, holes, popovers, vw, vh, oy, col };
4797
4936
  const sameList = (a, b) => a.length === b.length && a.every((h, i) => sameRect(h, b[i]));
4798
4937
  if (!sameRect(next.spot, last.spot) ||
4799
4938
  next.covered !== last.covered ||
4800
4939
  next.vw !== last.vw ||
4801
4940
  next.vh !== last.vh ||
4802
4941
  next.oy !== last.oy ||
4942
+ !sameRect(next.col, last.col) ||
4803
4943
  !sameList(next.holes, last.holes) ||
4804
4944
  !sameList(next.popovers, last.popovers)) {
4805
4945
  last = next;
@@ -4954,6 +5094,43 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4954
5094
  }
4955
5095
  return false;
4956
5096
  };
5097
+ /**
5098
+ * Whether a form may be submitted during this step (shell ≥ 0.1.77): only
5099
+ * when its submit control — the button pressed, or the one Enter in a
5100
+ * field would press — is the step's pressable target or in its `allow`.
5101
+ * Otherwise Enter in an allowed field would save a draft (and take a
5102
+ * document number) behind the guide's back.
5103
+ */
5104
+ const submitAllowed = (form, submitter) => {
5105
+ const btn = submitter ?? defaultSubmitControl(form);
5106
+ if (!btn)
5107
+ return false;
5108
+ const el = targetElRef.current;
5109
+ const sc = stateRef.current?.screen;
5110
+ const pressable = targetPressable(stepInfoRef.current?.step, sc?.name === "step" && sc.bridge);
5111
+ if (el && pressable && el.contains(btn))
5112
+ return true;
5113
+ for (const a of stepInfoRef.current?.step.allow ?? []) {
5114
+ const ael = findAnchor(a);
5115
+ if (ael && ael.contains(btn))
5116
+ return true;
5117
+ }
5118
+ return false;
5119
+ };
5120
+ const onSubmit = (e) => {
5121
+ const sc = stateRef.current?.screen;
5122
+ if (!guardingRef.current || sc?.name !== "step")
5123
+ return;
5124
+ const form = e.target;
5125
+ if (!(form instanceof HTMLFormElement) || inGuideLayer(form))
5126
+ return;
5127
+ const submitter = e.submitter ?? null;
5128
+ if (submitAllowed(form, submitter))
5129
+ return;
5130
+ e.preventDefault();
5131
+ e.stopPropagation();
5132
+ dispatchRef.current({ type: "blocked" });
5133
+ };
4957
5134
  const onPointer = (e) => {
4958
5135
  const t = e.target;
4959
5136
  if (inGuideLayer(t)) {
@@ -5073,8 +5250,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5073
5250
  return;
5074
5251
  }
5075
5252
  }
5076
- if (modalRef.current || !allowedTarget(active))
5253
+ if (modalRef.current || !allowedTarget(active)) {
5077
5254
  stop();
5255
+ return;
5256
+ }
5257
+ // Enter in a field the step lets the person type in: no implicit
5258
+ // submit unless the form's submit control is allowed now.
5259
+ if (e.key === "Enter" && sc.name === "step" && submitsOnEnter(active) && active.form && !submitAllowed(active.form, null)) {
5260
+ e.preventDefault();
5261
+ }
5078
5262
  }
5079
5263
  };
5080
5264
  window.addEventListener("pointerdown", onPointer, true);
@@ -5084,7 +5268,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5084
5268
  window.addEventListener("focusin", onFocusIn, true);
5085
5269
  window.addEventListener("focusout", onFocusOut, true);
5086
5270
  window.addEventListener("keydown", onKey, true);
5271
+ window.addEventListener("submit", onSubmit, true);
5087
5272
  return () => {
5273
+ window.removeEventListener("submit", onSubmit, true);
5088
5274
  window.removeEventListener("pointerdown", onPointer, true);
5089
5275
  window.removeEventListener("mousedown", onPointer, true);
5090
5276
  window.removeEventListener("touchstart", onPointer, true);
@@ -5512,7 +5698,11 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5512
5698
  const missionNo = missionNumber(suite, mission.key, ctxIdle);
5513
5699
  const kind = stepKind(step);
5514
5700
  const bridge = screen.bridge;
5515
- const pos = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
5701
+ // What is ahead now decides เสร็จ; the number shown is the run's counter,
5702
+ // which never goes down (0.1.77).
5703
+ const raw = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
5704
+ const last = raw.no >= raw.total;
5705
+ const pos = screen.pos ?? raw;
5516
5706
  const page = screen.page ? findPage(suite, screen.page) : undefined;
5517
5707
  const section = screen.section ? findSection(suite, screen.section) : undefined;
5518
5708
  const flow = screen.flow ? findFlow(suite, screen.flow) : undefined;
@@ -5521,16 +5711,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5521
5711
  ? {
5522
5712
  ...read,
5523
5713
  showBack: kind === "view" && !!backTarget(mission, screen.index, env(), state.progress.context),
5524
- nextLabel: pos.no >= pos.total ? "เสร็จ" : undefined,
5714
+ nextLabel: last ? "เสร็จ" : undefined,
5525
5715
  }
5526
5716
  : flow
5527
5717
  ? {
5528
5718
  heading: `พาทำ · ${flow.title}`,
5529
5719
  exitLabel: "ออกจากการพาทำ",
5530
- nextLabel: kind === "view" && pos.no >= pos.total ? "เสร็จ" : undefined,
5720
+ nextLabel: kind === "view" && last ? "เสร็จ" : undefined,
5531
5721
  }
5532
5722
  : {};
5533
- 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: {
5723
+ 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 }, bounds: layout.col, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
5534
5724
  missionNo,
5535
5725
  missionTitle: mission.title,
5536
5726
  replay: screen.replay,
@@ -5713,4 +5903,4 @@ function thaiMonthYear(m, opts = {}) {
5713
5903
  }
5714
5904
 
5715
5905
  export { API_EVENT as A, useGuideEntry as B, usePageGuideEntry as C, usePageGuides as D, useSectionGuideValues as E, GUIDE_EVENT_PREFIX as G, SectionGuideButton as S, THAI_MONTHS_SHORT as T, GUIDE_LAYER_ATTRIBUTE as a, GuideErrorBoundary as b, GuideProgressConflict as c, GuideRuntime as d, apiCallDetail as e, emitGuideEvent as f, fillRoute as g, formatBaht as h, guideAnchor as i, guideMenuItems as j, guideMenuShown as k, loadGuides as l, matchApiPath as m, matchRoute as n, openFlow as o, openFlowsPanel as p, openGuidePanel as q, openPageGuide as r, openPagesPanel as s, openSectionGuide as t, reportApiCall as u, reportApiEvent as v, setSectionGuideValues as w, storeGuides as x, thaiMonth as y, thaiMonthYear as z };
5716
- //# sourceMappingURL=format-BWUKNKxg.esm.js.map
5906
+ //# sourceMappingURL=format-ILrTMV4G.esm.js.map