@eintrek/erp-shell 0.1.77 → 0.1.79

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,30 @@ 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.79
180
+
181
+ - **The ภารกิจ chip no longer covers dialogs.** The guide layer sits above every
182
+ dialog, and the chip sat at the dialog's bottom right, over its buttons, in
183
+ every app. While an app dialog (Radix dialog or alert dialog) is open, the
184
+ chip is not drawn; it comes back when the dialog closes.
185
+
186
+ ### 0.1.78
187
+
188
+ - **Every tab shows the "?" menu.** Each tab of an app reads guides/me and
189
+ publishes "ไกด์เริ่มต้น n/N", "คู่มือหน้านี้", "คู่มือทุกหน้า", "พาทำงาน",
190
+ "อธิบายส่วนนี้" and the SectionGuideButtons. Before, only the tab holding
191
+ the Web Lock did, so with two tabs open "?" went straight to ช่วยเหลือ in
192
+ the other (since 0.1.74 every guide item was lost there).
193
+ - **One tab runs.** Steps, offers, the welcome, celebrations and saves stay
194
+ with the lock holder. Pressing a guide item in another tab makes that tab
195
+ the runner: it steals the lock, reads progress fresh and runs the item;
196
+ the old holder stops what it was running, silently and without writing,
197
+ and waits its turn again. Nothing resumes by itself in a tab taken over.
198
+ - **Progress reaches every tab.** A tab that saves posts "saved" on a
199
+ BroadcastChannel (`erp-guide:<orgId>:<app>`); the others read guides/me
200
+ again (bypassing the 5-minute cache), as does any tab coming back into
201
+ view, so n/N and ดูแล้ว stay right.
202
+
179
203
  ### 0.1.77
180
204
 
181
205
  Fixes from the apps' first week on 0.1.76.
@@ -2807,6 +2807,37 @@ function reduceGuide(state, action, suite, env) {
2807
2807
  d.state = { ...d.state, pages: { ...d.state.pages, [action.key]: action.row } };
2808
2808
  break;
2809
2809
  }
2810
+ case "yield": {
2811
+ const run = d.state.pageRun;
2812
+ if (run?.kind === "flow" && run.context)
2813
+ withProgress(d, { context: run.context });
2814
+ d.state = { ...d.state, pageRun: null, replayPage: null, panelOpen: false, justDone: null, pendingCelebrate: [] };
2815
+ setScreen(d, { name: "idle" });
2816
+ break;
2817
+ }
2818
+ case "refresh": {
2819
+ const p = action.progress;
2820
+ const local = d.state.progress;
2821
+ // The server's row when it is this version and not behind this tab's.
2822
+ if (p.guide_version === local.guide_version && p.rev >= local.rev)
2823
+ d.state = { ...d.state, progress: p };
2824
+ const newer = (server, mine) => {
2825
+ const out = { ...(mine ?? {}) };
2826
+ for (const [k, r] of Object.entries(server ?? {})) {
2827
+ const m = out[k];
2828
+ if (!m || r.version > m.version || (r.version === m.version && r.rev >= m.rev))
2829
+ out[k] = r;
2830
+ }
2831
+ return out;
2832
+ };
2833
+ d.state = {
2834
+ ...d.state,
2835
+ pages: newer(action.pages, d.state.pages),
2836
+ sections: newer(action.sections, d.state.sections),
2837
+ flows: newer(action.flows, d.state.flows),
2838
+ };
2839
+ break;
2840
+ }
2810
2841
  case "startSection": {
2811
2842
  // Never offered: only on request. Never over a mission step, a
2812
2843
  // welcome, a gate or a celebration; only on screen, only theirs.
@@ -3176,9 +3207,9 @@ function conflictOf(err) {
3176
3207
  return null;
3177
3208
  }
3178
3209
  /** The GET, parsed for this suite; null (show nothing) on any failure or odd answer. */
3179
- async function loadGuides(adapter, suite, signal) {
3210
+ async function loadGuides(adapter, suite, signal, fresh = false) {
3180
3211
  try {
3181
- const raw = await adapter.fetchProgress({ app: suite.app, guideKey: suite.key, signal });
3212
+ const raw = await adapter.fetchProgress({ app: suite.app, guideKey: suite.key, signal, ...(fresh ? { fresh: true } : {}) });
3182
3213
  return parseGuidesAnswer(raw, suite);
3183
3214
  }
3184
3215
  catch {
@@ -4377,36 +4408,81 @@ const flowMirrorKey = (orgId, suite) => `erp-guide-flow-run:${orgId}:${suite.app
4377
4408
  /* One tab at a time */
4378
4409
  /* ------------------------------------------------------------------ */
4379
4410
  /**
4380
- * Whether this tab runs the tour. With two tabs open, the first holds a Web
4381
- * Lock and the second shows nothing until the first goes away.
4411
+ * Whether this tab runs the tour: one tab at a time holds a Web Lock and
4412
+ * runs steps, offers, the welcome and saves. Every other tab still reads
4413
+ * progress and shows the "?" menu (shell ≥ 0.1.78); pressing one of its
4414
+ * guide items calls `takeover`, which steals the lock. The tab it was
4415
+ * stolen from goes back to waiting its turn (and yields what it was running).
4416
+ * Without Web Locks every tab runs, as before.
4382
4417
  */
4383
4418
  function usePrimaryTab(name) {
4384
4419
  const [primary, setPrimary] = useState(false);
4420
+ const primaryRef = useRef(false);
4421
+ const takeoverRef = useRef(() => Promise.resolve());
4385
4422
  useEffect(() => {
4386
4423
  const locks = (typeof navigator !== "undefined" ? navigator.locks : undefined);
4424
+ const set = (v) => {
4425
+ primaryRef.current = v;
4426
+ setPrimary(v);
4427
+ };
4387
4428
  if (!locks?.request) {
4388
- setPrimary(true);
4429
+ set(true);
4430
+ takeoverRef.current = () => Promise.resolve();
4389
4431
  return;
4390
4432
  }
4391
- const abort = new AbortController();
4433
+ let alive = true;
4434
+ let gen = 0;
4435
+ let pending = null;
4392
4436
  let release = null;
4393
- locks
4394
- .request(name, { signal: abort.signal }, () => {
4395
- setPrimary(true);
4396
- return new Promise(resolve => {
4397
- release = resolve;
4437
+ let waiters = [];
4438
+ const queue = (steal) => {
4439
+ const my = ++gen;
4440
+ const ac = new AbortController();
4441
+ pending = steal ? null : ac;
4442
+ locks
4443
+ .request(name, steal ? { steal: true } : { signal: ac.signal }, () => {
4444
+ if (!alive || my !== gen)
4445
+ return undefined;
4446
+ pending = null;
4447
+ set(true);
4448
+ const w = waiters;
4449
+ waiters = [];
4450
+ w.forEach(f => f());
4451
+ return new Promise(resolve => {
4452
+ release = resolve;
4453
+ });
4454
+ })
4455
+ .catch(() => {
4456
+ // Stolen by another tab (or this request aborted): not the runner;
4457
+ // wait for the lock again.
4458
+ if (!alive || my !== gen)
4459
+ return;
4460
+ release = null;
4461
+ set(false);
4462
+ queue(false);
4398
4463
  });
4399
- })
4400
- .catch(() => {
4401
- /* aborted on unmount */
4464
+ };
4465
+ queue(false);
4466
+ takeoverRef.current = () => new Promise(resolve => {
4467
+ if (primaryRef.current) {
4468
+ resolve();
4469
+ return;
4470
+ }
4471
+ waiters.push(resolve);
4472
+ pending?.abort();
4473
+ queue(true);
4402
4474
  });
4403
4475
  return () => {
4404
- abort.abort();
4476
+ alive = false;
4477
+ pending?.abort();
4405
4478
  release?.();
4406
- setPrimary(false);
4479
+ waiters.forEach(f => f());
4480
+ waiters = [];
4481
+ set(false);
4407
4482
  };
4408
4483
  }, [name]);
4409
- return primary;
4484
+ const takeover = useCallback(() => takeoverRef.current(), []);
4485
+ return { primary, takeover };
4410
4486
  }
4411
4487
  /* ------------------------------------------------------------------ */
4412
4488
  /* Putting the page back after a replay */
@@ -4515,7 +4591,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4515
4591
  busyRef.current = busy;
4516
4592
  // No menu: never a menu step (the machine sees it as always open).
4517
4593
  const sidebarOpen = menu ? !!menu.open : true;
4518
- const primary = usePrimaryTab(`erp-guide:${organizationId}:${suite.key}`);
4594
+ const { primary, takeover } = usePrimaryTab(`erp-guide:${organizationId}:${suite.key}`);
4595
+ const primaryRef = useRef(primary);
4596
+ primaryRef.current = primary;
4519
4597
  // Progress: read once this tab runs the tour and the app is ready (for
4520
4598
  // the missions, or for the page guides alone: config.pagesReady).
4521
4599
  const [answer, setAnswer] = useState(null);
@@ -4524,7 +4602,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4524
4602
  const hasPages = useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0 || validFlows(suite).length > 0, [suite]);
4525
4603
  const hasFlows = useMemo(() => validFlows(suite).length > 0, [suite]);
4526
4604
  const hasSections = useMemo(() => validSections(suite).length > 0, [suite]);
4527
- const enabled = primary && (missionsReady || (hasPages && pagesReady));
4605
+ // Every tab reads progress (0.1.78): the "?" menu is shown in all of them.
4606
+ const enabled = missionsReady || (hasPages && pagesReady);
4528
4607
  const loaded = useRef(false);
4529
4608
  useEffect(() => {
4530
4609
  if (!enabled || loaded.current)
@@ -4548,8 +4627,12 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4548
4627
  }, [enabled, suite]);
4549
4628
  // The missions run for their audience; the page guides for theirs (every
4550
4629
  // member while pages are on), each with its own ready.
4551
- const missionsLive = !!answer && answer.audience !== "none" && !answer.newerOnServer && primary && missionsReady && !hidden;
4552
- const pagesLive = !!answer && hasPages && answer.pageAudience !== "none" && primary && pagesReady && !hidden;
4630
+ // On: this person has them here (every tab, for the "?" menu). Live: this
4631
+ // tab runs them (the primary tab only): steps, offers, the welcome, saves.
4632
+ const missionsOn = !!answer && answer.audience !== "none" && !answer.newerOnServer && missionsReady && !hidden;
4633
+ const pagesOn = !!answer && hasPages && answer.pageAudience !== "none" && pagesReady && !hidden;
4634
+ const missionsLive = missionsOn && primary;
4635
+ const pagesLive = pagesOn && primary;
4553
4636
  const live = missionsLive || pagesLive;
4554
4637
  /* ---------------- machine ---------------- */
4555
4638
  const [state, setState] = useState(null);
@@ -4642,6 +4725,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4642
4725
  const row = await storeGuides(adapterRef.current, suite, cur.progress);
4643
4726
  if (row)
4644
4727
  savedKey.current = payloadKey(row, suite.app);
4728
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
4729
+ if (row)
4730
+ announceSaved();
4645
4731
  if (row && stateRef.current) {
4646
4732
  // eslint-disable-next-line @typescript-eslint/no-use-before-define
4647
4733
  dispatchRef.current({ type: "serverRow", progress: row });
@@ -4698,6 +4784,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4698
4784
  // eslint-disable-next-line @typescript-eslint/no-use-before-define
4699
4785
  if (row)
4700
4786
  dispatchRef.current({ type: "pageRow", key: e.key, row, kind });
4787
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
4788
+ if (row)
4789
+ announceSaved();
4701
4790
  });
4702
4791
  }
4703
4792
  }
@@ -4744,6 +4833,95 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4744
4833
  clearTimeout(toastTimer.current);
4745
4834
  };
4746
4835
  }, [flushSave]);
4836
+ /* ---------------- other tabs (0.1.78) ---------------- */
4837
+ // Progress saved in one tab reaches the others: a message on a channel
4838
+ // per organisation and app, and a fresh read when a tab comes back into
4839
+ // view. Only the server's rows are read, never another tab's state.
4840
+ const channelRef = useRef(null);
4841
+ const refreshNow = useCallback(async () => {
4842
+ const a = await loadGuides(adapterRef.current, suite, undefined, true);
4843
+ const cur = stateRef.current;
4844
+ if (!a || !cur)
4845
+ return;
4846
+ // The runner's own state wins while it is running something.
4847
+ if (primaryRef.current && (cur.screen.name !== "idle" || saving.current))
4848
+ return;
4849
+ if (a.progress.guide_version === cur.progress.guide_version && a.progress.rev >= cur.progress.rev) {
4850
+ savedKey.current = a.progress.rev > 0 ? payloadKey(a.progress, suite.app) : null;
4851
+ }
4852
+ dispatchRef.current({ type: "refresh", progress: a.progress, pages: a.pages, sections: a.sections, flows: a.flows });
4853
+ }, [suite]);
4854
+ const refreshRef = useRef(refreshNow);
4855
+ refreshRef.current = refreshNow;
4856
+ useEffect(() => {
4857
+ if (typeof BroadcastChannel === "undefined")
4858
+ return;
4859
+ let ch;
4860
+ try {
4861
+ ch = new BroadcastChannel(`erp-guide:${organizationId}:${suite.app}`);
4862
+ }
4863
+ catch {
4864
+ return;
4865
+ }
4866
+ channelRef.current = ch;
4867
+ ch.onmessage = (e) => {
4868
+ if (e.data?.type === "saved")
4869
+ void refreshRef.current();
4870
+ };
4871
+ return () => {
4872
+ channelRef.current = null;
4873
+ ch.close();
4874
+ };
4875
+ }, [organizationId, suite.app]);
4876
+ useEffect(() => {
4877
+ const onVisible = () => {
4878
+ if (document.visibilityState === "visible" && stateRef.current)
4879
+ void refreshRef.current();
4880
+ };
4881
+ document.addEventListener("visibilitychange", onVisible);
4882
+ return () => document.removeEventListener("visibilitychange", onVisible);
4883
+ }, []);
4884
+ function announceSaved() {
4885
+ try {
4886
+ channelRef.current?.postMessage({ type: "saved" });
4887
+ }
4888
+ catch {
4889
+ /* a closed channel: the next visibility change reads again */
4890
+ }
4891
+ }
4892
+ // The tab that lost the lock stops cleanly: whatever it was running ends
4893
+ // silently, without writing.
4894
+ useEffect(() => {
4895
+ if (primary || !stateRef.current)
4896
+ return;
4897
+ const cur = stateRef.current;
4898
+ if (saveTimer.current) {
4899
+ clearTimeout(saveTimer.current);
4900
+ saveTimer.current = null;
4901
+ }
4902
+ if (cur.screen.name !== "idle" || cur.panelOpen)
4903
+ dispatchRef.current({ type: "yield" });
4904
+ }, [primary]);
4905
+ /**
4906
+ * A guide item pressed in this tab: run it here. In a tab that is not the
4907
+ * runner, take the lock first (the other tab stops), read progress fresh,
4908
+ * then do it. Nothing resumes by itself in a tab taken over this way.
4909
+ */
4910
+ const viaRunner = useCallback((fn) => {
4911
+ if (primaryRef.current) {
4912
+ fn();
4913
+ return;
4914
+ }
4915
+ resumed.current = true;
4916
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
4917
+ flowResumed.current = true;
4918
+ void takeover()
4919
+ .then(() => refreshRef.current().catch(() => undefined))
4920
+ .then(() => {
4921
+ if (primaryRef.current)
4922
+ fn();
4923
+ });
4924
+ }, [takeover]);
4747
4925
  /* ---------------- boot: resume, welcome ---------------- */
4748
4926
  const screen = state?.screen ?? { name: "idle" };
4749
4927
  const resumed = useRef(false);
@@ -5392,12 +5570,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5392
5570
  !due;
5393
5571
  useAfterIdle(passiveDue, PASSIVE_IDLE_MS, busy, () => dispatchRef.current({ type: "idleCelebrate" }), [state?.pendingCelebrate?.join(",")]);
5394
5572
  useEffect(() => {
5395
- if (!missionsLive || !state) {
5573
+ // Every tab shows "ไกด์เริ่มต้น n/N"; pressing it runs it in this tab.
5574
+ if (!missionsOn || !state) {
5396
5575
  publishGuideEntry(null, null);
5397
5576
  return;
5398
5577
  }
5399
- publishGuideEntry({ done, total }, () => dispatchRef.current({ type: "openPanel" }));
5400
- }, [missionsLive, state, done, total]);
5578
+ publishGuideEntry({ done, total }, () => viaRunner(() => dispatchRef.current({ type: "openPanel" })));
5579
+ }, [missionsOn, state, done, total, viaRunner]);
5401
5580
  useEffect(() => () => publishGuideEntry(null, null), []);
5402
5581
  /* ---------------- page guides: this page's, the offer, the list ---------------- */
5403
5582
  // `when` anchors (LIFF's sub-screens) come and go without a route change:
@@ -5405,7 +5584,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5405
5584
  const hasWhen = useMemo(() => validPages(suite).some(p => !!p.when) || validSections(suite).length > 0, [suite]);
5406
5585
  const [domTick, setDomTick] = useState(0);
5407
5586
  useEffect(() => {
5408
- if (!pagesLive || !hasWhen || typeof MutationObserver === "undefined")
5587
+ if (!pagesOn || !hasWhen || typeof MutationObserver === "undefined")
5409
5588
  return;
5410
5589
  let t = null;
5411
5590
  const obs = new MutationObserver(records => {
@@ -5429,18 +5608,18 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5429
5608
  if (t)
5430
5609
  clearTimeout(t);
5431
5610
  };
5432
- }, [pagesLive, hasWhen]);
5611
+ }, [pagesOn, hasWhen]);
5433
5612
  const hasState = !!state;
5434
- const pageNow = useMemo(() => (pagesLive && hasState ? pageHere(suite, env()) : null),
5613
+ const pageNow = useMemo(() => (pagesOn && hasState ? pageHere(suite, env()) : null),
5435
5614
  // pathname/search/factsKey/domTick make it current.
5436
5615
  // eslint-disable-next-line react-hooks/exhaustive-deps
5437
- [pagesLive, hasState, suite, env, pathname, search, factsKey, domTick]);
5438
- const listed = useMemo(() => (pagesLive && ctxIdle ? listedPages(suite, ctxIdle) : []), [pagesLive, suite, ctxIdle]);
5439
- const sectionsNow = useMemo(() => pagesLive && hasState && hasSections
5616
+ [pagesOn, hasState, suite, env, pathname, search, factsKey, domTick]);
5617
+ const listed = useMemo(() => (pagesOn && ctxIdle ? listedPages(suite, ctxIdle) : []), [pagesOn, suite, ctxIdle]);
5618
+ const sectionsNow = useMemo(() => pagesOn && hasState && hasSections
5440
5619
  ? sectionsAvailable(suite, env()).map(k => ({ key: k, title: findSection(suite, k)?.title ?? "" }))
5441
5620
  : [],
5442
5621
  // eslint-disable-next-line react-hooks/exhaustive-deps
5443
- [pagesLive, hasState, hasSections, suite, env, pathname, search, factsKey, domTick]);
5622
+ [pagesOn, hasState, hasSections, suite, env, pathname, search, factsKey, domTick]);
5444
5623
  const sectionsSig = sectionsNow.map(x => x.key).join(",");
5445
5624
  const openSection = useCallback((key) => {
5446
5625
  openingSection.current = key;
@@ -5451,14 +5630,14 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5451
5630
  openingSection.current = null;
5452
5631
  }
5453
5632
  }, []);
5454
- const flowsNow = useMemo(() => pagesLive && hasState && hasFlows
5633
+ const flowsNow = useMemo(() => pagesOn && hasState && hasFlows
5455
5634
  ? flowsAvailable(suite, env()).map(k => ({ key: k, title: findFlow(suite, k)?.title ?? "" }))
5456
5635
  : [],
5457
5636
  // eslint-disable-next-line react-hooks/exhaustive-deps
5458
- [pagesLive, hasState, hasFlows, suite, env, pathname, search, factsKey]);
5637
+ [pagesOn, hasState, hasFlows, suite, env, pathname, search, factsKey]);
5459
5638
  const flowsSig = flowsNow.map(x => x.key).join(",");
5460
5639
  /** The flows the panel lists: this person's, startable here or with somewhere to go. */
5461
- const flowsListed = useMemo(() => (pagesLive && hasFlows && ctxIdle ? shownFlows(suite, ctxIdle).filter(f => onFlowStart(f, { code, pathname, search: new URLSearchParams(search) }) || !!flowHref(f, ctxIdle)) : []), [pagesLive, hasFlows, suite, ctxIdle, code, pathname, search]);
5640
+ const flowsListed = useMemo(() => (pagesOn && hasFlows && ctxIdle ? shownFlows(suite, ctxIdle).filter(f => onFlowStart(f, { code, pathname, search: new URLSearchParams(search) }) || !!flowHref(f, ctxIdle)) : []), [pagesOn, hasFlows, suite, ctxIdle, code, pathname, search]);
5462
5641
  const openFlowHere = useCallback((key) => dispatchRef.current({ type: "startFlow", flow: key }), []);
5463
5642
  const openPageHere = useCallback(() => {
5464
5643
  const p = pageHere(suite, env());
@@ -5466,7 +5645,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5466
5645
  dispatchRef.current({ type: "startPage", page: p.key });
5467
5646
  }, [suite, env]);
5468
5647
  useEffect(() => {
5469
- if (!pagesLive || !hasState) {
5648
+ if (!pagesOn || !hasState) {
5470
5649
  publishPageGuides(null, null, null);
5471
5650
  return;
5472
5651
  }
@@ -5476,10 +5655,10 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5476
5655
  sections: sectionsNow,
5477
5656
  flows: flowsNow,
5478
5657
  flowsListed: flowsListed.length,
5479
- }, openPageHere, () => dispatchRef.current({ type: "openPanel", section: "pages" }), openSection, openFlowHere, () => dispatchRef.current({ type: "openPanel", section: "flows" }));
5658
+ }, () => viaRunner(openPageHere), () => viaRunner(() => dispatchRef.current({ type: "openPanel", section: "pages" })), (key) => viaRunner(() => openSection(key)), (key) => viaRunner(() => openFlowHere(key)), () => viaRunner(() => dispatchRef.current({ type: "openPanel", section: "flows" })));
5480
5659
  // sectionsSig / flowsSig stand for sectionsNow / flowsNow.
5481
5660
  // eslint-disable-next-line react-hooks/exhaustive-deps
5482
- }, [pagesLive, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection, flowsSig, flowsListed.length, openFlowHere]);
5661
+ }, [pagesOn, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection, flowsSig, flowsListed.length, openFlowHere, viaRunner]);
5483
5662
  useEffect(() => () => publishPageGuides(null, null, null), []);
5484
5663
  // The first-visit offer: in "offer" mode, nothing else up, the welcome
5485
5664
  // not due here, and a moment of nothing loading on this page.
@@ -5676,6 +5855,32 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5676
5855
  dispatchRef.current({ type: "resumeFlow", flow: saved.flow, step: saved.step, context, write: saved.write === true });
5677
5856
  // eslint-disable-next-line react-hooks/exhaustive-deps
5678
5857
  }, [pagesLive, !!state, hasFlows, missionsLive]);
5858
+ // The layer sits above every dialog, and a dialog's buttons sit at its
5859
+ // bottom right, where the chip is: the chip covered them in every app.
5860
+ // While an app dialog is open, the chip is not drawn.
5861
+ const [appDialogShown, setAppDialogShown] = useState(false);
5862
+ useEffect(() => {
5863
+ if (!missionsLive || typeof MutationObserver === "undefined" || typeof document === "undefined" || !document.body)
5864
+ return;
5865
+ let raf = 0;
5866
+ const check = () => {
5867
+ raf = 0;
5868
+ setAppDialogShown(appDialogOpen());
5869
+ };
5870
+ check();
5871
+ const obs = new MutationObserver(records => {
5872
+ if (records.every(r => inGuideLayer(r.target)))
5873
+ return;
5874
+ if (!raf)
5875
+ raf = requestAnimationFrame(check);
5876
+ });
5877
+ obs.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-state", "role"] });
5878
+ return () => {
5879
+ obs.disconnect();
5880
+ if (raf)
5881
+ cancelAnimationFrame(raf);
5882
+ };
5883
+ }, [missionsLive]);
5679
5884
  if (!live || !state || !layer || !ctxIdle)
5680
5885
  return null;
5681
5886
  /* ---------------- render ---------------- */
@@ -5783,7 +5988,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
5783
5988
  .join(", ")
5784
5989
  : undefined,
5785
5990
  }));
5786
- const showChip = missionsLive && chipVisible(state, suite, ctxIdle);
5991
+ const showChip = missionsLive && !appDialogShown && chipVisible(state, suite, ctxIdle);
5787
5992
  // The panel: the missions (when this person has them, and not "คู่มือทุกหน้า"),
5788
5993
  // then the page guides they have.
5789
5994
  const panelMissions = missionsLive && total > 0 && state.panelSection === "all";
@@ -5903,4 +6108,4 @@ function thaiMonthYear(m, opts = {}) {
5903
6108
  }
5904
6109
 
5905
6110
  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 };
5906
- //# sourceMappingURL=format-ILrTMV4G.esm.js.map
6111
+ //# sourceMappingURL=format-BqUpxbzD.esm.js.map