@eintrek/erp-shell 0.1.72 → 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,22 @@ 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
+
179
195
  ### 0.1.72
180
196
 
181
197
  - "ขั้นที่ x/y" counts only the steps this person will see: a step whose
@@ -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?.();
@@ -803,6 +813,7 @@ function enterStep(d, suite, env, mission, index, opts) {
803
813
  const where = placeRoute(steps, i);
804
814
  const away = !!where && !matchRoute(where, env.pathname, env.search, env.code);
805
815
  const bridge = !away && needsBridge(step, env, ctx);
816
+ const nts = !away && !bridge && nothingToSaveHolds(step, env, ctx);
806
817
  setScreen(d, {
807
818
  name: "step",
808
819
  mission: mission.key,
@@ -814,7 +825,9 @@ function enterStep(d, suite, env, mission, index, opts) {
814
825
  confirmExit: false,
815
826
  misses: 0,
816
827
  armed: hiddenAnchorShown(step, env),
817
- 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),
818
831
  });
819
832
  if (!opts.replay) {
820
833
  const cp = checkpointAtOrBefore(steps, i);
@@ -857,6 +870,54 @@ function setNothingToSave(d, suite, env, on) {
857
870
  if (stepTargetVisible(step, env, ctx, s.bridge))
858
871
  announceStep(d, suite, env);
859
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
+ }
860
921
  function markReadyIfVisible(d, suite, env) {
861
922
  const s = d.state.screen;
862
923
  if (s.name !== "step" || s.phase !== "waiting")
@@ -1242,6 +1303,7 @@ function reduceGuide(state, action, suite, env) {
1242
1303
  else if (!cur.bridge && needsBridge(step, env, ctx)) {
1243
1304
  setScreen(d, { ...cur, bridge: true, phase: "waiting" });
1244
1305
  }
1306
+ syncStatus(d, suite, env);
1245
1307
  markReadyIfVisible(d, suite, env);
1246
1308
  break;
1247
1309
  }
@@ -1304,6 +1366,12 @@ function reduceGuide(state, action, suite, env) {
1304
1366
  advance(d, suite, env);
1305
1367
  break;
1306
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
+ }
1307
1375
  if (kind !== "input")
1308
1376
  break;
1309
1377
  const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
@@ -2310,6 +2378,7 @@ const GUIDE_CSS = `
2310
2378
  .eg-b-t{margin:0;font-size:16px;font-weight:600;line-height:1.45}
2311
2379
  .eg-b-w{margin:2px 0 0;font-size:13.5px;color:var(--eg-muted)}
2312
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)}
2313
2382
  .eg-k{display:inline-block;background:var(--eg-tint);color:var(--eg-strong);border-radius:7px;padding:0 7px;font-weight:600}
2314
2383
  .eg-b-f{display:flex;align-items:center;justify-content:space-between;margin-top:12px;gap:8px}
2315
2384
  .eg-b-f .eg-btn{height:34px;padding:0 14px;font-size:13.5px}
@@ -2543,7 +2612,7 @@ function StepLayer(p) {
2543
2612
  const showBubble = p.phase === "ready" && (!!spot || !p.hasTarget);
2544
2613
  const gap = p.showHand ? 34 : 14;
2545
2614
  const spotKey = spot ? `${spot.x}|${spot.y}|${spot.width}|${spot.height}` : "";
2546
- 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}`;
2547
2616
  // What the bubble sits beside: the target, with an open list over it (the
2548
2617
  // list is what the person is choosing from now). What it must not cover:
2549
2618
  // open lists, and the other anchors the step allows (a map step's
@@ -2666,7 +2735,7 @@ function BubbleBody(p) {
2666
2735
  const b = p.bubble;
2667
2736
  const title = b.error ? b.error.title : b.title;
2668
2737
  const why = b.error ? (b.error.detail ?? "") : b.why;
2669
- 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] })] })] }));
2670
2739
  }
2671
2740
  /** The fingertip at ≈62% across and down the target; upside down near the bottom. */
2672
2741
  function handPosition(spot, viewportHeight) {
@@ -3818,7 +3887,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
3818
3887
  title: bridge ? "กดปุ่มเมนู" : resolveText(step.title, stepCtx),
3819
3888
  why: bridge ? `เมนูทั้งหมดของ ${suite.appName} อยู่ในนี้` : resolveText(step.why, stepCtx),
3820
3889
  error: screen.error,
3821
- 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)),
3822
3892
  showEndWithoutSave: !bridge && screen.replay && kind === "commit",
3823
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" }) }));
3824
3894
  }
@@ -3870,5 +3940,5 @@ function useFactsKey(facts) {
3870
3940
  }, [facts]);
3871
3941
  }
3872
3942
 
3873
- 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 };
3874
- //# sourceMappingURL=guide-runtime-B-3myqxe.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