@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.
- package/README.md +27 -0
- package/dist/chunks/{format-BVoVc7Yk.cjs → format-DQLPHq7h.cjs} +223 -33
- package/dist/chunks/format-DQLPHq7h.cjs.map +1 -0
- package/dist/chunks/{format-BWUKNKxg.esm.js → format-ILrTMV4G.esm.js} +223 -33
- package/dist/chunks/format-ILrTMV4G.esm.js.map +1 -0
- package/dist/guide/guide-ui.d.ts +6 -0
- package/dist/guide/machine.d.ts +39 -0
- package/dist/guide/types.d.ts +6 -0
- package/dist/guide-core.cjs +1 -1
- package/dist/guide-core.esm.js +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.esm.js +3 -3
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunks/format-BVoVc7Yk.cjs.map +0 -1
- package/dist/chunks/format-BWUKNKxg.esm.js.map +0 -1
package/README.md
CHANGED
|
@@ -176,6 +176,33 @@ progress API: `API.md` next to it.
|
|
|
176
176
|
shell's adapters), `guide-ui.tsx`, `guide-styles.ts`. Tests:
|
|
177
177
|
`src/guide/*.test.ts`.
|
|
178
178
|
|
|
179
|
+
### 0.1.77
|
|
180
|
+
|
|
181
|
+
Fixes from the apps' first week on 0.1.76.
|
|
182
|
+
|
|
183
|
+
- **No implicit submit during a step.** While a guide step is up (a mission,
|
|
184
|
+
a page or section guide, a flow), Enter in a field and a form's `submit`
|
|
185
|
+
are blocked unless the form's submit control — the button pressed, or
|
|
186
|
+
the one Enter would press — is the step's pressable target or in its
|
|
187
|
+
`allow`. Peoplx: Enter in an allowed field saved a draft and took a
|
|
188
|
+
document number.
|
|
189
|
+
- **Skipped → completed, once.** A page guide, section guide or flow left
|
|
190
|
+
once (skipped) records the person's later finish (completed). Completed
|
|
191
|
+
is final: a replay writes nothing; skipped again changes nothing.
|
|
192
|
+
- **A counter that never goes down.** "ขั้นที่ x/y": `x` counts the steps
|
|
193
|
+
actually shown in this run; `y` is worked out when a step is entered on a
|
|
194
|
+
page it was not worked out on, held while the run stays on that page,
|
|
195
|
+
and never lowered. Steps on other pages whose showing depends on that
|
|
196
|
+
page (`skipIf`, `onMissing: "skip"`) are counted once it is reached. So
|
|
197
|
+
the total can grow on a new page, never shrink (no more 1/5 → 2/6 → 2/5).
|
|
198
|
+
Going back a step (a checkpoint, ย้อนกลับ) moves `x` back.
|
|
199
|
+
- **Flows carry on between their pages.** When the person goes to the next
|
|
200
|
+
step's page by themselves (within the chain), the flow carries on there,
|
|
201
|
+
with no "ทำต่อ". ทำต่อ stays for going back to an earlier page and after
|
|
202
|
+
a reload.
|
|
203
|
+
- **`config.contentRoot`**: a selector for the app's content column (LIFF's
|
|
204
|
+
460 px column on a desktop); bubbles stay inside it.
|
|
205
|
+
|
|
179
206
|
### 0.1.76
|
|
180
207
|
|
|
181
208
|
Workflow guides ("พาทำงาน", phase 2; authors: `_claude-work/guides-r4/GUIDE-PAGES.md` §11).
|
|
@@ -1012,6 +1012,7 @@ function initialGuideState(audience, progress, pages) {
|
|
|
1012
1012
|
pageRun: null,
|
|
1013
1013
|
panelSection: "all",
|
|
1014
1014
|
autoPassed: null,
|
|
1015
|
+
counter: null,
|
|
1015
1016
|
audience,
|
|
1016
1017
|
progress,
|
|
1017
1018
|
screen: { name: "idle" },
|
|
@@ -1390,7 +1391,7 @@ function announceStep(d, suite, env) {
|
|
|
1390
1391
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
1391
1392
|
const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
|
|
1392
1393
|
const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
|
|
1393
|
-
const pos = stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
|
|
1394
|
+
const pos = s.pos ?? stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
|
|
1394
1395
|
d.effects.push({
|
|
1395
1396
|
type: "announce",
|
|
1396
1397
|
text: `ขั้นที่ ${pos.no} จาก ${pos.total}. ${title}${why ? `. ${why}` : ""}`,
|
|
@@ -1424,6 +1425,54 @@ function stepPosition(mission, index, env, context, autoPassed = []) {
|
|
|
1424
1425
|
});
|
|
1425
1426
|
return { no: Math.max(no, 1), total: Math.max(total, 1) };
|
|
1426
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* The steps after `index` this person will see, as far as can be known from
|
|
1430
|
+
* here: on this page, everything is read (requires, skipIf, a click whose
|
|
1431
|
+
* outcome already holds); on another page, a step whose showing depends on
|
|
1432
|
+
* what is on that page (`skipIf`, `onMissing: "skip"`) is not counted until
|
|
1433
|
+
* its page is reached.
|
|
1434
|
+
*/
|
|
1435
|
+
function stepsAhead(mission, index, env, context) {
|
|
1436
|
+
let n = 0;
|
|
1437
|
+
for (let j = index + 1; j < mission.steps.length; j++) {
|
|
1438
|
+
const step = mission.steps[j];
|
|
1439
|
+
if (step.requires && !safe(() => env.can(step.requires), true))
|
|
1440
|
+
continue;
|
|
1441
|
+
const home = stepHomeRoute(mission.steps, j);
|
|
1442
|
+
const here = !home || !!matchRoute(home, env.pathname, env.search, env.code);
|
|
1443
|
+
if (here) {
|
|
1444
|
+
const ctx = guideContext(env, context, routeParamsFor(step, env));
|
|
1445
|
+
if (step.skipIf && safe(() => step.skipIf(ctx), false))
|
|
1446
|
+
continue;
|
|
1447
|
+
if (wouldAutoPass(step, env))
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
else if (step.skipIf || step.onMissing === "skip") {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
n++;
|
|
1454
|
+
}
|
|
1455
|
+
return n;
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* The counter as shown when entering step `index` (shell ≥ 0.1.77). `no`
|
|
1459
|
+
* counts the steps actually shown in this run up to this one. The total is
|
|
1460
|
+
* worked out on entering a step on a new page — the shown steps so far plus
|
|
1461
|
+
* stepsAhead — and kept while the run stays on that page; it never goes
|
|
1462
|
+
* down (the larger of the old and the new), and is never below `no`.
|
|
1463
|
+
*/
|
|
1464
|
+
function countStep(d, env, mission, index) {
|
|
1465
|
+
const prev = d.state.counter?.mission === mission.key ? d.state.counter : null;
|
|
1466
|
+
const shown = prev ? (prev.shown.includes(index) ? prev.shown : [...prev.shown, index]) : [index];
|
|
1467
|
+
const no = Math.max(1, shown.filter(i => i <= index).length);
|
|
1468
|
+
let total;
|
|
1469
|
+
if (prev && prev.path === env.pathname)
|
|
1470
|
+
total = Math.max(prev.total, no);
|
|
1471
|
+
else
|
|
1472
|
+
total = Math.max(prev?.total ?? 0, no + stepsAhead(mission, index, env, d.state.progress.context), no);
|
|
1473
|
+
d.state = { ...d.state, counter: { mission: mission.key, shown, total, path: env.pathname } };
|
|
1474
|
+
return { no, total };
|
|
1475
|
+
}
|
|
1427
1476
|
/** A step entered now would pass at once: a click or select step whose route or visible outcome holds. */
|
|
1428
1477
|
function wouldAutoPass(step, env) {
|
|
1429
1478
|
const kind = stepKind(step);
|
|
@@ -1493,6 +1542,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1493
1542
|
const away = !page && !section && !!where && !matchRoute(where, env.pathname, env.search, env.code);
|
|
1494
1543
|
const bridge = !away && needsBridge(step, env, ctx);
|
|
1495
1544
|
const nts = !away && !bridge && nothingToSaveHolds(step, env, ctx);
|
|
1545
|
+
const pos = countStep(d, env, mission, i);
|
|
1496
1546
|
setScreen(d, {
|
|
1497
1547
|
name: "step",
|
|
1498
1548
|
mission: mission.key,
|
|
@@ -1510,6 +1560,7 @@ function enterStep(d, suite, env, mission, index, opts) {
|
|
|
1510
1560
|
...(page ? { page } : {}),
|
|
1511
1561
|
...(section ? { section } : {}),
|
|
1512
1562
|
...(flow ? { flow } : {}),
|
|
1563
|
+
pos,
|
|
1513
1564
|
});
|
|
1514
1565
|
if (!opts.replay) {
|
|
1515
1566
|
const cp = checkpointAtOrBefore(steps, i);
|
|
@@ -1784,7 +1835,7 @@ function startMission(d, suite, env, key, replay) {
|
|
|
1784
1835
|
const isReplay = replay || entry?.status === "done";
|
|
1785
1836
|
const before = d.state.progress;
|
|
1786
1837
|
// A new run: what passed by itself is counted afresh.
|
|
1787
|
-
d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null };
|
|
1838
|
+
d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null, counter: null };
|
|
1788
1839
|
if (d.state.progress.welcome === "not_seen")
|
|
1789
1840
|
withProgress(d, { welcome: "started" });
|
|
1790
1841
|
if (!isReplay && d.state.progress.mission_key !== key)
|
|
@@ -1934,24 +1985,33 @@ function pageStillHere(suite, key, env) {
|
|
|
1934
1985
|
const page = findPage(suite, key);
|
|
1935
1986
|
return !!page && pageMatchScore(page, whereOf(env)) !== null;
|
|
1936
1987
|
}
|
|
1937
|
-
/**
|
|
1988
|
+
/**
|
|
1989
|
+
* Whether a row may be written now: none of this version yet, or — once —
|
|
1990
|
+
* a skipped one that the person has now finished (skipped → completed,
|
|
1991
|
+
* shell ≥ 0.1.77). A completed row is final; skipped again changes nothing.
|
|
1992
|
+
*/
|
|
1993
|
+
function rowWritable(row, version, status) {
|
|
1994
|
+
if (!row || row.version < version)
|
|
1995
|
+
return true;
|
|
1996
|
+
return row.version === version && row.status === "skipped" && status === "completed";
|
|
1997
|
+
}
|
|
1998
|
+
/** Whether a run should keep its row to write at its end: no row yet, or only a skipped one. */
|
|
1999
|
+
function rowStillDue(row, version) {
|
|
2000
|
+
return rowWritable(row, version, "completed");
|
|
2001
|
+
}
|
|
2002
|
+
/** Write a page guide's row (once per page and version; skipped → completed once): recorded here at once, saved by the host. */
|
|
1938
2003
|
function recordPage(d, suite, key, status) {
|
|
1939
2004
|
const page = findPage(suite, key);
|
|
1940
|
-
if (!page ||
|
|
2005
|
+
if (!page || !rowWritable(d.state.pages[key], page.version, status))
|
|
1941
2006
|
return;
|
|
1942
2007
|
const prev = d.state.pages[key];
|
|
1943
2008
|
d.state = { ...d.state, pages: { ...d.state.pages, [key]: { version: page.version, status, rev: prev?.rev ?? 0 } } };
|
|
1944
2009
|
d.effects.push({ type: "savePage", key, version: page.version, status });
|
|
1945
2010
|
}
|
|
1946
|
-
/** The section guide's row says this person has ended this version of it. */
|
|
1947
|
-
function sectionRecorded(state, section) {
|
|
1948
|
-
const row = state.sections?.[section.key];
|
|
1949
|
-
return !!row && row.version >= section.version;
|
|
1950
|
-
}
|
|
1951
2011
|
/** Write a section guide's row (once per section and version). */
|
|
1952
2012
|
function recordSection(d, suite, key, status) {
|
|
1953
2013
|
const section = findSection(suite, key);
|
|
1954
|
-
if (!section ||
|
|
2014
|
+
if (!section || !rowWritable(d.state.sections?.[key], section.version, status))
|
|
1955
2015
|
return;
|
|
1956
2016
|
const prev = d.state.sections?.[key];
|
|
1957
2017
|
d.state = { ...d.state, sections: { ...d.state.sections, [key]: { version: section.version, status, rev: prev?.rev ?? 0 } } };
|
|
@@ -1982,7 +2042,7 @@ function flowRecorded(state, flow) {
|
|
|
1982
2042
|
/** Write a flow's row (once per flow and version). */
|
|
1983
2043
|
function recordFlow(d, suite, key, status) {
|
|
1984
2044
|
const flow = findFlow(suite, key);
|
|
1985
|
-
if (!flow ||
|
|
2045
|
+
if (!flow || !rowWritable(d.state.flows?.[key], flow.version, status))
|
|
1986
2046
|
return;
|
|
1987
2047
|
const prev = d.state.flows?.[key];
|
|
1988
2048
|
d.state = { ...d.state, flows: { ...d.state.flows, [key]: { version: flow.version, status, rev: prev?.rev ?? 0 } } };
|
|
@@ -2067,6 +2127,7 @@ function startFlow(d, suite, env, key, write, at) {
|
|
|
2067
2127
|
justDone: null,
|
|
2068
2128
|
pageRun: { key, write, kind: "flow", context: saved },
|
|
2069
2129
|
autoPassed: null,
|
|
2130
|
+
counter: null,
|
|
2070
2131
|
replayPage: {
|
|
2071
2132
|
mission: mission.key,
|
|
2072
2133
|
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
@@ -2097,6 +2158,7 @@ function startSection(d, suite, env, key, write) {
|
|
|
2097
2158
|
justDone: null,
|
|
2098
2159
|
pageRun: { key, write, kind: "section" },
|
|
2099
2160
|
autoPassed: null,
|
|
2161
|
+
counter: null,
|
|
2100
2162
|
replayPage: {
|
|
2101
2163
|
mission: mission.key,
|
|
2102
2164
|
anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
|
|
@@ -2116,6 +2178,7 @@ function startPage(d, suite, env, key, write) {
|
|
|
2116
2178
|
justDone: null,
|
|
2117
2179
|
pageRun: { key, write, kind: "page" },
|
|
2118
2180
|
autoPassed: null,
|
|
2181
|
+
counter: null,
|
|
2119
2182
|
// What is open now is the person's: ending the guide leaves it alone.
|
|
2120
2183
|
replayPage: {
|
|
2121
2184
|
mission: mission.key,
|
|
@@ -2159,6 +2222,28 @@ function backTarget(mission, index, env, context) {
|
|
|
2159
2222
|
}
|
|
2160
2223
|
return prev >= 0 ? { index: prev, close: false } : null;
|
|
2161
2224
|
}
|
|
2225
|
+
/**
|
|
2226
|
+
* A flow's next step (the next one this person sees) when the person is on
|
|
2227
|
+
* its page and no longer on the current step's: they moved on by
|
|
2228
|
+
* themselves. -1 otherwise (still on this step's page, or elsewhere).
|
|
2229
|
+
*/
|
|
2230
|
+
function flowStepArrivedAt(mission, index, env, context) {
|
|
2231
|
+
const here = (route) => !!route && !!matchRoute(route, env.pathname, env.search, env.code);
|
|
2232
|
+
const curHome = stepHomeRoute(mission.steps, index);
|
|
2233
|
+
if (!curHome || here(curHome))
|
|
2234
|
+
return -1;
|
|
2235
|
+
for (let j = index + 1; j < mission.steps.length; j++) {
|
|
2236
|
+
const st = mission.steps[j];
|
|
2237
|
+
const ctx = guideContext(env, context, routeParamsFor(st, env));
|
|
2238
|
+
if (st.requires && !safe(() => env.can(st.requires), true))
|
|
2239
|
+
continue;
|
|
2240
|
+
if (st.skipIf && safe(() => st.skipIf(ctx), false))
|
|
2241
|
+
continue;
|
|
2242
|
+
const where = st.route ?? stepHomeRoute(mission.steps, j);
|
|
2243
|
+
return here(where) ? j : -1;
|
|
2244
|
+
}
|
|
2245
|
+
return -1;
|
|
2246
|
+
}
|
|
2162
2247
|
function reduceGuide(state, action, suite, env) {
|
|
2163
2248
|
const d = { state, effects: [] };
|
|
2164
2249
|
const s = state.screen;
|
|
@@ -2236,7 +2321,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2236
2321
|
const stepAt = mission.steps.findIndex(st => st.key === d.state.progress.step_key);
|
|
2237
2322
|
const cpAt = mission.steps.findIndex(st => st.key === d.state.progress.checkpoint_key);
|
|
2238
2323
|
const index = stepAt >= 0 ? checkpointAtOrBefore(mission.steps, stepAt) : Math.max(cpAt, 0);
|
|
2239
|
-
d.state = { ...d.state, autoPassed: null };
|
|
2324
|
+
d.state = { ...d.state, autoPassed: null, counter: null };
|
|
2240
2325
|
enterStep(d, suite, env, mission, index, { replay: false });
|
|
2241
2326
|
break;
|
|
2242
2327
|
}
|
|
@@ -2279,6 +2364,15 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2279
2364
|
break;
|
|
2280
2365
|
const step = mission.steps[s.index];
|
|
2281
2366
|
const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
|
|
2367
|
+
if (s.flow && !s.confirmExit) {
|
|
2368
|
+
// Within the chain, the person went on to the next step's page by
|
|
2369
|
+
// themselves: carry on there, no "ทำต่อ" (shell ≥ 0.1.77).
|
|
2370
|
+
const next = flowStepArrivedAt(mission, s.index, env, d.state.progress.context);
|
|
2371
|
+
if (next >= 0) {
|
|
2372
|
+
enterStep(d, suite, env, mission, next, { replay: s.replay });
|
|
2373
|
+
break;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2282
2376
|
if (s.phase === "away") {
|
|
2283
2377
|
const where = placeRoute(mission.steps, s.index);
|
|
2284
2378
|
if (!where || matchRoute(where, env.pathname, env.search, env.code)) {
|
|
@@ -2673,7 +2767,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2673
2767
|
if (!flowsAvailable(suite, env).includes(action.flow))
|
|
2674
2768
|
break;
|
|
2675
2769
|
const flow = findFlow(suite, action.flow);
|
|
2676
|
-
startFlow(d, suite, env, flow.key,
|
|
2770
|
+
startFlow(d, suite, env, flow.key, rowStillDue(state.flows?.[flow.key], flow.version));
|
|
2677
2771
|
break;
|
|
2678
2772
|
}
|
|
2679
2773
|
case "resumeFlow": {
|
|
@@ -2682,7 +2776,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2682
2776
|
const flow = findFlow(suite, action.flow);
|
|
2683
2777
|
if (!flow || !flowShown(flow, guideContext(env, {})) || !inFlowChain(flow, whereOf(env)))
|
|
2684
2778
|
break;
|
|
2685
|
-
startFlow(d, suite, env, flow.key, action.write &&
|
|
2779
|
+
startFlow(d, suite, env, flow.key, action.write && rowStillDue(state.flows?.[flow.key], flow.version), { step: action.step, context: action.context });
|
|
2686
2780
|
break;
|
|
2687
2781
|
}
|
|
2688
2782
|
case "startPage": {
|
|
@@ -2694,7 +2788,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2694
2788
|
const page = pageHere(suite, env);
|
|
2695
2789
|
if (!page || page.key !== action.page)
|
|
2696
2790
|
break;
|
|
2697
|
-
startPage(d, suite, env, page.key,
|
|
2791
|
+
startPage(d, suite, env, page.key, rowStillDue(state.pages[page.key], page.version));
|
|
2698
2792
|
break;
|
|
2699
2793
|
}
|
|
2700
2794
|
case "back": {
|
|
@@ -2743,7 +2837,7 @@ function reduceGuide(state, action, suite, env) {
|
|
|
2743
2837
|
if (!sectionsAvailable(suite, env).includes(action.section))
|
|
2744
2838
|
break;
|
|
2745
2839
|
const section = findSection(suite, action.section);
|
|
2746
|
-
startSection(d, suite, env, section.key,
|
|
2840
|
+
startSection(d, suite, env, section.key, rowStillDue(state.sections?.[section.key], section.version));
|
|
2747
2841
|
break;
|
|
2748
2842
|
}
|
|
2749
2843
|
case "idleCelebrate": {
|
|
@@ -3189,7 +3283,10 @@ async function storePageRow(adapter, app, key, version, status, rev, kind = "pag
|
|
|
3189
3283
|
}
|
|
3190
3284
|
if (current.version > version)
|
|
3191
3285
|
return null;
|
|
3192
|
-
|
|
3286
|
+
// This version already recorded: kept, except a skipped row this run
|
|
3287
|
+
// finished (skipped → completed, once, shell ≥ 0.1.77).
|
|
3288
|
+
const finishing = status === "completed" && current.status === "skipped";
|
|
3289
|
+
if (current.version === version && current.status !== "in_progress" && !finishing)
|
|
3193
3290
|
return current;
|
|
3194
3291
|
at = current.rev;
|
|
3195
3292
|
}
|
|
@@ -3856,6 +3953,9 @@ function useNudge(nudge, ref) {
|
|
|
3856
3953
|
}
|
|
3857
3954
|
function StepLayer(p) {
|
|
3858
3955
|
const { spot, phone, viewport } = p;
|
|
3956
|
+
// Where the bubble may sit: the content column when the app has one.
|
|
3957
|
+
const col = p.bounds && p.bounds.width > 0 ? p.bounds : { x: 0, width: viewport.width || (typeof window !== "undefined" ? window.innerWidth : 0)};
|
|
3958
|
+
const colKey = `${col.x}|${col.width}`;
|
|
3859
3959
|
const bubbleRef = React.useRef(null);
|
|
3860
3960
|
const arrowRef = React.useRef(null);
|
|
3861
3961
|
const ringRef = React.useRef(null);
|
|
@@ -3896,8 +3996,8 @@ function StepLayer(p) {
|
|
|
3896
3996
|
const bw = el.offsetWidth;
|
|
3897
3997
|
const bh = el.offsetHeight;
|
|
3898
3998
|
setPos(phone
|
|
3899
|
-
? { left: m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
|
|
3900
|
-
: { left: (
|
|
3999
|
+
? { left: col.x + m, top: viewport.offsetTop + viewport.height - bh - m, side: "dock" }
|
|
4000
|
+
: { left: col.x + (col.width - bw) / 2, top: viewport.offsetTop + (viewport.height - bh) / 2, side: "dock" });
|
|
3901
4001
|
return;
|
|
3902
4002
|
}
|
|
3903
4003
|
if (phone) {
|
|
@@ -3918,7 +4018,7 @@ function StepLayer(p) {
|
|
|
3918
4018
|
: overlapBot <= overlapTop
|
|
3919
4019
|
? botT
|
|
3920
4020
|
: topT;
|
|
3921
|
-
setPos({ left: m, top, side: "dock" });
|
|
4021
|
+
setPos({ left: col.x + m, top, side: "dock" });
|
|
3922
4022
|
return;
|
|
3923
4023
|
}
|
|
3924
4024
|
let cancelled = false;
|
|
@@ -3940,7 +4040,7 @@ function StepLayer(p) {
|
|
|
3940
4040
|
placement,
|
|
3941
4041
|
middleware: [
|
|
3942
4042
|
dom.offset(placement.startsWith("bottom") ? gap : 14),
|
|
3943
|
-
dom.shift({ padding: m }),
|
|
4043
|
+
dom.shift({ padding: m, rootBoundary: { x: col.x, y: 0, width: col.width, height: window.innerHeight } }),
|
|
3944
4044
|
...(arrowRef.current ? [dom.arrow({ element: arrowRef.current, padding: 14 })] : []),
|
|
3945
4045
|
],
|
|
3946
4046
|
});
|
|
@@ -3950,11 +4050,11 @@ function StepLayer(p) {
|
|
|
3950
4050
|
return;
|
|
3951
4051
|
const bw = el.offsetWidth;
|
|
3952
4052
|
const bh = el.offsetHeight;
|
|
3953
|
-
const bounds = { x: m, y: viewport.offsetTop + m, width:
|
|
4053
|
+
const bounds = { x: col.x + m, y: viewport.offsetTop + m, width: col.width - 2 * m, height: viewport.height - 2 * m };
|
|
3954
4054
|
const i = pickPlacement(results, bw, bh, a, clear, bounds);
|
|
3955
4055
|
if (i < 0) {
|
|
3956
4056
|
// Nowhere beside it: the bottom-right corner.
|
|
3957
|
-
setPos({ left:
|
|
4057
|
+
setPos({ left: col.x + col.width - bw - m, top: viewport.height + viewport.offsetTop - bh - m, side: "dock" });
|
|
3958
4058
|
return;
|
|
3959
4059
|
}
|
|
3960
4060
|
const { x, y, placement, middlewareData } = results[i];
|
|
@@ -3970,7 +4070,7 @@ function StepLayer(p) {
|
|
|
3970
4070
|
};
|
|
3971
4071
|
// The keys stand for spot, the lists, the holes and the bubble's size.
|
|
3972
4072
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3973
|
-
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop]);
|
|
4073
|
+
}, [spotKey, anchorKey, clearKey, contentKey, phone, showBubble, gap, viewport.height, viewport.offsetTop, colKey]);
|
|
3974
4074
|
const popovers = p.popovers ?? [];
|
|
3975
4075
|
const covered = !!p.covered;
|
|
3976
4076
|
const holes = [...(spot && !covered ? [spot] : []), ...popovers, ...p.holes];
|
|
@@ -3991,7 +4091,7 @@ function StepLayer(p) {
|
|
|
3991
4091
|
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.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 ? (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: {
|
|
3992
4092
|
left: pos?.left ?? 0,
|
|
3993
4093
|
top: pos?.top ?? 0,
|
|
3994
|
-
width: phone ?
|
|
4094
|
+
width: phone ? col.width - 32 : col.width < 352 ? col.width - 32 : undefined,
|
|
3995
4095
|
visibility: pos ? "visible" : "hidden",
|
|
3996
4096
|
}, children: [jsxRuntime.jsx(BubbleBody, { ...p }), jsxRuntime.jsx("span", { ref: arrowRef, className: "eg-arrow" + (pos?.side === "bottom" ? " eg-top" : ""), style: arrowStyle ?? { display: "none" }, "aria-hidden": "true" })] })) : null, p.phase === "waiting" ? (jsxRuntime.jsxs("div", { className: "eg-card", role: "status", "data-eg-card": "", children: [jsxRuntime.jsxs("div", { className: "eg-loading", children: [jsxRuntime.jsx("span", { className: "eg-spin", "aria-hidden": "true" }), "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E42\u0E2B\u0E25\u0E14\u2026"] }), jsxRuntime.jsx("div", { className: "eg-row", children: jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }) })] })) : null, p.phase === "missing" ? (jsxRuntime.jsxs("div", { className: "eg-card eg-err", role: "alertdialog", "aria-labelledby": "eg-missing-t", "data-eg-card": "", tabIndex: -1, children: [jsxRuntime.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" }), jsxRuntime.jsxs("div", { className: "eg-row", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }), jsxRuntime.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" ? (jsxRuntime.jsxs("div", { className: "eg-card", role: "dialog", "aria-modal": "false", "aria-labelledby": "eg-away-t", "data-eg-card": "", tabIndex: -1, children: [jsxRuntime.jsxs("div", { className: "eg-b-top", children: [jsxRuntime.jsx("span", { className: "eg-mis", children: headingOf(p.bubble) }), jsxRuntime.jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", p.bubble.index + 1, "/", p.bubble.total] })] }), jsxRuntime.jsx("p", { className: "eg-b-t", id: "eg-away-t", style: { marginTop: 8 }, children: jsxRuntime.jsx(GuideTitle, { text: p.bubble.title }) }), jsxRuntime.jsxs("div", { className: "eg-row", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: exitLabelOf(p.bubble) }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onGoResume, children: "\u0E17\u0E33\u0E15\u0E48\u0E2D" })] })] })) : null, p.confirmExit ? (jsxRuntime.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] }));
|
|
3997
4097
|
}
|
|
@@ -4359,6 +4459,42 @@ function runAbort(actions) {
|
|
|
4359
4459
|
}, ABORT_DELAY_MS + i * ABORT_GAP_MS);
|
|
4360
4460
|
});
|
|
4361
4461
|
}
|
|
4462
|
+
/** The app's content column (GuideConfig.contentRoot), cut to the viewport; null when none is declared or found. */
|
|
4463
|
+
function contentColumn(selector, vw) {
|
|
4464
|
+
if (!selector || typeof document === "undefined")
|
|
4465
|
+
return null;
|
|
4466
|
+
let el = null;
|
|
4467
|
+
try {
|
|
4468
|
+
el = document.querySelector(selector);
|
|
4469
|
+
}
|
|
4470
|
+
catch {
|
|
4471
|
+
return null;
|
|
4472
|
+
}
|
|
4473
|
+
if (!el)
|
|
4474
|
+
return null;
|
|
4475
|
+
const r = el.getBoundingClientRect();
|
|
4476
|
+
const x = Math.max(0, r.left);
|
|
4477
|
+
const right = Math.min(vw || window.innerWidth, r.right);
|
|
4478
|
+
if (right - x <= 0)
|
|
4479
|
+
return null;
|
|
4480
|
+
return { x, y: 0, width: right - x, height: 0 };
|
|
4481
|
+
}
|
|
4482
|
+
/** The control a form's implicit submit (Enter in a field) presses: its first submit button. */
|
|
4483
|
+
function defaultSubmitControl(form) {
|
|
4484
|
+
for (const el of Array.from(form.elements)) {
|
|
4485
|
+
if (el instanceof HTMLButtonElement && (el.type === "submit" || !el.getAttribute("type")))
|
|
4486
|
+
return el;
|
|
4487
|
+
if (el instanceof HTMLInputElement && (el.type === "submit" || el.type === "image"))
|
|
4488
|
+
return el;
|
|
4489
|
+
}
|
|
4490
|
+
return null;
|
|
4491
|
+
}
|
|
4492
|
+
/** A field where Enter submits its form (not a textarea, a button or a select). */
|
|
4493
|
+
function submitsOnEnter(el) {
|
|
4494
|
+
if (!(el instanceof HTMLInputElement))
|
|
4495
|
+
return false;
|
|
4496
|
+
return !["button", "submit", "reset", "image", "file", "hidden"].includes(el.type);
|
|
4497
|
+
}
|
|
4362
4498
|
/** A step's own target takes presses: not on a "view" step, unless the step allows it. */
|
|
4363
4499
|
function targetPressable(step, bridge) {
|
|
4364
4500
|
if (!step || bridge)
|
|
@@ -4737,6 +4873,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4737
4873
|
const stepId = screen.name === "step" ? `${screen.mission}:${screen.index}:${screen.bridge ? "b" : ""}${screen.nothingToSave ? "n" : ""}` : "";
|
|
4738
4874
|
/* Measure every frame while a step is shown: the spotlight follows
|
|
4739
4875
|
scrolling, dialogs animating in, the drawer sliding, the keyboard. */
|
|
4876
|
+
const contentRoot = config.contentRoot;
|
|
4740
4877
|
const [layout, setLayout] = React.useState({
|
|
4741
4878
|
spot: null,
|
|
4742
4879
|
covered: false,
|
|
@@ -4745,6 +4882,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4745
4882
|
vw: 0,
|
|
4746
4883
|
vh: 0,
|
|
4747
4884
|
oy: 0,
|
|
4885
|
+
col: null,
|
|
4748
4886
|
});
|
|
4749
4887
|
React.useEffect(() => {
|
|
4750
4888
|
if (!inStep) {
|
|
@@ -4753,7 +4891,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4753
4891
|
}
|
|
4754
4892
|
let raf = 0;
|
|
4755
4893
|
let lostSince = null;
|
|
4756
|
-
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0 };
|
|
4894
|
+
let last = { spot: null, covered: false, holes: [], popovers: [], vw: 0, vh: 0, oy: 0, col: null };
|
|
4757
4895
|
targetElRef.current = target ? findAnchor(target.anchor, target.id) : null;
|
|
4758
4896
|
const allow = stepInfo?.step.allow ?? [];
|
|
4759
4897
|
// The spotlight is the part of the target a person can see: cut by the
|
|
@@ -4813,13 +4951,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4813
4951
|
holes.push(padRect(rectOf(ael), TARGET_PAD));
|
|
4814
4952
|
}
|
|
4815
4953
|
const covered = !!spot && coveredByDialog(targetElRef.current);
|
|
4816
|
-
const
|
|
4954
|
+
const col = contentColumn(contentRoot, vw);
|
|
4955
|
+
const next = { spot, covered, holes, popovers, vw, vh, oy, col };
|
|
4817
4956
|
const sameList = (a, b) => a.length === b.length && a.every((h, i) => sameRect(h, b[i]));
|
|
4818
4957
|
if (!sameRect(next.spot, last.spot) ||
|
|
4819
4958
|
next.covered !== last.covered ||
|
|
4820
4959
|
next.vw !== last.vw ||
|
|
4821
4960
|
next.vh !== last.vh ||
|
|
4822
4961
|
next.oy !== last.oy ||
|
|
4962
|
+
!sameRect(next.col, last.col) ||
|
|
4823
4963
|
!sameList(next.holes, last.holes) ||
|
|
4824
4964
|
!sameList(next.popovers, last.popovers)) {
|
|
4825
4965
|
last = next;
|
|
@@ -4974,6 +5114,43 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
4974
5114
|
}
|
|
4975
5115
|
return false;
|
|
4976
5116
|
};
|
|
5117
|
+
/**
|
|
5118
|
+
* Whether a form may be submitted during this step (shell ≥ 0.1.77): only
|
|
5119
|
+
* when its submit control — the button pressed, or the one Enter in a
|
|
5120
|
+
* field would press — is the step's pressable target or in its `allow`.
|
|
5121
|
+
* Otherwise Enter in an allowed field would save a draft (and take a
|
|
5122
|
+
* document number) behind the guide's back.
|
|
5123
|
+
*/
|
|
5124
|
+
const submitAllowed = (form, submitter) => {
|
|
5125
|
+
const btn = submitter ?? defaultSubmitControl(form);
|
|
5126
|
+
if (!btn)
|
|
5127
|
+
return false;
|
|
5128
|
+
const el = targetElRef.current;
|
|
5129
|
+
const sc = stateRef.current?.screen;
|
|
5130
|
+
const pressable = targetPressable(stepInfoRef.current?.step, sc?.name === "step" && sc.bridge);
|
|
5131
|
+
if (el && pressable && el.contains(btn))
|
|
5132
|
+
return true;
|
|
5133
|
+
for (const a of stepInfoRef.current?.step.allow ?? []) {
|
|
5134
|
+
const ael = findAnchor(a);
|
|
5135
|
+
if (ael && ael.contains(btn))
|
|
5136
|
+
return true;
|
|
5137
|
+
}
|
|
5138
|
+
return false;
|
|
5139
|
+
};
|
|
5140
|
+
const onSubmit = (e) => {
|
|
5141
|
+
const sc = stateRef.current?.screen;
|
|
5142
|
+
if (!guardingRef.current || sc?.name !== "step")
|
|
5143
|
+
return;
|
|
5144
|
+
const form = e.target;
|
|
5145
|
+
if (!(form instanceof HTMLFormElement) || inGuideLayer(form))
|
|
5146
|
+
return;
|
|
5147
|
+
const submitter = e.submitter ?? null;
|
|
5148
|
+
if (submitAllowed(form, submitter))
|
|
5149
|
+
return;
|
|
5150
|
+
e.preventDefault();
|
|
5151
|
+
e.stopPropagation();
|
|
5152
|
+
dispatchRef.current({ type: "blocked" });
|
|
5153
|
+
};
|
|
4977
5154
|
const onPointer = (e) => {
|
|
4978
5155
|
const t = e.target;
|
|
4979
5156
|
if (inGuideLayer(t)) {
|
|
@@ -5093,8 +5270,15 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5093
5270
|
return;
|
|
5094
5271
|
}
|
|
5095
5272
|
}
|
|
5096
|
-
if (modalRef.current || !allowedTarget(active))
|
|
5273
|
+
if (modalRef.current || !allowedTarget(active)) {
|
|
5097
5274
|
stop();
|
|
5275
|
+
return;
|
|
5276
|
+
}
|
|
5277
|
+
// Enter in a field the step lets the person type in: no implicit
|
|
5278
|
+
// submit unless the form's submit control is allowed now.
|
|
5279
|
+
if (e.key === "Enter" && sc.name === "step" && submitsOnEnter(active) && active.form && !submitAllowed(active.form, null)) {
|
|
5280
|
+
e.preventDefault();
|
|
5281
|
+
}
|
|
5098
5282
|
}
|
|
5099
5283
|
};
|
|
5100
5284
|
window.addEventListener("pointerdown", onPointer, true);
|
|
@@ -5104,7 +5288,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5104
5288
|
window.addEventListener("focusin", onFocusIn, true);
|
|
5105
5289
|
window.addEventListener("focusout", onFocusOut, true);
|
|
5106
5290
|
window.addEventListener("keydown", onKey, true);
|
|
5291
|
+
window.addEventListener("submit", onSubmit, true);
|
|
5107
5292
|
return () => {
|
|
5293
|
+
window.removeEventListener("submit", onSubmit, true);
|
|
5108
5294
|
window.removeEventListener("pointerdown", onPointer, true);
|
|
5109
5295
|
window.removeEventListener("mousedown", onPointer, true);
|
|
5110
5296
|
window.removeEventListener("touchstart", onPointer, true);
|
|
@@ -5532,7 +5718,11 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5532
5718
|
const missionNo = missionNumber(suite, mission.key, ctxIdle);
|
|
5533
5719
|
const kind = stepKind(step);
|
|
5534
5720
|
const bridge = screen.bridge;
|
|
5535
|
-
|
|
5721
|
+
// What is ahead now decides เสร็จ; the number shown is the run's counter,
|
|
5722
|
+
// which never goes down (0.1.77).
|
|
5723
|
+
const raw = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
|
|
5724
|
+
const last = raw.no >= raw.total;
|
|
5725
|
+
const pos = screen.pos ?? raw;
|
|
5536
5726
|
const page = screen.page ? findPage(suite, screen.page) : undefined;
|
|
5537
5727
|
const section = screen.section ? findSection(suite, screen.section) : undefined;
|
|
5538
5728
|
const flow = screen.flow ? findFlow(suite, screen.flow) : undefined;
|
|
@@ -5541,16 +5731,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
|
|
|
5541
5731
|
? {
|
|
5542
5732
|
...read,
|
|
5543
5733
|
showBack: kind === "view" && !!backTarget(mission, screen.index, env(), state.progress.context),
|
|
5544
|
-
nextLabel:
|
|
5734
|
+
nextLabel: last ? "เสร็จ" : undefined,
|
|
5545
5735
|
}
|
|
5546
5736
|
: flow
|
|
5547
5737
|
? {
|
|
5548
5738
|
heading: `พาทำ · ${flow.title}`,
|
|
5549
5739
|
exitLabel: "ออกจากการพาทำ",
|
|
5550
|
-
nextLabel: kind === "view" &&
|
|
5740
|
+
nextLabel: kind === "view" && last ? "เสร็จ" : undefined,
|
|
5551
5741
|
}
|
|
5552
5742
|
: {};
|
|
5553
|
-
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: {
|
|
5743
|
+
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 }, bounds: layout.col, phone: phone, showHand: bridge || (kind !== "input" && kind !== "view"), bubble: {
|
|
5554
5744
|
missionNo,
|
|
5555
5745
|
missionTitle: mission.title,
|
|
5556
5746
|
replay: screen.replay,
|
|
@@ -5766,4 +5956,4 @@ exports.useGuideEntry = useGuideEntry;
|
|
|
5766
5956
|
exports.usePageGuideEntry = usePageGuideEntry;
|
|
5767
5957
|
exports.usePageGuides = usePageGuides;
|
|
5768
5958
|
exports.useSectionGuideValues = useSectionGuideValues;
|
|
5769
|
-
//# sourceMappingURL=format-
|
|
5959
|
+
//# sourceMappingURL=format-DQLPHq7h.cjs.map
|