@eintrek/erp-shell 0.1.71 → 0.1.73

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 CHANGED
@@ -176,6 +176,28 @@ 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.73
180
+
181
+ - `GuideStep.status`: a short status line in the bubble while it returns
182
+ text, read again on every page change. A select step names a wrong pick
183
+ ("ยังไม่ใช่ชื่อคุณ") instead of waiting in silence (owner, 3 Nov 2026:
184
+ Plannex's managers step, someone else picked, nothing moved).
185
+ - In a replay (ทบทวน), a select step offers ถัดไป when the next step's target
186
+ is already on the page (a form's save button), so a review never has to
187
+ change a real record to go on. Not while the next target needs the pick
188
+ (Plannex's save shows only once the user is a manager). First runs are
189
+ unchanged.
190
+ - The "?" button opens the help dialog straight away, without the
191
+ "ไกด์เริ่มต้น 0/0" row, when every mission is hidden for this user
192
+ (`guideMenuShown`). `useGuideEntry()` still answers `{ done, total }`;
193
+ an app's own entry row should hide at `total === 0` too.
194
+
195
+ ### 0.1.72
196
+
197
+ - "ขั้นที่ x/y" counts only the steps this person will see: a step whose
198
+ `requires` they lack or whose `skipIf` holds is left out (Plannex mission 3
199
+ said 1/6 and ended at 5/6).
200
+
179
201
  ### 0.1.71
180
202
 
181
203
  - Escape on the finish card closes it where the person is. Only the button
@@ -337,6 +337,16 @@ function publishGuideEntry(next, openPanel) {
337
337
  emit();
338
338
  }
339
339
  }
340
+ /**
341
+ * Whether the "?" menu offers "ไกด์เริ่มต้น": a tour whose missions are all
342
+ * hidden for this user (`visible`, e.g. Plannex without an employee record)
343
+ * has nothing to open, and "0/0" led to an empty panel. The entry itself
344
+ * stays published: apps wait on it before they load the facts that make
345
+ * missions visible.
346
+ */
347
+ function guideMenuShown(e) {
348
+ return !!e && e.total > 0;
349
+ }
340
350
  /** Opens the tour's quest panel, when this user has a tour here. */
341
351
  function openGuidePanel() {
342
352
  openPanelImpl?.();
@@ -727,11 +737,34 @@ function announceStep(d, suite, env) {
727
737
  const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
728
738
  const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
729
739
  const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
740
+ const pos = stepPosition(mission, s.index, env, d.state.progress.context);
730
741
  d.effects.push({
731
742
  type: "announce",
732
- text: `ขั้นที่ ${s.index + 1} จาก ${mission.steps.length}. ${title}${why ? `. ${why}` : ""}`,
743
+ text: `ขั้นที่ ${pos.no} จาก ${pos.total}. ${title}${why ? `. ${why}` : ""}`,
733
744
  });
734
745
  }
746
+ /**
747
+ * "ขั้นที่ no จาก total" counting only the steps this person will see: a step
748
+ * whose `requires` they lack or whose `skipIf` holds is left out (the step on
749
+ * screen always counts).
750
+ */
751
+ function stepPosition(mission, index, env, context) {
752
+ let no = 0;
753
+ let total = 0;
754
+ mission.steps.forEach((step, i) => {
755
+ if (i !== index) {
756
+ if (step.requires && !safe(() => env.can(step.requires), true))
757
+ return;
758
+ const ctx = guideContext(env, context, routeParamsFor(step, env));
759
+ if (step.skipIf && safe(() => step.skipIf(ctx), false))
760
+ return;
761
+ }
762
+ total++;
763
+ if (i <= index)
764
+ no = total;
765
+ });
766
+ return { no: Math.max(no, 1), total: Math.max(total, 1) };
767
+ }
735
768
  /** "กด [บันทึก]" → "กด บันทึก" (for screen readers). */
736
769
  function plainTitle(t) {
737
770
  return t.replace(/\[([^\]]+)\]/g, "$1");
@@ -780,6 +813,7 @@ function enterStep(d, suite, env, mission, index, opts) {
780
813
  const where = placeRoute(steps, i);
781
814
  const away = !!where && !matchRoute(where, env.pathname, env.search, env.code);
782
815
  const bridge = !away && needsBridge(step, env, ctx);
816
+ const nts = !away && !bridge && nothingToSaveHolds(step, env, ctx);
783
817
  setScreen(d, {
784
818
  name: "step",
785
819
  mission: mission.key,
@@ -791,7 +825,9 @@ function enterStep(d, suite, env, mission, index, opts) {
791
825
  confirmExit: false,
792
826
  misses: 0,
793
827
  armed: hiddenAnchorShown(step, env),
794
- nothingToSave: !away && !bridge && nothingToSaveHolds(step, env, ctx),
828
+ nothingToSave: nts,
829
+ status: away || bridge ? null : stepStatus(shownStep(step, nts), ctx),
830
+ replayNext: !away && !bridge && opts.replay && replayCanGoOn(mission, i, env, d.state.progress.context, nts),
795
831
  });
796
832
  if (!opts.replay) {
797
833
  const cp = checkpointAtOrBefore(steps, i);
@@ -834,6 +870,54 @@ function setNothingToSave(d, suite, env, on) {
834
870
  if (stepTargetVisible(step, env, ctx, s.bridge))
835
871
  announceStep(d, suite, env);
836
872
  }
873
+ /** The step's status line (GuideStep.status): trimmed text, or null. */
874
+ function stepStatus(step, ctx) {
875
+ if (!step.status)
876
+ return null;
877
+ const text = safe(() => step.status(ctx), undefined);
878
+ return typeof text === "string" && text.trim() ? text.trim() : null;
879
+ }
880
+ /**
881
+ * In a replay, whether a "select" step may go on with ถัดไป: the next step
882
+ * this person sees has its target on the page already (or there is none).
883
+ * A pick that makes the next step's target (a save shown only once the
884
+ * right value is chosen) is still needed.
885
+ */
886
+ function replayCanGoOn(mission, index, env, context, nothingToSave = false) {
887
+ if (stepKind(shownStep(mission.steps[index], nothingToSave)) !== "select")
888
+ return false;
889
+ for (let i = index + 1; i < mission.steps.length; i++) {
890
+ const next = mission.steps[i];
891
+ const ctx = guideContext(env, context, routeParamsFor(next, env));
892
+ if (next.requires && !safe(() => env.can(next.requires), true))
893
+ continue;
894
+ if (next.skipIf && safe(() => next.skipIf(ctx), false))
895
+ continue;
896
+ if (!next.target)
897
+ return true;
898
+ if (stepTargetVisible(next, env, ctx, false))
899
+ return true;
900
+ // A commit step with nothing to save is read on its saved state.
901
+ return nothingToSaveHolds(next, env, ctx);
902
+ }
903
+ return true;
904
+ }
905
+ /** Keep the status line and a replay's ถัดไป current (the page changed). */
906
+ function syncStatus(d, suite, env) {
907
+ const s = d.state.screen;
908
+ if (s.name !== "step")
909
+ return;
910
+ const mission = findMission(suite, s.mission);
911
+ if (!mission)
912
+ return;
913
+ const step = shownStep(mission.steps[s.index], s.nothingToSave);
914
+ const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
915
+ const off = s.phase === "away" || s.bridge;
916
+ const status = off ? null : stepStatus(step, ctx);
917
+ const replayNext = !off && s.replay && replayCanGoOn(mission, s.index, env, d.state.progress.context, s.nothingToSave);
918
+ if (status !== s.status || replayNext !== s.replayNext)
919
+ setScreen(d, { ...s, status, replayNext });
920
+ }
837
921
  function markReadyIfVisible(d, suite, env) {
838
922
  const s = d.state.screen;
839
923
  if (s.name !== "step" || s.phase !== "waiting")
@@ -1219,6 +1303,7 @@ function reduceGuide(state, action, suite, env) {
1219
1303
  else if (!cur.bridge && needsBridge(step, env, ctx)) {
1220
1304
  setScreen(d, { ...cur, bridge: true, phase: "waiting" });
1221
1305
  }
1306
+ syncStatus(d, suite, env);
1222
1307
  markReadyIfVisible(d, suite, env);
1223
1308
  break;
1224
1309
  }
@@ -1281,6 +1366,12 @@ function reduceGuide(state, action, suite, env) {
1281
1366
  advance(d, suite, env);
1282
1367
  break;
1283
1368
  }
1369
+ if (kind === "select") {
1370
+ // A replay goes on without a pick when the next step is on the page.
1371
+ if (s.replay && replayCanGoOn(mission, s.index, env, d.state.progress.context, s.nothingToSave))
1372
+ advance(d, suite, env);
1373
+ break;
1374
+ }
1284
1375
  if (kind !== "input")
1285
1376
  break;
1286
1377
  const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
@@ -2287,6 +2378,7 @@ const GUIDE_CSS = `
2287
2378
  .eg-b-t{margin:0;font-size:16px;font-weight:600;line-height:1.45}
2288
2379
  .eg-b-w{margin:2px 0 0;font-size:13.5px;color:var(--eg-muted)}
2289
2380
  .eg-bubble.eg-err .eg-b-t{color:var(--eg-danger)}
2381
+ .eg-b-s{margin:6px 0 0;font-size:13px;font-weight:600;color:var(--eg-danger)}
2290
2382
  .eg-k{display:inline-block;background:var(--eg-tint);color:var(--eg-strong);border-radius:7px;padding:0 7px;font-weight:600}
2291
2383
  .eg-b-f{display:flex;align-items:center;justify-content:space-between;margin-top:12px;gap:8px}
2292
2384
  .eg-b-f .eg-btn{height:34px;padding:0 14px;font-size:13.5px}
@@ -2520,7 +2612,7 @@ function StepLayer(p) {
2520
2612
  const showBubble = p.phase === "ready" && (!!spot || !p.hasTarget);
2521
2613
  const gap = p.showHand ? 34 : 14;
2522
2614
  const spotKey = spot ? `${spot.x}|${spot.y}|${spot.width}|${spot.height}` : "";
2523
- const contentKey = `${p.bubble.title}|${p.bubble.why}|${p.bubble.error?.title ?? ""}|${p.bubble.showNext}`;
2615
+ const contentKey = `${p.bubble.title}|${p.bubble.why}|${p.bubble.error?.title ?? ""}|${p.bubble.status ?? ""}|${p.bubble.showNext}`;
2524
2616
  // What the bubble sits beside: the target, with an open list over it (the
2525
2617
  // list is what the person is choosing from now). What it must not cover:
2526
2618
  // open lists, and the other anchors the step allows (a map step's
@@ -2643,7 +2735,7 @@ function BubbleBody(p) {
2643
2735
  const b = p.bubble;
2644
2736
  const title = b.error ? b.error.title : b.title;
2645
2737
  const why = b.error ? (b.error.detail ?? "") : b.why;
2646
- return (jsxs(Fragment, { children: [jsxs("div", { className: "eg-b-top", children: [jsxs("span", { className: "eg-mis", children: ["\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08 ", b.missionNo, " \u00B7 ", b.missionTitle, b.replay ? " (ทบทวน)" : ""] }), jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", b.index + 1, "/", b.total] })] }), jsx("div", { className: "eg-pips", "aria-hidden": "true", children: Array.from({ length: b.total }, (_, i) => (jsx("i", { className: i < b.index ? "eg-d" : i === b.index ? "eg-c" : undefined }, i))) }), jsx("p", { className: "eg-b-t", id: BUBBLE_TITLE_ID, children: jsx(GuideTitle, { text: title }) }), jsx("p", { className: "eg-b-w", id: BUBBLE_WHY_ID, hidden: !why, children: jsx(GuideTitle, { text: why }) }), jsxs("div", { className: "eg-b-f", children: [jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: b.replay ? "จบการทบทวน" : "ออกจากไกด์" }), jsxs("span", { style: { display: "flex", gap: 6 }, children: [b.showEndWithoutSave ? (jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: p.onEndWithoutSave, children: "\u0E08\u0E1A\u0E42\u0E14\u0E22\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01" })) : null, b.showNext ? (jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onNext, children: "\u0E16\u0E31\u0E14\u0E44\u0E1B" })) : null] })] })] }));
2738
+ return (jsxs(Fragment, { children: [jsxs("div", { className: "eg-b-top", children: [jsxs("span", { className: "eg-mis", children: ["\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08 ", b.missionNo, " \u00B7 ", b.missionTitle, b.replay ? " (ทบทวน)" : ""] }), jsxs("span", { className: "eg-cnt", children: ["\u0E02\u0E31\u0E49\u0E19\u0E17\u0E35\u0E48 ", b.index + 1, "/", b.total] })] }), jsx("div", { className: "eg-pips", "aria-hidden": "true", children: Array.from({ length: b.total }, (_, i) => (jsx("i", { className: i < b.index ? "eg-d" : i === b.index ? "eg-c" : undefined }, i))) }), jsx("p", { className: "eg-b-t", id: BUBBLE_TITLE_ID, children: jsx(GuideTitle, { text: title }) }), jsx("p", { className: "eg-b-w", id: BUBBLE_WHY_ID, hidden: !why, children: jsx(GuideTitle, { text: why }) }), b.status && !b.error ? (jsx("p", { className: "eg-b-s", role: "status", "data-eg-status": "", children: b.status })) : null, jsxs("div", { className: "eg-b-f", children: [jsx("button", { type: "button", className: "eg-exit", onClick: p.onExit, children: b.replay ? "จบการทบทวน" : "ออกจากไกด์" }), jsxs("span", { style: { display: "flex", gap: 6 }, children: [b.showEndWithoutSave ? (jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: p.onEndWithoutSave, children: "\u0E08\u0E1A\u0E42\u0E14\u0E22\u0E44\u0E21\u0E48\u0E1A\u0E31\u0E19\u0E17\u0E36\u0E01" })) : null, b.showNext ? (jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onNext, children: "\u0E16\u0E31\u0E14\u0E44\u0E1B" })) : null] })] })] }));
2647
2739
  }
2648
2740
  /** The fingertip at ≈62% across and down the target; upside down near the bottom. */
2649
2741
  function handPosition(spot, viewportHeight) {
@@ -3789,12 +3881,14 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
3789
3881
  missionNo,
3790
3882
  missionTitle: mission.title,
3791
3883
  replay: screen.replay,
3792
- index: screen.index,
3793
- total: mission.steps.length,
3884
+ // Steps this person skips (requires / skipIf) are not counted.
3885
+ index: stepPosition(mission, screen.index, env(), state.progress.context).no - 1,
3886
+ total: stepPosition(mission, screen.index, env(), state.progress.context).total,
3794
3887
  title: bridge ? "กดปุ่มเมนู" : resolveText(step.title, stepCtx),
3795
3888
  why: bridge ? `เมนูทั้งหมดของ ${suite.appName} อยู่ในนี้` : resolveText(step.why, stepCtx),
3796
3889
  error: screen.error,
3797
- showNext: !bridge && (kind === "input" || kind === "view"),
3890
+ status: bridge ? null : screen.status,
3891
+ showNext: !bridge && (kind === "input" || kind === "view" || (kind === "select" && screen.replay && screen.replayNext)),
3798
3892
  showEndWithoutSave: !bridge && screen.replay && kind === "commit",
3799
3893
  }, hasTarget: bridge || !!step.target, targetPressable: targetPressable(step, bridge), phase: screen.phase, confirmExit: screen.confirmExit, reopenHint: suite.reopenHint, nudge: state.nudge, moving: moving, onExit: () => dispatch({ type: "exit" }), onNext: () => dispatch({ type: "next" }), onEndWithoutSave: () => dispatch({ type: "endWithoutSave" }), onRetry: () => dispatch({ type: "retry" }), onGoResume: () => dispatch({ type: "goResume" }), onExitConfirm: () => dispatch({ type: "exitConfirm" }), onExitCancel: () => dispatch({ type: "exitCancel" }) }));
3800
3894
  }
@@ -3846,5 +3940,5 @@ function useFactsKey(facts) {
3846
3940
  }, [facts]);
3847
3941
  }
3848
3942
 
3849
- export { API_EVENT as A, GUIDE_EVENT_PREFIX as G, GUIDE_LAYER_ATTRIBUTE as a, GuideErrorBoundary as b, GuideProgressConflict as c, GuideRuntime as d, apiCallDetail as e, emitGuideEvent as f, fillRoute as g, guideAnchor as h, matchRoute as i, reportApiEvent as j, loadGuides as l, matchApiPath as m, openGuidePanel as o, reportApiCall as r, storeGuides as s, useGuideEntry as u };
3850
- //# sourceMappingURL=guide-runtime-BTatpm-V.esm.js.map
3943
+ export { API_EVENT as A, GUIDE_EVENT_PREFIX as G, GUIDE_LAYER_ATTRIBUTE as a, GuideErrorBoundary as b, GuideProgressConflict as c, GuideRuntime as d, apiCallDetail as e, emitGuideEvent as f, fillRoute as g, guideAnchor as h, guideMenuShown as i, matchRoute as j, reportApiEvent as k, loadGuides as l, matchApiPath as m, openGuidePanel as o, reportApiCall as r, storeGuides as s, useGuideEntry as u };
3944
+ //# sourceMappingURL=guide-runtime-C7Z00QwM.esm.js.map