@eintrek/erp-shell 0.1.74 → 0.1.76

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.
@@ -357,18 +357,35 @@ function publishGuideEntry(next, openPanel) {
357
357
  emit();
358
358
  }
359
359
  }
360
- const NO_PAGES = { page: null, listed: 0 };
360
+ const NO_PAGES = { page: null, listed: 0, sections: [], flows: [], flowsListed: 0 };
361
361
  let pages = NO_PAGES;
362
362
  let openPageImpl = null;
363
363
  let openPagesImpl = null;
364
- /** Called by the host: this page's guide and the listed count, and how to open them. */
365
- function publishPageGuides(next, openPage, openPages) {
364
+ let openSectionImpl = null;
365
+ let openFlowImpl = null;
366
+ let openFlowsImpl = null;
367
+ const sectionsKey = (x) => (x ?? []).map(s => `${s.key}\u0000${s.title}`).join("\u0001");
368
+ /** Called by the host: this page's guide, the listed count, the sections on screen, and how to open them. */
369
+ function publishPageGuides(next, openPage, openPages, openSection = null, openFlow = null, openFlows = null) {
366
370
  const n = next ?? NO_PAGES;
367
371
  openPageImpl = openPage;
368
372
  openPagesImpl = openPages;
369
- const same = n.listed === pages.listed && (n.page?.title ?? null) === (pages.page?.title ?? null);
373
+ openSectionImpl = openSection;
374
+ openFlowImpl = openFlow;
375
+ openFlowsImpl = openFlows;
376
+ const same = n.listed === pages.listed &&
377
+ (n.flowsListed ?? 0) === (pages.flowsListed ?? 0) &&
378
+ (n.page?.title ?? null) === (pages.page?.title ?? null) &&
379
+ sectionsKey(n.sections) === sectionsKey(pages.sections) &&
380
+ sectionsKey(n.flows) === sectionsKey(pages.flows);
370
381
  if (!same) {
371
- pages = n.page || n.listed ? { page: n.page ? { title: n.page.title } : null, listed: n.listed } : NO_PAGES;
382
+ const sections = (n.sections ?? []).map(x => ({ key: x.key, title: x.title }));
383
+ const flows = (n.flows ?? []).map(x => ({ key: x.key, title: x.title }));
384
+ const flowsListed = n.flowsListed ?? 0;
385
+ pages =
386
+ n.page || n.listed || sections.length || flows.length || flowsListed
387
+ ? { page: n.page ? { title: n.page.title } : null, listed: n.listed, sections, flows, flowsListed }
388
+ : NO_PAGES;
372
389
  emit();
373
390
  }
374
391
  }
@@ -376,10 +393,16 @@ function guideMenuItems(e, p = NO_PAGES) {
376
393
  const items = [];
377
394
  if (p.page)
378
395
  items.push("page");
396
+ if ((p.sections ?? []).length > 0)
397
+ items.push("sections");
398
+ if ((p.flows ?? []).length > 0)
399
+ items.push("flowsHere");
379
400
  if (e && e.total > 0)
380
401
  items.push("missions");
381
402
  if (p.listed > 0)
382
403
  items.push("pages");
404
+ if ((p.flowsListed ?? 0) > 0)
405
+ items.push("flows");
383
406
  if (items.length)
384
407
  items.push("help");
385
408
  return items;
@@ -425,6 +448,87 @@ function usePageGuideEntry() {
425
448
  const title = p.page?.title ?? null;
426
449
  return React.useMemo(() => (title === null ? null : { title, open: openPageGuide }), [title]);
427
450
  }
451
+ /**
452
+ * Opens a section guide ("อธิบายส่วนนี้"), when it is on screen here for this
453
+ * person. The values it shows are the ones the app last rendered
454
+ * (`useSectionGuideValues`).
455
+ */
456
+ function openSectionGuide(key) {
457
+ openSectionImpl?.(key);
458
+ }
459
+ /** Starts a flow that starts on this page ("พาทำ · <title>"). */
460
+ function openFlow(key) {
461
+ openFlowImpl?.(key);
462
+ }
463
+ /** Opens the panel on the flows alone ("พาทำงาน"). */
464
+ function openFlowsPanel() {
465
+ openFlowsImpl?.();
466
+ }
467
+ const sectionValues = new Map();
468
+ const valueListeners = new Set();
469
+ let valuesVersion = 0;
470
+ function valuesSig(v) {
471
+ try {
472
+ return JSON.stringify(v);
473
+ }
474
+ catch {
475
+ return String(Math.random());
476
+ }
477
+ }
478
+ /**
479
+ * Sets (or with undefined, clears) a section guide's live values. The
480
+ * engine reads the latest whenever it draws a step, so a page that
481
+ * re-renders mid-guide never leaves an old number on screen.
482
+ */
483
+ function setSectionGuideValues(key, values) {
484
+ const prev = sectionValues.get(key);
485
+ if (values === undefined) {
486
+ if (!prev)
487
+ return;
488
+ sectionValues.delete(key);
489
+ }
490
+ else {
491
+ const sig = valuesSig(values);
492
+ if (prev && prev.sig === sig)
493
+ return;
494
+ sectionValues.set(key, { values, sig });
495
+ }
496
+ valuesVersion++;
497
+ valueListeners.forEach(l => l());
498
+ }
499
+ /** A section guide's live values now (empty when the app has given none). */
500
+ function getSectionGuideValues(key) {
501
+ return (key && sectionValues.get(key)?.values) || {};
502
+ }
503
+ /** Re-renders when any section guide's values change (the engine's own use). */
504
+ function useSectionValuesVersion() {
505
+ return React.useSyncExternalStore(l => {
506
+ valueListeners.add(l);
507
+ return () => {
508
+ valueListeners.delete(l);
509
+ };
510
+ }, () => valuesVersion, () => 0);
511
+ }
512
+ /**
513
+ * Hands a section guide the numbers its copy uses, from the component that
514
+ * computes them: `useSectionGuideValues("cash-flow", { month: "ม.ค.", cashIn,
515
+ * … })`. Call it on every render (the latest wins); they go when the
516
+ * component unmounts. undefined values: nothing.
517
+ */
518
+ function useSectionGuideValues(key, values) {
519
+ const sig = values === undefined ? "" : valuesSig(values);
520
+ React.useEffect(() => {
521
+ if (values !== undefined)
522
+ setSectionGuideValues(key, values);
523
+ // sig stands for the values.
524
+ // eslint-disable-next-line react-hooks/exhaustive-deps
525
+ }, [key, sig]);
526
+ React.useEffect(() => {
527
+ if (values === undefined)
528
+ return;
529
+ return () => setSectionGuideValues(key, undefined);
530
+ }, [key, values === undefined]); // eslint-disable-line react-hooks/exhaustive-deps
531
+ }
428
532
 
429
533
  /** The synthetic mission key of a page guide: `page:<key>`. */
430
534
  const PAGE_PREFIX = "page:";
@@ -603,6 +707,278 @@ function currentPage(suite, where, ctx) {
603
707
  function listedPages(suite, ctx) {
604
708
  return validPages(suite).filter(p => p.href !== undefined && pageShown(p, ctx));
605
709
  }
710
+ /* ------------------------------------------------------------------ */
711
+ /* Section guides ("อธิบายส่วนนี้", shell 0.1.75) */
712
+ /* ------------------------------------------------------------------ */
713
+ /** The synthetic mission key of a section guide: `section:<key>`. */
714
+ const SECTION_PREFIX = "section:";
715
+ function isSectionMissionKey(key) {
716
+ return !!key && key.startsWith(SECTION_PREFIX);
717
+ }
718
+ function sectionKeyOf(missionKey) {
719
+ return isSectionMissionKey(missionKey) ? missionKey.slice(SECTION_PREFIX.length) : undefined;
720
+ }
721
+ /** Why a section guide cannot run, or null when it can. */
722
+ function sectionProblem(app, section) {
723
+ if (!section || typeof section.key !== "string" || !PAGE_KEY_RE.test(section.key))
724
+ return "its key is not [a-z0-9-]";
725
+ if (`${app}.section.${section.key}`.length > ROW_KEY_MAX)
726
+ return `its row key is over ${ROW_KEY_MAX} characters`;
727
+ if (!Number.isInteger(section.version) || section.version < 1)
728
+ return "its version is not a whole number ≥ 1";
729
+ const routes = Array.isArray(section.route) ? section.route : [section.route];
730
+ if (!routes.length || routes.some(r => typeof r !== "string" || !r.startsWith("/")))
731
+ return "it has no route";
732
+ if (typeof section.anchor !== "string" || !section.anchor)
733
+ return "it has no anchor";
734
+ if (!Array.isArray(section.steps) || section.steps.length === 0)
735
+ return "it has no steps";
736
+ for (const st of section.steps) {
737
+ const kind = st.kind ?? "view";
738
+ if (kind !== "view")
739
+ return `step "${st.key}" is a ${kind} step (a section guide only reads)`;
740
+ }
741
+ return null;
742
+ }
743
+ const validSectionCache = new WeakMap();
744
+ /** The suite's section guides that can run (as validPages: the rest dropped, with a warning in development). */
745
+ function validSections(suite) {
746
+ const hit = validSectionCache.get(suite);
747
+ if (hit)
748
+ return hit;
749
+ const out = [];
750
+ const seen = new Set();
751
+ for (const section of suite.sections ?? []) {
752
+ let problem = sectionProblem(suite.app, section);
753
+ if (!problem && seen.has(section.key))
754
+ problem = "another section guide has the same key";
755
+ if (problem) {
756
+ const id = `${suite.key}:section:${section?.key}:${problem}`;
757
+ if (!warned.has(id)) {
758
+ warned.add(id);
759
+ devWarn(`section guide "${section?.key}" dropped: ${problem}`);
760
+ }
761
+ continue;
762
+ }
763
+ seen.add(section.key);
764
+ out.push(section);
765
+ }
766
+ validSectionCache.set(suite, out);
767
+ return out;
768
+ }
769
+ function findSection(suite, key) {
770
+ return key ? validSections(suite).find(s => s.key === key) : undefined;
771
+ }
772
+ /** A live value is missing: undefined, null, "" or a number that is not finite. */
773
+ function valueMissing(v) {
774
+ return v === undefined || v === null || v === "" || (typeof v === "number" && !Number.isFinite(v));
775
+ }
776
+ function textOf(t, ctx) {
777
+ try {
778
+ return typeof t === "function" ? t(ctx) : t;
779
+ }
780
+ catch {
781
+ return "";
782
+ }
783
+ }
784
+ const sectionMissionCache = new WeakMap();
785
+ /**
786
+ * The section guide as a mission the machine plays: `section:<key>`, read
787
+ * steps only, checkpoints unless they say otherwise. A step is skipped while
788
+ * a value it `needs` is missing, or when its title comes out empty.
789
+ */
790
+ function sectionMission(section) {
791
+ const hit = sectionMissionCache.get(section);
792
+ if (hit)
793
+ return hit;
794
+ const steps = section.steps.map(st => {
795
+ const { route: _route, resumeUrl: _resume, ...rest } = st;
796
+ const own = st.skipIf;
797
+ return {
798
+ ...rest,
799
+ kind: "view",
800
+ checkpoint: st.checkpoint ?? true,
801
+ skipIf: (ctx) => {
802
+ const values = ctx.values ?? {};
803
+ if ((st.needs ?? []).some(k => valueMissing(values[k])))
804
+ return true;
805
+ if (!textOf(st.title, ctx).trim())
806
+ return true;
807
+ return own ? own(ctx) : false;
808
+ },
809
+ };
810
+ });
811
+ const m = { key: SECTION_PREFIX + section.key, title: section.title, minutes: 1, steps };
812
+ sectionMissionCache.set(section, m);
813
+ return m;
814
+ }
815
+ /** Whether the section guide applies here: one of its routes matches and its root is on screen. */
816
+ function sectionApplies(section, where) {
817
+ const routes = Array.isArray(section.route) ? section.route : [section.route];
818
+ if (!routes.some(r => !!matchRoute(r, where.pathname, where.search, where.code)))
819
+ return false;
820
+ try {
821
+ return where.isVisible(section.anchor);
822
+ }
823
+ catch {
824
+ return false;
825
+ }
826
+ }
827
+ /** Whether this person has the section guide: `visible` (only true) and `requires`. */
828
+ function sectionShown(section, ctx) {
829
+ try {
830
+ if (section.visible && section.visible(ctx) !== true)
831
+ return false;
832
+ if (section.requires && !ctx.can(section.requires))
833
+ return false;
834
+ return true;
835
+ }
836
+ catch {
837
+ return false;
838
+ }
839
+ }
840
+ /** The section guides on screen here that this person has, in the suite's order. */
841
+ function sectionsHere(suite, where, ctx) {
842
+ return validSections(suite).filter(s => sectionApplies(s, where) && sectionShown(s, ctx));
843
+ }
844
+ /* ------------------------------------------------------------------ */
845
+ /* Workflow guides ("พาทำงาน", shell 0.1.76) */
846
+ /* ------------------------------------------------------------------ */
847
+ /** The synthetic mission key of a flow: `flow:<key>`. */
848
+ const FLOW_PREFIX = "flow:";
849
+ function isFlowMissionKey(key) {
850
+ return !!key && key.startsWith(FLOW_PREFIX);
851
+ }
852
+ function flowKeyOf(missionKey) {
853
+ return isFlowMissionKey(missionKey) ? missionKey.slice(FLOW_PREFIX.length) : undefined;
854
+ }
855
+ const asList = (r) => (r === undefined ? [] : Array.isArray(r) ? r : [r]);
856
+ /** Why a flow cannot run, or null when it can. */
857
+ function flowProblem(app, flow) {
858
+ if (!flow || typeof flow.key !== "string" || !PAGE_KEY_RE.test(flow.key))
859
+ return "its key is not [a-z0-9-]";
860
+ if (`${app}.flow.${flow.key}`.length > ROW_KEY_MAX)
861
+ return `its row key is over ${ROW_KEY_MAX} characters`;
862
+ if (!Number.isInteger(flow.version) || flow.version < 1)
863
+ return "its version is not a whole number ≥ 1";
864
+ const starts = asList(flow.startRoute);
865
+ if (!starts.length || starts.some(r => typeof r !== "string" || !r.startsWith("/")))
866
+ return "it has no start route";
867
+ if (!Array.isArray(flow.steps) || flow.steps.length === 0)
868
+ return "it has no steps";
869
+ for (const st of flow.steps) {
870
+ const kind = st.kind ?? "click";
871
+ if (kind === "view" || kind === "click")
872
+ continue;
873
+ if (!flow.allowCommit)
874
+ return `step "${st.key}" is a ${kind} step and the flow does not set allowCommit`;
875
+ if (kind === "commit" && !st.abort)
876
+ return `commit step "${st.key}" has no abort`;
877
+ }
878
+ return null;
879
+ }
880
+ const validFlowCache = new WeakMap();
881
+ /** The suite's flows that can run (as validPages: the rest dropped, with a warning in development). */
882
+ function validFlows(suite) {
883
+ const hit = validFlowCache.get(suite);
884
+ if (hit)
885
+ return hit;
886
+ const out = [];
887
+ const seen = new Set();
888
+ for (const flow of suite.flows ?? []) {
889
+ let problem = flowProblem(suite.app, flow);
890
+ if (!problem && seen.has(flow.key))
891
+ problem = "another flow has the same key";
892
+ if (problem) {
893
+ const id = `${suite.key}:flow:${flow?.key}:${problem}`;
894
+ if (!warned.has(id)) {
895
+ warned.add(id);
896
+ devWarn(`flow "${flow?.key}" dropped: ${problem}`);
897
+ }
898
+ continue;
899
+ }
900
+ seen.add(flow.key);
901
+ out.push(flow);
902
+ }
903
+ validFlowCache.set(suite, out);
904
+ return out;
905
+ }
906
+ function findFlow(suite, key) {
907
+ return key ? validFlows(suite).find(f => f.key === key) : undefined;
908
+ }
909
+ const flowMissionCache = new WeakMap();
910
+ /** The flow as a mission the machine plays (in replay mode): `flow:<key>`, its steps as authored. */
911
+ function flowMission(flow) {
912
+ const hit = flowMissionCache.get(flow);
913
+ if (hit)
914
+ return hit;
915
+ const m = { key: FLOW_PREFIX + flow.key, title: flow.title, minutes: flow.minutes ?? 1, steps: flow.steps };
916
+ flowMissionCache.set(flow, m);
917
+ return m;
918
+ }
919
+ /**
920
+ * The flow's chain: its start routes, every step's `route` and route
921
+ * trigger, and its `transit` routes. Leaving all of them ends the flow.
922
+ */
923
+ function flowChain(flow) {
924
+ const out = new Set(asList(flow.startRoute));
925
+ for (const st of flow.steps) {
926
+ if (st.route)
927
+ out.add(st.route);
928
+ const triggers = st.advanceOn ? (Array.isArray(st.advanceOn) ? st.advanceOn : [st.advanceOn]) : [];
929
+ for (const t of triggers)
930
+ if ("route" in t && t.route)
931
+ out.add(t.route);
932
+ }
933
+ for (const t of flow.transit ?? [])
934
+ out.add(t);
935
+ return Array.from(out);
936
+ }
937
+ /** Path patterns only (`?k=v` dropped): the chain is about pages, not their tabs. */
938
+ const pathOf = (pattern) => (pattern.includes("?") ? pattern.slice(0, pattern.indexOf("?")) : pattern);
939
+ /** Whether the person is on one of the flow's pages. */
940
+ function inFlowChain(flow, where) {
941
+ return flowChain(flow).some(r => !!matchRoute(pathOf(r), where.pathname, null, where.code));
942
+ }
943
+ /** Whether the person is on the flow's start page. */
944
+ function onFlowStart(flow, where) {
945
+ return asList(flow.startRoute).some(r => !!matchRoute(r, where.pathname, where.search, where.code));
946
+ }
947
+ /** Whether this person has the flow: `visible` (only true) and `requires`. */
948
+ function flowShown(flow, ctx) {
949
+ try {
950
+ if (flow.visible && flow.visible(ctx) !== true)
951
+ return false;
952
+ if (flow.requires && !ctx.can(flow.requires))
953
+ return false;
954
+ return true;
955
+ }
956
+ catch {
957
+ return false;
958
+ }
959
+ }
960
+ /** The flows this person has (the panel's "พาทำงาน" list), in the suite's order. */
961
+ function shownFlows(suite, ctx) {
962
+ return validFlows(suite).filter(f => flowShown(f, ctx));
963
+ }
964
+ /** Where the panel sends the person to start a flow, or null when it needs an id it does not have. */
965
+ function flowHref(flow, ctx) {
966
+ let raw;
967
+ try {
968
+ raw = flow.href === undefined ? undefined : typeof flow.href === "function" ? flow.href(ctx) : flow.href;
969
+ }
970
+ catch {
971
+ raw = undefined;
972
+ }
973
+ if (raw)
974
+ return /^https?:\/\//i.test(raw) ? raw : (fillRoute(raw, { code: ctx.code }) ?? null);
975
+ for (const r of asList(flow.startRoute)) {
976
+ const filled = fillRoute(r, { code: ctx.code });
977
+ if (filled)
978
+ return filled;
979
+ }
980
+ return null;
981
+ }
606
982
 
607
983
  /* ------------------------------------------------------------------ */
608
984
  /* Copy */
@@ -617,6 +993,8 @@ const COPY = {
617
993
  hiddenToast: "ซ่อนแล้ว",
618
994
  aborted: "ไกด์ไปต่อไม่ได้ ลองอีกครั้งภายหลัง",
619
995
  pageDone: "จบคู่มือหน้านี้",
996
+ sectionDone: "จบคำอธิบายส่วนนี้",
997
+ flowDone: "พาทำงานเสร็จแล้ว",
620
998
  bridgeTitle: "กดปุ่มเมนู",
621
999
  bridgeWhy: (app) => `เมนูทั้งหมดของ ${app} อยู่ในนี้`,
622
1000
  };
@@ -629,8 +1007,11 @@ function initialGuideState(audience, progress, pages) {
629
1007
  pageAudience: pages?.pageAudience ?? "none",
630
1008
  pagesMode: pages?.pagesMode ?? "off",
631
1009
  pages: pages?.pages ?? {},
1010
+ sections: pages?.sections ?? {},
1011
+ flows: pages?.flows ?? {},
632
1012
  pageRun: null,
633
1013
  panelSection: "all",
1014
+ autoPassed: null,
634
1015
  audience,
635
1016
  progress,
636
1017
  screen: { name: "idle" },
@@ -650,6 +1031,14 @@ function findMission(suite, key) {
650
1031
  const page = findPage(suite, pageKeyOf(key));
651
1032
  return page ? pageMission(page) : undefined;
652
1033
  }
1034
+ if (isSectionMissionKey(key)) {
1035
+ const section = findSection(suite, sectionKeyOf(key));
1036
+ return section ? sectionMission(section) : undefined;
1037
+ }
1038
+ if (isFlowMissionKey(key)) {
1039
+ const flow = findFlow(suite, flowKeyOf(key));
1040
+ return flow ? flowMission(flow) : undefined;
1041
+ }
653
1042
  return suite.missions.find(m => m.key === key);
654
1043
  }
655
1044
  /**
@@ -687,6 +1076,7 @@ function guideContext(env, data, params = {}) {
687
1076
  facts: env.facts,
688
1077
  can: env.can,
689
1078
  visible: (anchor) => env.isVisible(anchor),
1079
+ values: env.values ?? {},
690
1080
  };
691
1081
  }
692
1082
  function resolveText(t, ctx) {
@@ -1000,7 +1390,7 @@ function announceStep(d, suite, env) {
1000
1390
  const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
1001
1391
  const title = s.bridge ? COPY.bridgeTitle : plainTitle(resolveText(step.title, ctx));
1002
1392
  const why = s.bridge ? COPY.bridgeWhy(suite.appName) : plainTitle(resolveText(step.why, ctx));
1003
- const pos = stepPosition(mission, s.index, env, d.state.progress.context);
1393
+ const pos = stepPosition(mission, s.index, env, d.state.progress.context, autoPassedOf(d.state, mission.key));
1004
1394
  d.effects.push({
1005
1395
  type: "announce",
1006
1396
  text: `ขั้นที่ ${pos.no} จาก ${pos.total}. ${title}${why ? `. ${why}` : ""}`,
@@ -1011,7 +1401,7 @@ function announceStep(d, suite, env) {
1011
1401
  * whose `requires` they lack or whose `skipIf` holds is left out (the step on
1012
1402
  * screen always counts).
1013
1403
  */
1014
- function stepPosition(mission, index, env, context) {
1404
+ function stepPosition(mission, index, env, context, autoPassed = []) {
1015
1405
  let no = 0;
1016
1406
  let total = 0;
1017
1407
  mission.steps.forEach((step, i) => {
@@ -1021,6 +1411,12 @@ function stepPosition(mission, index, env, context) {
1021
1411
  const ctx = guideContext(env, context, routeParamsFor(step, env));
1022
1412
  if (step.skipIf && safe(() => step.skipIf(ctx), false))
1023
1413
  return;
1414
+ // A step that passed by itself (shell ≥ 0.1.75): before this one, as
1415
+ // recorded; after it, one whose outcome already holds now.
1416
+ if (i < index && autoPassed.includes(i))
1417
+ return;
1418
+ if (i > index && wouldAutoPass(step, env))
1419
+ return;
1024
1420
  }
1025
1421
  total++;
1026
1422
  if (i <= index)
@@ -1028,6 +1424,17 @@ function stepPosition(mission, index, env, context) {
1028
1424
  });
1029
1425
  return { no: Math.max(no, 1), total: Math.max(total, 1) };
1030
1426
  }
1427
+ /** A step entered now would pass at once: a click or select step whose route or visible outcome holds. */
1428
+ function wouldAutoPass(step, env) {
1429
+ const kind = stepKind(step);
1430
+ if (step.autoPass === false || kind === "input" || kind === "view")
1431
+ return false;
1432
+ return safe(() => stateTriggerHolds(step, env) !== null, false);
1433
+ }
1434
+ /** The steps of this mission that passed by themselves in this run. */
1435
+ function autoPassedOf(state, mission) {
1436
+ return state.autoPassed?.mission === mission ? state.autoPassed.steps : [];
1437
+ }
1031
1438
  /** "กด [บันทึก]" → "กด บันทึก" (for screen readers). */
1032
1439
  function plainTitle(t) {
1033
1440
  return t.replace(/\[([^\]]+)\]/g, "$1");
@@ -1040,6 +1447,8 @@ function enterStep(d, suite, env, mission, index, opts) {
1040
1447
  const steps = mission.steps;
1041
1448
  let i = Math.max(0, index);
1042
1449
  let firstPass = true;
1450
+ if (d.state.autoPassed?.mission !== mission.key)
1451
+ d.state = { ...d.state, autoPassed: { mission: mission.key, steps: [] } };
1043
1452
  while (i < steps.length) {
1044
1453
  const step = steps[i];
1045
1454
  const params = routeParamsFor(step, env);
@@ -1061,6 +1470,9 @@ function enterStep(d, suite, env, mission, index, opts) {
1061
1470
  const held = stateTriggerHolds(step, env);
1062
1471
  if (held) {
1063
1472
  captureInto(d, step, held, ctx);
1473
+ const ap = d.state.autoPassed;
1474
+ if (!ap.steps.includes(i))
1475
+ d.state = { ...d.state, autoPassed: { mission: ap.mission, steps: [...ap.steps, i] } };
1064
1476
  i++;
1065
1477
  continue;
1066
1478
  }
@@ -1074,9 +1486,11 @@ function enterStep(d, suite, env, mission, index, opts) {
1074
1486
  const step = steps[i];
1075
1487
  const ctx = guideContext(env, d.state.progress.context, routeParamsFor(step, env));
1076
1488
  const page = pageKeyOf(mission.key);
1489
+ const section = sectionKeyOf(mission.key);
1490
+ const flow = flowKeyOf(mission.key);
1077
1491
  const where = placeRoute(steps, i);
1078
- // A page guide is on its page: leaving it ends the guide (never "ทำต่อ").
1079
- const away = !page && !!where && !matchRoute(where, env.pathname, env.search, env.code);
1492
+ // A page or section guide is on its page: leaving it ends the guide (never "ทำต่อ").
1493
+ const away = !page && !section && !!where && !matchRoute(where, env.pathname, env.search, env.code);
1080
1494
  const bridge = !away && needsBridge(step, env, ctx);
1081
1495
  const nts = !away && !bridge && nothingToSaveHolds(step, env, ctx);
1082
1496
  setScreen(d, {
@@ -1094,6 +1508,8 @@ function enterStep(d, suite, env, mission, index, opts) {
1094
1508
  status: away || bridge ? null : stepStatus(shownStep(step, nts), ctx),
1095
1509
  replayNext: !away && !bridge && opts.replay && replayCanGoOn(mission, i, env, d.state.progress.context, nts),
1096
1510
  ...(page ? { page } : {}),
1511
+ ...(section ? { section } : {}),
1512
+ ...(flow ? { flow } : {}),
1097
1513
  });
1098
1514
  if (!opts.replay) {
1099
1515
  const cp = checkpointAtOrBefore(steps, i);
@@ -1258,9 +1674,12 @@ function advance(d, suite, env, capture) {
1258
1674
  enterStep(d, suite, env, mission, s.index + 1, { replay: s.replay });
1259
1675
  }
1260
1676
  function finishMission(d, suite, env, mission, replay) {
1261
- if (isPageMissionKey(mission.key)) {
1677
+ if (isPageMissionKey(mission.key) || isSectionMissionKey(mission.key) || isFlowMissionKey(mission.key)) {
1262
1678
  endPage(d, suite, "completed");
1263
- d.effects.push({ type: "toast", text: COPY.pageDone });
1679
+ d.effects.push({
1680
+ type: "toast",
1681
+ text: isSectionMissionKey(mission.key) ? COPY.sectionDone : isFlowMissionKey(mission.key) ? COPY.flowDone : COPY.pageDone,
1682
+ });
1264
1683
  return;
1265
1684
  }
1266
1685
  if (replay) {
@@ -1364,7 +1783,8 @@ function startMission(d, suite, env, key, replay) {
1364
1783
  // A mission already done is only ever replayed; its done status stays.
1365
1784
  const isReplay = replay || entry?.status === "done";
1366
1785
  const before = d.state.progress;
1367
- d.state = { ...d.state, panelOpen: false, justDone: null };
1786
+ // A new run: what passed by itself is counted afresh.
1787
+ d.state = { ...d.state, panelOpen: false, justDone: null, autoPassed: null };
1368
1788
  if (d.state.progress.welcome === "not_seen")
1369
1789
  withProgress(d, { welcome: "started" });
1370
1790
  if (!isReplay && d.state.progress.mission_key !== key)
@@ -1523,14 +1943,168 @@ function recordPage(d, suite, key, status) {
1523
1943
  d.state = { ...d.state, pages: { ...d.state.pages, [key]: { version: page.version, status, rev: prev?.rev ?? 0 } } };
1524
1944
  d.effects.push({ type: "savePage", key, version: page.version, status });
1525
1945
  }
1526
- /** The page guide ends: its row written when it is due one; back to idle. */
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
+ /** Write a section guide's row (once per section and version). */
1952
+ function recordSection(d, suite, key, status) {
1953
+ const section = findSection(suite, key);
1954
+ if (!section || sectionRecorded(d.state, section))
1955
+ return;
1956
+ const prev = d.state.sections?.[key];
1957
+ d.state = { ...d.state, sections: { ...d.state.sections, [key]: { version: section.version, status, rev: prev?.rev ?? 0 } } };
1958
+ d.effects.push({ type: "savePage", key, version: section.version, status, kind: "section" });
1959
+ }
1960
+ /** The page or section guide on screen, or null. */
1961
+ function readOf(s) {
1962
+ if (s.name !== "step")
1963
+ return null;
1964
+ if (s.page)
1965
+ return { kind: "page", key: s.page };
1966
+ if (s.section)
1967
+ return { kind: "section", key: s.section };
1968
+ return null;
1969
+ }
1970
+ /** Whether a page or section guide still applies where the person is. */
1971
+ function readStillHere(suite, r, env) {
1972
+ if (r.kind === "page")
1973
+ return pageStillHere(suite, r.key, env);
1974
+ const section = findSection(suite, r.key);
1975
+ return !!section && sectionApplies(section, whereOf(env));
1976
+ }
1977
+ /** The flow's row says this person has ended this version of it. */
1978
+ function flowRecorded(state, flow) {
1979
+ const row = state.flows?.[flow.key];
1980
+ return !!row && row.version >= flow.version;
1981
+ }
1982
+ /** Write a flow's row (once per flow and version). */
1983
+ function recordFlow(d, suite, key, status) {
1984
+ const flow = findFlow(suite, key);
1985
+ if (!flow || flowRecorded(d.state, flow))
1986
+ return;
1987
+ const prev = d.state.flows?.[key];
1988
+ d.state = { ...d.state, flows: { ...d.state.flows, [key]: { version: flow.version, status, rev: prev?.rev ?? 0 } } };
1989
+ d.effects.push({ type: "savePage", key, version: flow.version, status, kind: "flow" });
1990
+ }
1991
+ /** The page guide, section guide or flow ends: its row written when it is due one; back to idle. */
1527
1992
  function endPage(d, suite, how) {
1528
1993
  const run = d.state.pageRun;
1529
- if (run?.write)
1530
- recordPage(d, suite, run.key, how);
1994
+ if (run?.write) {
1995
+ if (run.kind === "section")
1996
+ recordSection(d, suite, run.key, how);
1997
+ else if (run.kind === "flow")
1998
+ recordFlow(d, suite, run.key, how);
1999
+ else
2000
+ recordPage(d, suite, run.key, how);
2001
+ }
2002
+ // A flow's captured ids were its own: the tour's come back.
2003
+ if (run?.kind === "flow" && run.context)
2004
+ withProgress(d, { context: run.context });
1531
2005
  d.state = { ...d.state, pageRun: null, replayPage: null };
1532
2006
  setScreen(d, { name: "idle" });
1533
2007
  }
2008
+ /** The flow on screen, or null. */
2009
+ function flowOf(s) {
2010
+ return s.name === "step" && s.flow ? s.flow : null;
2011
+ }
2012
+ /** The flows this person has that start on this page ("พาทำ · <title>"). */
2013
+ function flowsAvailable(suite, env) {
2014
+ if (!suite.flows?.length)
2015
+ return [];
2016
+ const ctx = guideContext(env, {});
2017
+ const where = whereOf(env);
2018
+ return (suite.flows ?? [])
2019
+ .map(f => findFlow(suite, f?.key))
2020
+ .filter((f) => !!f && onFlowStart(f, where) && flowShown(f, ctx))
2021
+ .map(f => f.key);
2022
+ }
2023
+ /**
2024
+ * The flow to offer now, or null: only in "offer" mode, only a flow that
2025
+ * says `offer: true`, on its start page, with no row of this version, when
2026
+ * nothing else is up and this page's own guide is not due (it comes first:
2027
+ * no guide here, offer: false, or already recorded), its first step's
2028
+ * target on screen, no app dialog open. The host adds the welcome and the
2029
+ * idle wait, as for the page guide's offer.
2030
+ */
2031
+ function flowOfferDue(state, suite, env) {
2032
+ if (state.pagesMode !== "offer" || state.pageAudience === "none")
2033
+ return null;
2034
+ if (state.screen.name !== "idle" || state.panelOpen || (state.pendingCelebrate?.length ?? 0) > 0)
2035
+ return null;
2036
+ const page = pageHere(suite, env);
2037
+ if (page && page.offer !== false && !pageRecorded(state, page))
2038
+ return null;
2039
+ if (safe(() => env.dialogOpen?.() === true, false))
2040
+ return null;
2041
+ for (const key of flowsAvailable(suite, env)) {
2042
+ const flow = findFlow(suite, key);
2043
+ if (flow.offer !== true || flowRecorded(state, flow))
2044
+ continue;
2045
+ const mission = flowMission(flow);
2046
+ const first = mission.steps.find(st => {
2047
+ const ctx = guideContext(env, {});
2048
+ if (st.requires && !safe(() => env.can(st.requires), true))
2049
+ return false;
2050
+ return !(st.skipIf && safe(() => st.skipIf(ctx), false));
2051
+ });
2052
+ if (first && !stepTargetVisible(first, env, guideContext(env, {}), false))
2053
+ continue;
2054
+ return key;
2055
+ }
2056
+ return null;
2057
+ }
2058
+ function startFlow(d, suite, env, key, write, at) {
2059
+ const flow = findFlow(suite, key);
2060
+ if (!flow)
2061
+ return;
2062
+ const mission = flowMission(flow);
2063
+ const saved = d.state.progress.context;
2064
+ d.state = {
2065
+ ...d.state,
2066
+ panelOpen: false,
2067
+ justDone: null,
2068
+ pageRun: { key, write, kind: "flow", context: saved },
2069
+ autoPassed: null,
2070
+ replayPage: {
2071
+ mission: mission.key,
2072
+ anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
2073
+ dialog: safe(() => env.dialogOpen?.() === true, false),
2074
+ },
2075
+ };
2076
+ withProgress(d, { context: at?.context ?? {} });
2077
+ let index = 0;
2078
+ if (at) {
2079
+ const stepAt = mission.steps.findIndex(st => st.key === at.step);
2080
+ if (stepAt > 0)
2081
+ index = checkpointAtOrBefore(mission.steps, stepAt);
2082
+ }
2083
+ enterStep(d, suite, env, mission, index, { replay: true });
2084
+ }
2085
+ /** The section guides on screen here that this person has (the "?" entries, the buttons). */
2086
+ function sectionsAvailable(suite, env) {
2087
+ return sectionsHere(suite, whereOf(env), guideContext(env, {})).map(x => x.key);
2088
+ }
2089
+ function startSection(d, suite, env, key, write) {
2090
+ const section = findSection(suite, key);
2091
+ if (!section)
2092
+ return;
2093
+ const mission = sectionMission(section);
2094
+ d.state = {
2095
+ ...d.state,
2096
+ panelOpen: false,
2097
+ justDone: null,
2098
+ pageRun: { key, write, kind: "section" },
2099
+ autoPassed: null,
2100
+ replayPage: {
2101
+ mission: mission.key,
2102
+ anchors: abortAnchorsOf(mission).filter(a => safe(() => env.isVisible(a), false)),
2103
+ dialog: safe(() => env.dialogOpen?.() === true, false),
2104
+ },
2105
+ };
2106
+ enterStep(d, suite, env, mission, 0, { replay: true });
2107
+ }
1534
2108
  function startPage(d, suite, env, key, write) {
1535
2109
  const page = findPage(suite, key);
1536
2110
  if (!page)
@@ -1540,7 +2114,8 @@ function startPage(d, suite, env, key, write) {
1540
2114
  ...d.state,
1541
2115
  panelOpen: false,
1542
2116
  justDone: null,
1543
- pageRun: { key, write },
2117
+ pageRun: { key, write, kind: "page" },
2118
+ autoPassed: null,
1544
2119
  // What is open now is the person's: ending the guide leaves it alone.
1545
2120
  replayPage: {
1546
2121
  mission: mission.key,
@@ -1640,7 +2215,7 @@ function reduceGuide(state, action, suite, env) {
1640
2215
  break;
1641
2216
  // A mission always wins over a page guide: a running one ends (its row
1642
2217
  // written if due), an offer goes unrecorded.
1643
- if (s.name === "step" && s.page)
2218
+ if (readOf(s) || flowOf(s))
1644
2219
  endPage(d, suite, "skipped");
1645
2220
  else if (s.name === "pageOffer")
1646
2221
  setScreen(d, { name: "idle" });
@@ -1661,12 +2236,21 @@ function reduceGuide(state, action, suite, env) {
1661
2236
  const stepAt = mission.steps.findIndex(st => st.key === d.state.progress.step_key);
1662
2237
  const cpAt = mission.steps.findIndex(st => st.key === d.state.progress.checkpoint_key);
1663
2238
  const index = stepAt >= 0 ? checkpointAtOrBefore(mission.steps, stepAt) : Math.max(cpAt, 0);
2239
+ d.state = { ...d.state, autoPassed: null };
1664
2240
  enterStep(d, suite, env, mission, index, { replay: false });
1665
2241
  break;
1666
2242
  }
1667
2243
  case "sync": {
1668
2244
  if (s.name === "pageOffer") {
1669
2245
  // Ignored: the person left the page (or its sub-screen). Recorded.
2246
+ if (s.flow) {
2247
+ const flow = findFlow(suite, s.page);
2248
+ if (!flow || !onFlowStart(flow, whereOf(env))) {
2249
+ recordFlow(d, suite, s.page, "skipped");
2250
+ setScreen(d, { name: "idle" });
2251
+ }
2252
+ break;
2253
+ }
1670
2254
  if (!pageStillHere(suite, s.page, env)) {
1671
2255
  recordPage(d, suite, s.page, "skipped");
1672
2256
  setScreen(d, { name: "idle" });
@@ -1675,8 +2259,18 @@ function reduceGuide(state, action, suite, env) {
1675
2259
  }
1676
2260
  if (s.name !== "step")
1677
2261
  break;
1678
- if (s.page && !pageStillHere(suite, s.page, env)) {
1679
- // Leaving the page ends its guide, silently.
2262
+ if (s.flow) {
2263
+ // Off the flow's pages: it ends, silently. Between them it goes on
2264
+ // (a step on another of its pages shows "ทำต่อ").
2265
+ const flow = findFlow(suite, s.flow);
2266
+ if (!flow || !inFlowChain(flow, env)) {
2267
+ endPage(d, suite, "skipped");
2268
+ break;
2269
+ }
2270
+ }
2271
+ const read = readOf(s);
2272
+ if (read && !readStillHere(suite, read, env)) {
2273
+ // Leaving the page (or the section's root going) ends its guide, silently.
1680
2274
  endPage(d, suite, "skipped");
1681
2275
  break;
1682
2276
  }
@@ -1709,7 +2303,7 @@ function reduceGuide(state, action, suite, env) {
1709
2303
  break;
1710
2304
  }
1711
2305
  }
1712
- if (!s.bridge && !stepTargetVisible(shownStep(step, s.nothingToSave), env, ctx, false) && offRoute(mission.steps, s.index, env, s.page)) {
2306
+ if (!s.bridge && !stepTargetVisible(shownStep(step, s.nothingToSave), env, ctx, false) && offRoute(mission.steps, s.index, env, s.page ?? s.section)) {
1713
2307
  // The target is missing because the page is another one.
1714
2308
  recoverOffRoute(d, suite, env, false);
1715
2309
  break;
@@ -1825,7 +2419,7 @@ function reduceGuide(state, action, suite, env) {
1825
2419
  const cur = d.state.screen;
1826
2420
  if (stepTargetVisible(shownStep(raw, cur.nothingToSave), env, ctx, cur.bridge))
1827
2421
  break;
1828
- if (!cur.bridge && offRoute(mission.steps, cur.index, env, cur.page)) {
2422
+ if (!cur.bridge && offRoute(mission.steps, cur.index, env, cur.page ?? cur.section)) {
1829
2423
  recoverOffRoute(d, suite, env, false);
1830
2424
  break;
1831
2425
  }
@@ -1846,7 +2440,7 @@ function reduceGuide(state, action, suite, env) {
1846
2440
  if (!mission)
1847
2441
  break;
1848
2442
  const step = mission.steps[s.index];
1849
- if (!s.bridge && offRoute(mission.steps, s.index, env, s.page)) {
2443
+ if (!s.bridge && offRoute(mission.steps, s.index, env, s.page ?? s.section)) {
1850
2444
  recoverOffRoute(d, suite, env, false);
1851
2445
  break;
1852
2446
  }
@@ -1863,7 +2457,7 @@ function reduceGuide(state, action, suite, env) {
1863
2457
  advance(d, suite, env);
1864
2458
  }
1865
2459
  else if (policy === "abort") {
1866
- if (s.page)
2460
+ if (readOf(s) || flowOf(s))
1867
2461
  endPage(d, suite, "skipped");
1868
2462
  else
1869
2463
  exitMission(d);
@@ -1889,7 +2483,7 @@ function reduceGuide(state, action, suite, env) {
1889
2483
  if (s.name !== "step" || s.phase !== "missing")
1890
2484
  break;
1891
2485
  const mission = findMission(suite, s.mission);
1892
- if (mission && !s.bridge && offRoute(mission.steps, s.index, env, s.page)) {
2486
+ if (mission && !s.bridge && offRoute(mission.steps, s.index, env, s.page ?? s.section)) {
1893
2487
  // Polling again cannot help on another page: go back to the step's.
1894
2488
  recoverOffRoute(d, suite, env, true);
1895
2489
  break;
@@ -1905,7 +2499,7 @@ function reduceGuide(state, action, suite, env) {
1905
2499
  case "exit":
1906
2500
  if (s.name !== "step")
1907
2501
  break;
1908
- if (s.page) {
2502
+ if (readOf(s) || flowOf(s)) {
1909
2503
  // ✕, Escape or ปิดคู่มือ: ends silently.
1910
2504
  pushAbort(d, suite, s);
1911
2505
  endPage(d, suite, "skipped");
@@ -1933,6 +2527,11 @@ function reduceGuide(state, action, suite, env) {
1933
2527
  case "endWithoutSave":
1934
2528
  if (s.name !== "step" || !s.replay)
1935
2529
  break;
2530
+ if (flowOf(s)) {
2531
+ pushAbort(d, suite, s);
2532
+ endPage(d, suite, "skipped");
2533
+ break;
2534
+ }
1936
2535
  pushAbort(d, suite, s);
1937
2536
  setScreen(d, { name: "idle" });
1938
2537
  d.effects.push({ type: "toast", text: COPY.replayEnded });
@@ -1981,7 +2580,7 @@ function reduceGuide(state, action, suite, env) {
1981
2580
  break;
1982
2581
  case "openPanel": {
1983
2582
  const section = action.section ?? "all";
1984
- const allowed = section === "pages" ? state.pageAudience !== "none" : state.audience !== "none" || state.pageAudience !== "none";
2583
+ const allowed = section !== "all" ? state.pageAudience !== "none" : state.audience !== "none" || state.pageAudience !== "none";
1985
2584
  if (!allowed || (s.name !== "idle" && s.name !== "pageOffer"))
1986
2585
  break;
1987
2586
  // The offer gives way to the panel, unrecorded.
@@ -2034,6 +2633,13 @@ function reduceGuide(state, action, suite, env) {
2034
2633
  break;
2035
2634
  }
2036
2635
  case "pageOffer": {
2636
+ if (action.flow) {
2637
+ if (flowOfferDue(state, suite, env) !== action.page)
2638
+ break;
2639
+ setScreen(d, { name: "pageOffer", page: action.page, flow: true });
2640
+ d.effects.push({ type: "announce", text: `พาทำงาน ${findFlow(suite, action.page)?.title ?? ""}` });
2641
+ break;
2642
+ }
2037
2643
  if (pageOfferDue(state, suite, env) !== action.page)
2038
2644
  break;
2039
2645
  setScreen(d, { name: "pageOffer", page: action.page });
@@ -2043,14 +2649,42 @@ function reduceGuide(state, action, suite, env) {
2043
2649
  case "pageOfferStart":
2044
2650
  if (s.name !== "pageOffer" || state.pageAudience === "none")
2045
2651
  break;
2046
- startPage(d, suite, env, s.page, true);
2652
+ if (s.flow)
2653
+ startFlow(d, suite, env, s.page, true);
2654
+ else
2655
+ startPage(d, suite, env, s.page, true);
2047
2656
  break;
2048
2657
  case "pageOfferLater":
2049
2658
  if (s.name !== "pageOffer")
2050
2659
  break;
2051
- recordPage(d, suite, s.page, "skipped");
2660
+ if (s.flow)
2661
+ recordFlow(d, suite, s.page, "skipped");
2662
+ else
2663
+ recordPage(d, suite, s.page, "skipped");
2052
2664
  setScreen(d, { name: "idle" });
2053
2665
  break;
2666
+ case "startFlow": {
2667
+ // Only on its start page; never over a mission step, a welcome, a gate
2668
+ // or a celebration.
2669
+ if (state.pageAudience === "none" || state.pagesMode === "off")
2670
+ break;
2671
+ if (s.name !== "idle" && s.name !== "pageOffer")
2672
+ break;
2673
+ if (!flowsAvailable(suite, env).includes(action.flow))
2674
+ break;
2675
+ const flow = findFlow(suite, action.flow);
2676
+ startFlow(d, suite, env, flow.key, !flowRecorded(state, flow));
2677
+ break;
2678
+ }
2679
+ case "resumeFlow": {
2680
+ if (state.pageAudience === "none" || state.pagesMode === "off" || s.name !== "idle")
2681
+ break;
2682
+ const flow = findFlow(suite, action.flow);
2683
+ if (!flow || !flowShown(flow, guideContext(env, {})) || !inFlowChain(flow, whereOf(env)))
2684
+ break;
2685
+ startFlow(d, suite, env, flow.key, action.write && !flowRecorded(state, flow), { step: action.step, context: action.context });
2686
+ break;
2687
+ }
2054
2688
  case "startPage": {
2055
2689
  // Never over a mission step, a welcome, a gate or a celebration.
2056
2690
  if (state.pageAudience === "none" || state.pagesMode === "off")
@@ -2064,7 +2698,7 @@ function reduceGuide(state, action, suite, env) {
2064
2698
  break;
2065
2699
  }
2066
2700
  case "back": {
2067
- if (s.name !== "step" || !s.page || s.phase !== "ready" || s.confirmExit)
2701
+ if (s.name !== "step" || !readOf(s) || s.phase !== "ready" || s.confirmExit)
2068
2702
  break;
2069
2703
  const mission = findMission(suite, s.mission);
2070
2704
  if (!mission)
@@ -2079,12 +2713,39 @@ function reduceGuide(state, action, suite, env) {
2079
2713
  break;
2080
2714
  }
2081
2715
  case "pageRow": {
2716
+ if (action.kind === "flow") {
2717
+ const cur = d.state.flows?.[action.key];
2718
+ if (cur && cur.version > action.row.version)
2719
+ break;
2720
+ d.state = { ...d.state, flows: { ...d.state.flows, [action.key]: action.row } };
2721
+ break;
2722
+ }
2723
+ if (action.kind === "section") {
2724
+ const cur = d.state.sections?.[action.key];
2725
+ if (cur && cur.version > action.row.version)
2726
+ break;
2727
+ d.state = { ...d.state, sections: { ...d.state.sections, [action.key]: action.row } };
2728
+ break;
2729
+ }
2082
2730
  const cur = d.state.pages[action.key];
2083
2731
  if (cur && cur.version > action.row.version)
2084
2732
  break;
2085
2733
  d.state = { ...d.state, pages: { ...d.state.pages, [action.key]: action.row } };
2086
2734
  break;
2087
2735
  }
2736
+ case "startSection": {
2737
+ // Never offered: only on request. Never over a mission step, a
2738
+ // welcome, a gate or a celebration; only on screen, only theirs.
2739
+ if (state.pageAudience === "none" || state.pagesMode === "off")
2740
+ break;
2741
+ if (s.name !== "idle" && s.name !== "pageOffer")
2742
+ break;
2743
+ if (!sectionsAvailable(suite, env).includes(action.section))
2744
+ break;
2745
+ const section = findSection(suite, action.section);
2746
+ startSection(d, suite, env, section.key, !sectionRecorded(state, section));
2747
+ break;
2748
+ }
2088
2749
  case "idleCelebrate": {
2089
2750
  if (s.name !== "idle" || state.panelOpen)
2090
2751
  break;
@@ -2213,6 +2874,8 @@ function parseGuidesAnswer(payload, suite) {
2213
2874
  ? data.page_audience
2214
2875
  : "none";
2215
2876
  const pages = parsePageRows(rows, suite.app);
2877
+ const sections = parsePageRows(rows, suite.app, "section");
2878
+ const flows = parsePageRows(rows, suite.app, "flow");
2216
2879
  let row = null;
2217
2880
  for (const r of rows) {
2218
2881
  if (isRecord(r) && r.guide_key === suite.key) {
@@ -2233,15 +2896,15 @@ function parseGuidesAnswer(payload, suite) {
2233
2896
  progress = row;
2234
2897
  newerOnServer = row.guide_version > suite.version;
2235
2898
  }
2236
- return { mode, audience: mode === "off" ? "none" : audience, progress, newerOnServer, pagesMode, pageAudience, pages };
2899
+ return { mode, audience: mode === "off" ? "none" : audience, progress, newerOnServer, pagesMode, pageAudience, pages, sections, flows };
2237
2900
  }
2238
- /** The row key of a page guide: `<app>.page.<key>`. */
2239
- function pageRowKey(app, key) {
2240
- return `${app}.page.${key}`;
2901
+ /** The row key of a page guide `<app>.page.<key>`, a section guide `<app>.section.<key>`, or a flow `<app>.flow.<key>`. */
2902
+ function pageRowKey(app, key, kind = "page") {
2903
+ return `${app}.${kind}.${key}`;
2241
2904
  }
2242
- /** Page rows among the list's rows, by page key. Off-shape rows are left out. */
2243
- function parsePageRows(rows, app) {
2244
- const prefix = pageRowKey(app, "");
2905
+ /** Page (or section) rows among the list's rows, by key. Off-shape rows are left out. */
2906
+ function parsePageRows(rows, app, kind = "page") {
2907
+ const prefix = pageRowKey(app, "", kind);
2245
2908
  const out = {};
2246
2909
  for (const r of rows) {
2247
2910
  if (!isRecord(r) || typeof r.guide_key !== "string" || !r.guide_key.startsWith(prefix))
@@ -2491,14 +3154,15 @@ async function storeGuides(adapter, suite, progress) {
2491
3154
  return null;
2492
3155
  }
2493
3156
  /**
2494
- * Writes a page guide's row (`<app>.page.<key>`) once: the minimal body,
3157
+ * Writes a page guide's row (`<app>.page.<key>`), or a section guide's
3158
+ * (`<app>.section.<key>`, kind "section"), once: the minimal body,
2495
3159
  * with the same 409 handling as the tour's row. Another tab that already
2496
3160
  * recorded this version wins (its row is returned, nothing is sent again);
2497
3161
  * an older version's row is replaced with its rev; a newer one stops. Returns
2498
3162
  * the stored row, or null when it could not be saved.
2499
3163
  */
2500
- async function storePageRow(adapter, app, key, version, status, rev) {
2501
- const guideKey = pageRowKey(app, key);
3164
+ async function storePageRow(adapter, app, key, version, status, rev, kind = "page") {
3165
+ const guideKey = pageRowKey(app, key, kind);
2502
3166
  let at = rev;
2503
3167
  for (let attempt = 0; attempt < 2; attempt++) {
2504
3168
  let res;
@@ -3416,7 +4080,10 @@ function CompleteModal(p) {
3416
4080
  * corner, not modal (the page stays usable; focus is not taken). Shell ≥ 0.1.74.
3417
4081
  */
3418
4082
  function PageOfferCard(p) {
3419
- return (jsxRuntime.jsxs("section", { className: "eg-card eg-offer", role: "region", "aria-labelledby": "eg-po-t", "data-eg-offer": "", children: [jsxRuntime.jsx("p", { className: "eg-po-e", children: "\u0E04\u0E39\u0E48\u0E21\u0E37\u0E2D\u0E2B\u0E19\u0E49\u0E32\u0E19\u0E35\u0E49" }), jsxRuntime.jsx("p", { className: "eg-b-t", id: "eg-po-t", children: p.title }), jsxRuntime.jsxs("p", { className: "eg-b-w", children: [p.steps, " \u0E02\u0E31\u0E49\u0E19"] }), jsxRuntime.jsxs("div", { className: "eg-row", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: p.onLater, children: "\u0E44\u0E21\u0E48\u0E15\u0E2D\u0E19\u0E19\u0E35\u0E49" }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onStart, children: "\u0E40\u0E23\u0E34\u0E48\u0E21" })] })] }));
4083
+ return (jsxRuntime.jsxs("section", { className: "eg-card eg-offer", role: "region", "aria-labelledby": "eg-po-t", "data-eg-offer": "", children: [jsxRuntime.jsx("p", { className: "eg-po-e", children: p.eyebrow ?? "คู่มือหน้านี้" }), jsxRuntime.jsx("p", { className: "eg-b-t", id: "eg-po-t", children: p.title }), jsxRuntime.jsxs("p", { className: "eg-b-w", children: [p.steps, " \u0E02\u0E31\u0E49\u0E19"] }), jsxRuntime.jsxs("div", { className: "eg-row", children: [jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: p.onLater, children: "\u0E44\u0E21\u0E48\u0E15\u0E2D\u0E19\u0E19\u0E35\u0E49" }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-primary", onClick: p.onStart, children: "\u0E40\u0E23\u0E34\u0E48\u0E21" })] })] }));
4084
+ }
4085
+ function FlowList({ items, onOpen }) {
4086
+ return (jsxRuntime.jsx("ul", { className: "eg-q-list eg-pg-list", children: items.map(it => (jsxRuntime.jsxs("li", { className: "eg-q-item eg-pg-item", children: [jsxRuntime.jsxs("div", { className: "eg-t", children: [jsxRuntime.jsx("b", { children: it.title }), jsxRuntime.jsx("small", { children: it.done ? "ทำแล้ว" : it.minutes ? `ประมาณ ${it.minutes} นาที` : "ใหม่" })] }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onOpen(it.key), children: it.done ? "ทำอีกครั้ง" : "เริ่ม" })] }, it.key))) }));
3420
4087
  }
3421
4088
  function PageList({ items, onOpen }) {
3422
4089
  return (jsxRuntime.jsx("ul", { className: "eg-q-list eg-pg-list", children: items.map(it => (jsxRuntime.jsxs("li", { className: "eg-q-item eg-pg-item", children: [jsxRuntime.jsxs("div", { className: "eg-t", children: [jsxRuntime.jsx("b", { children: it.title }), jsxRuntime.jsx("small", { className: it.seen ? undefined : "eg-new", children: it.seen ? "ดูแล้ว" : "ใหม่" })] }), jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-ghost", onClick: () => onOpen(it.key), children: "\u0E40\u0E1B\u0E34\u0E14" })] }, it.key))) }));
@@ -3443,12 +4110,16 @@ function QuestChip({ done, total, pulse, onOpen }) {
3443
4110
  }
3444
4111
  function QuestPanel(p) {
3445
4112
  const ref = useAutoFocus();
3446
- const pages = p.pages ?? [];
4113
+ const pages = p.only === "flows" ? [] : (p.pages ?? []);
4114
+ const flows = p.only === "pages" ? [] : (p.flows ?? []);
3447
4115
  const openPage = p.onOpenPage ?? (() => { });
4116
+ const openFlow = p.onOpenFlow ?? (() => { });
4117
+ const flowSection = flows.length ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("h3", { className: "eg-q-sub", id: "eg-q-flows", children: "\u0E1E\u0E32\u0E17\u0E33\u0E07\u0E32\u0E19" }), jsxRuntime.jsx(FlowList, { items: flows, onOpen: openFlow })] })) : null;
3448
4118
  if (p.showMissions === false) {
3449
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxRuntime.jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxRuntime.jsxs("div", { className: "eg-q-h", children: [jsxRuntime.jsx("h2", { id: "eg-q-t", children: "\u0E04\u0E39\u0E48\u0E21\u0E37\u0E2D\u0E41\u0E15\u0E48\u0E25\u0E30\u0E2B\u0E19\u0E49\u0E32" }), jsxRuntime.jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsxRuntime.jsx(IconX, {}) })] }), jsxRuntime.jsx(PageList, { items: pages, onOpen: openPage })] })] }));
4119
+ const heading = pages.length ? "คู่มือแต่ละหน้า" : "พาทำงาน";
4120
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxRuntime.jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxRuntime.jsxs("div", { className: "eg-q-h", children: [jsxRuntime.jsx("h2", { id: "eg-q-t", children: heading }), jsxRuntime.jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsxRuntime.jsx(IconX, {}) })] }), pages.length ? jsxRuntime.jsx(PageList, { items: pages, onOpen: openPage }) : null, pages.length ? flowSection : flows.length ? jsxRuntime.jsx(FlowList, { items: flows, onOpen: openFlow }) : null] })] }));
3450
4121
  }
3451
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxRuntime.jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxRuntime.jsxs("div", { className: "eg-q-h", children: [jsxRuntime.jsx("h2", { id: "eg-q-t", children: "\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19" }), jsxRuntime.jsxs("span", { className: "eg-n", children: [p.done, "/", p.total] }), jsxRuntime.jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsxRuntime.jsx(IconX, {}) })] }), jsxRuntime.jsx("div", { className: "eg-prog", children: jsxRuntime.jsx("div", { className: "eg-bar", children: jsxRuntime.jsx("i", { style: { width: `${p.total ? (p.done / p.total) * 100 : 0}%` } }) }) }), jsxRuntime.jsx("ol", { className: "eg-q-list", children: p.items.map((it, i) => (jsxRuntime.jsx(QuestRow, { item: it, n: i + 1, just: p.justDone === it.key, gate: p.gate, onStart: p.onStart, onDismiss: p.onDismiss }, it.key))) }), pages.length ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("h3", { className: "eg-q-sub", id: "eg-q-pages", children: "\u0E04\u0E39\u0E48\u0E21\u0E37\u0E2D\u0E41\u0E15\u0E48\u0E25\u0E30\u0E2B\u0E19\u0E49\u0E32" }), jsxRuntime.jsx(PageList, { items: pages, onOpen: openPage })] })) : null, p.canHideChip ? (jsxRuntime.jsx("div", { className: "eg-q-f", children: jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: p.onHideChip, children: "\u0E0B\u0E48\u0E2D\u0E19\u0E1B\u0E38\u0E48\u0E21\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08" }) })) : null] })] }));
4122
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "eg-panel-bd", onClick: p.onClose }), jsxRuntime.jsxs("section", { ref: ref, className: "eg-panel", role: "dialog", "aria-labelledby": "eg-q-t", tabIndex: -1, "data-eg-panel": "", children: [jsxRuntime.jsxs("div", { className: "eg-q-h", children: [jsxRuntime.jsx("h2", { id: "eg-q-t", children: "\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08\u0E40\u0E23\u0E34\u0E48\u0E21\u0E15\u0E49\u0E19" }), jsxRuntime.jsxs("span", { className: "eg-n", children: [p.done, "/", p.total] }), jsxRuntime.jsx("button", { type: "button", className: "eg-x", onClick: p.onClose, "aria-label": "\u0E1B\u0E34\u0E14", children: jsxRuntime.jsx(IconX, {}) })] }), jsxRuntime.jsx("div", { className: "eg-prog", children: jsxRuntime.jsx("div", { className: "eg-bar", children: jsxRuntime.jsx("i", { style: { width: `${p.total ? (p.done / p.total) * 100 : 0}%` } }) }) }), jsxRuntime.jsx("ol", { className: "eg-q-list", children: p.items.map((it, i) => (jsxRuntime.jsx(QuestRow, { item: it, n: i + 1, just: p.justDone === it.key, gate: p.gate, onStart: p.onStart, onDismiss: p.onDismiss }, it.key))) }), pages.length ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("h3", { className: "eg-q-sub", id: "eg-q-pages", children: "\u0E04\u0E39\u0E48\u0E21\u0E37\u0E2D\u0E41\u0E15\u0E48\u0E25\u0E30\u0E2B\u0E19\u0E49\u0E32" }), jsxRuntime.jsx(PageList, { items: pages, onOpen: openPage })] })) : null, flowSection, p.canHideChip ? (jsxRuntime.jsx("div", { className: "eg-q-f", children: jsxRuntime.jsx("button", { type: "button", className: "eg-btn eg-link", onClick: p.onHideChip, children: "\u0E0B\u0E48\u0E2D\u0E19\u0E1B\u0E38\u0E48\u0E21\u0E20\u0E32\u0E23\u0E01\u0E34\u0E08" }) })) : null] })] }));
3452
4123
  }
3453
4124
  function QuestRow({ item, n, just, gate, onStart, onDismiss, }) {
3454
4125
  const v = item.view;
@@ -3586,11 +4257,10 @@ function writeMirror(orgId, suite, p) {
3586
4257
  /* private mode: the server copy is enough */
3587
4258
  }
3588
4259
  }
3589
- /** Page rows written in this tab, kept for a remount within the GET's cache time. */
3590
- const pagesMirrorKey = (orgId, suite) => `erp-guide-pages:${orgId}:${suite.app}`;
3591
- function readPagesMirror(orgId, suite) {
4260
+ const pagesMirrorKey = (orgId, suite, kind = "page") => `erp-guide-${kind}s:${orgId}:${suite.app}`;
4261
+ function readPagesMirror(orgId, suite, kind = "page") {
3592
4262
  try {
3593
- const raw = sessionStorage.getItem(pagesMirrorKey(orgId, suite));
4263
+ const raw = sessionStorage.getItem(pagesMirrorKey(orgId, suite, kind));
3594
4264
  const parsed = raw ? JSON.parse(raw) : {};
3595
4265
  const out = {};
3596
4266
  for (const [k, v] of Object.entries(parsed ?? {})) {
@@ -3603,9 +4273,9 @@ function readPagesMirror(orgId, suite) {
3603
4273
  return {};
3604
4274
  }
3605
4275
  }
3606
- function writePagesMirror(orgId, suite, pages) {
4276
+ function writePagesMirror(orgId, suite, pages, kind = "page") {
3607
4277
  try {
3608
- sessionStorage.setItem(pagesMirrorKey(orgId, suite), JSON.stringify(pages));
4278
+ sessionStorage.setItem(pagesMirrorKey(orgId, suite, kind), JSON.stringify(pages));
3609
4279
  }
3610
4280
  catch {
3611
4281
  /* private mode */
@@ -3621,6 +4291,8 @@ function mergePageRows(server, local) {
3621
4291
  }
3622
4292
  return out;
3623
4293
  }
4294
+ /** A flow in progress in this tab (resumed after a refresh, never across sessions). */
4295
+ const flowMirrorKey = (orgId, suite) => `erp-guide-flow-run:${orgId}:${suite.app}`;
3624
4296
  /* ------------------------------------------------------------------ */
3625
4297
  /* One tab at a time */
3626
4298
  /* ------------------------------------------------------------------ */
@@ -3733,7 +4405,9 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
3733
4405
  const [answer, setAnswer] = React.useState(null);
3734
4406
  const missionsReady = config.ready !== false;
3735
4407
  const pagesReady = config.pagesReady ?? missionsReady;
3736
- const hasPages = React.useMemo(() => validPages(suite).length > 0, [suite]);
4408
+ const hasPages = React.useMemo(() => validPages(suite).length > 0 || validSections(suite).length > 0 || validFlows(suite).length > 0, [suite]);
4409
+ const hasFlows = React.useMemo(() => validFlows(suite).length > 0, [suite]);
4410
+ const hasSections = React.useMemo(() => validSections(suite).length > 0, [suite]);
3737
4411
  const enabled = primary && (missionsReady || (hasPages && pagesReady));
3738
4412
  const loaded = React.useRef(false);
3739
4413
  React.useEffect(() => {
@@ -3789,6 +4463,8 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
3789
4463
  pageAudience: pagesOn ? answer.pageAudience : "none",
3790
4464
  pagesMode: answer.pagesMode,
3791
4465
  pages: mergePageRows(answer.pages, readPagesMirror(organizationId, suite)),
4466
+ sections: mergePageRows(answer.sections ?? {}, readPagesMirror(organizationId, suite, "section")),
4467
+ flows: mergePageRows(answer.flows ?? {}, readPagesMirror(organizationId, suite, "flow")),
3792
4468
  });
3793
4469
  stateRef.current = s;
3794
4470
  setState(s);
@@ -3797,8 +4473,12 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
3797
4473
  const pageRef = React.useRef({ pathname, search, sidebarOpen, facts: config.facts ?? {}, can: config.can });
3798
4474
  pageRef.current = { pathname, search, sidebarOpen, facts: config.facts ?? {}, can: config.can };
3799
4475
  const targetElRef = React.useRef(null);
4476
+ // The section guide being opened (before its first step is on screen).
4477
+ const openingSection = React.useRef(null);
3800
4478
  const env = React.useCallback(() => {
3801
4479
  const pg = pageRef.current;
4480
+ const sc = stateRef.current?.screen;
4481
+ const sectionKey = (sc?.name === "step" ? sc.section : undefined) ?? openingSection.current;
3802
4482
  return {
3803
4483
  code,
3804
4484
  pathname: pg.pathname,
@@ -3816,6 +4496,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
3816
4496
  sidebarOpen: pg.sidebarOpen,
3817
4497
  targetValue: () => fieldValue(targetElRef.current),
3818
4498
  dialogOpen: () => appDialogOpen(),
4499
+ values: getSectionGuideValues(sectionKey),
3819
4500
  };
3820
4501
  }, [code]);
3821
4502
  /* ---------------- effects: save, announce, toast, navigate ---------------- */
@@ -3891,13 +4572,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
3891
4572
  }
3892
4573
  else if (e.type === "savePage") {
3893
4574
  // Once per page and version; the machine already holds it as written.
3894
- if (stateRef.current)
3895
- writePagesMirror(organizationId, suite, stateRef.current.pages);
3896
- const rev = stateRef.current?.pages[e.key]?.rev ?? 0;
3897
- void storePageRow(adapterRef.current, suite.app, e.key, e.version, e.status, rev).then(row => {
4575
+ const kind = e.kind ?? "page";
4576
+ const cur = stateRef.current;
4577
+ const rows = cur ? (kind === "section" ? cur.sections : kind === "flow" ? cur.flows : cur.pages) : undefined;
4578
+ if (rows)
4579
+ writePagesMirror(organizationId, suite, rows, kind);
4580
+ const rev = rows?.[e.key]?.rev ?? 0;
4581
+ void storePageRow(adapterRef.current, suite.app, e.key, e.version, e.status, rev, kind).then(row => {
3898
4582
  // eslint-disable-next-line @typescript-eslint/no-use-before-define
3899
4583
  if (row)
3900
- dispatchRef.current({ type: "pageRow", key: e.key, row });
4584
+ dispatchRef.current({ type: "pageRow", key: e.key, row, kind });
3901
4585
  });
3902
4586
  }
3903
4587
  }
@@ -3987,7 +4671,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
3987
4671
  /* ---------------- sync on route, sidebar, facts, DOM ---------------- */
3988
4672
  // A page guide's step runs while page guides are live; a mission's while
3989
4673
  // the missions are. The offer card is watched the same way (left: ignored).
3990
- const inStep = screen.name === "step" && (screen.page ? pagesLive : missionsLive);
4674
+ const inStep = screen.name === "step" && (screen.page || screen.section || screen.flow ? pagesLive : missionsLive);
3991
4675
  const offerUp = pagesLive && screen.name === "pageOffer";
3992
4676
  const watching = inStep || offerUp;
3993
4677
  React.useEffect(() => {
@@ -4035,6 +4719,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4035
4719
  const step = shownStep(raw, screen.nothingToSave);
4036
4720
  return { mission, step, raw };
4037
4721
  }, [state, screen, suite]);
4722
+ const valuesVersion = useSectionValuesVersion();
4038
4723
  const stepCtx = React.useMemo(() => {
4039
4724
  if (!state)
4040
4725
  return null;
@@ -4043,7 +4728,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4043
4728
  return guideContext(e, state.progress.context, params);
4044
4729
  // pathname/search/factsKey make it current.
4045
4730
  // eslint-disable-next-line react-hooks/exhaustive-deps
4046
- }, [state, stepInfo, env, code, pathname, search, factsKey]);
4731
+ }, [state, stepInfo, env, code, pathname, search, factsKey, valuesVersion]);
4047
4732
  const target = stepInfo && stepCtx && screen.name === "step"
4048
4733
  ? spotlightTarget(stepInfo.step, stepCtx, screen.bridge)
4049
4734
  : null;
@@ -4551,7 +5236,7 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4551
5236
  /* ---------------- page guides: this page's, the offer, the list ---------------- */
4552
5237
  // `when` anchors (LIFF's sub-screens) come and go without a route change:
4553
5238
  // look again when the page changes.
4554
- const hasWhen = React.useMemo(() => validPages(suite).some(p => !!p.when), [suite]);
5239
+ const hasWhen = React.useMemo(() => validPages(suite).some(p => !!p.when) || validSections(suite).length > 0, [suite]);
4555
5240
  const [domTick, setDomTick] = React.useState(0);
4556
5241
  React.useEffect(() => {
4557
5242
  if (!pagesLive || !hasWhen || typeof MutationObserver === "undefined")
@@ -4585,6 +5270,30 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4585
5270
  // eslint-disable-next-line react-hooks/exhaustive-deps
4586
5271
  [pagesLive, hasState, suite, env, pathname, search, factsKey, domTick]);
4587
5272
  const listed = React.useMemo(() => (pagesLive && ctxIdle ? listedPages(suite, ctxIdle) : []), [pagesLive, suite, ctxIdle]);
5273
+ const sectionsNow = React.useMemo(() => pagesLive && hasState && hasSections
5274
+ ? sectionsAvailable(suite, env()).map(k => ({ key: k, title: findSection(suite, k)?.title ?? "" }))
5275
+ : [],
5276
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5277
+ [pagesLive, hasState, hasSections, suite, env, pathname, search, factsKey, domTick]);
5278
+ const sectionsSig = sectionsNow.map(x => x.key).join(",");
5279
+ const openSection = React.useCallback((key) => {
5280
+ openingSection.current = key;
5281
+ try {
5282
+ dispatchRef.current({ type: "startSection", section: key });
5283
+ }
5284
+ finally {
5285
+ openingSection.current = null;
5286
+ }
5287
+ }, []);
5288
+ const flowsNow = React.useMemo(() => pagesLive && hasState && hasFlows
5289
+ ? flowsAvailable(suite, env()).map(k => ({ key: k, title: findFlow(suite, k)?.title ?? "" }))
5290
+ : [],
5291
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5292
+ [pagesLive, hasState, hasFlows, suite, env, pathname, search, factsKey]);
5293
+ const flowsSig = flowsNow.map(x => x.key).join(",");
5294
+ /** The flows the panel lists: this person's, startable here or with somewhere to go. */
5295
+ const flowsListed = React.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]);
5296
+ const openFlowHere = React.useCallback((key) => dispatchRef.current({ type: "startFlow", flow: key }), []);
4588
5297
  const openPageHere = React.useCallback(() => {
4589
5298
  const p = pageHere(suite, env());
4590
5299
  if (p)
@@ -4595,8 +5304,16 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4595
5304
  publishPageGuides(null, null, null);
4596
5305
  return;
4597
5306
  }
4598
- publishPageGuides({ page: pageNow ? { title: pageNow.title } : null, listed: listed.length }, openPageHere, () => dispatchRef.current({ type: "openPanel", section: "pages" }));
4599
- }, [pagesLive, hasState, pageNow, listed.length, openPageHere]);
5307
+ publishPageGuides({
5308
+ page: pageNow ? { title: pageNow.title } : null,
5309
+ listed: listed.length,
5310
+ sections: sectionsNow,
5311
+ flows: flowsNow,
5312
+ flowsListed: flowsListed.length,
5313
+ }, openPageHere, () => dispatchRef.current({ type: "openPanel", section: "pages" }), openSection, openFlowHere, () => dispatchRef.current({ type: "openPanel", section: "flows" }));
5314
+ // sectionsSig / flowsSig stand for sectionsNow / flowsNow.
5315
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5316
+ }, [pagesLive, hasState, pageNow, listed.length, openPageHere, sectionsSig, openSection, flowsSig, flowsListed.length, openFlowHere]);
4600
5317
  React.useEffect(() => () => publishPageGuides(null, null, null), []);
4601
5318
  // The first-visit offer: in "offer" mode, nothing else up, the welcome
4602
5319
  // not due here, and a moment of nothing loading on this page.
@@ -4624,14 +5341,21 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4624
5341
  if (Date.now() - idleSince < PAGE_OFFER_IDLE_MS)
4625
5342
  return;
4626
5343
  let key = null;
5344
+ let flow = null;
4627
5345
  try {
4628
5346
  key = pageOfferDue(cur, suite, env());
5347
+ // A flow's offer comes after the page guide's (and only for `offer: true` flows).
5348
+ if (!key)
5349
+ flow = flowOfferDue(cur, suite, env());
4629
5350
  }
4630
5351
  catch {
4631
5352
  key = null;
5353
+ flow = null;
4632
5354
  }
4633
5355
  if (key)
4634
5356
  dispatchRef.current({ type: "pageOffer", page: key });
5357
+ else if (flow)
5358
+ dispatchRef.current({ type: "pageOffer", page: flow, flow: true });
4635
5359
  }, PAGE_POLL_MS);
4636
5360
  return () => clearInterval(iv);
4637
5361
  }, [offerWatch, suite, env, pathname, search]);
@@ -4683,6 +5407,14 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4683
5407
  if (b)
4684
5408
  return;
4685
5409
  const e = env();
5410
+ if (pending.flow) {
5411
+ const flow = findFlow(suite, pending.key);
5412
+ if (!flow || !onFlowStart(flow, e))
5413
+ return;
5414
+ setPending(null);
5415
+ dispatchRef.current({ type: "startFlow", flow: pending.key });
5416
+ return;
5417
+ }
4686
5418
  const page = findPage(suite, pending.key);
4687
5419
  if (!page || !pageMatchScore(page, { code, pathname: e.pathname, search: e.search, isVisible: a => e.isVisible(a) }))
4688
5420
  return;
@@ -4691,6 +5423,93 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4691
5423
  }, PAGE_POLL_MS);
4692
5424
  return () => clearInterval(iv);
4693
5425
  }, [pending, pagesLive, suite, env, code]);
5426
+ // A flow in the panel's list: start it here, or go to its start page and start it there.
5427
+ const openListedFlow = React.useCallback((key) => {
5428
+ const flow = findFlow(suite, key);
5429
+ if (!flow)
5430
+ return;
5431
+ const e = env();
5432
+ if (onFlowStart(flow, e)) {
5433
+ dispatchRef.current({ type: "startFlow", flow: key });
5434
+ return;
5435
+ }
5436
+ dispatchRef.current({ type: "closePanel" });
5437
+ const href = flowHref(flow, guideContext(e, {}));
5438
+ if (!href)
5439
+ return;
5440
+ setPending({ key, until: Date.now() + PAGE_ARRIVE_MS, flow: true });
5441
+ try {
5442
+ if (/^https?:\/\//i.test(href))
5443
+ window.location.assign(href);
5444
+ else
5445
+ navigateRef.current(href);
5446
+ }
5447
+ catch {
5448
+ /* never breaks the app */
5449
+ }
5450
+ }, [suite, env]);
5451
+ // A flow in progress is kept in this tab's session (sessionStorage), so a
5452
+ // refresh or a full page load on one of its pages picks it up again at
5453
+ // its checkpoint ("ทำต่อ" when its step is on another page). Never across
5454
+ // sessions; a flow that ends is forgotten.
5455
+ const flowRun = state?.pageRun?.kind === "flow" ? state.pageRun : null;
5456
+ const flowStepKey = screen.name === "step" && screen.flow ? (findMission(suite, screen.mission)?.steps[screen.index]?.key ?? null) : null;
5457
+ React.useEffect(() => {
5458
+ if (!hasFlows)
5459
+ return;
5460
+ const k = flowMirrorKey(organizationId, suite);
5461
+ try {
5462
+ if (!flowRun) {
5463
+ if (state)
5464
+ sessionStorage.removeItem(k);
5465
+ return;
5466
+ }
5467
+ if (!flowStepKey)
5468
+ return;
5469
+ const flow = findFlow(suite, flowRun.key);
5470
+ sessionStorage.setItem(k, JSON.stringify({ flow: flowRun.key, version: flow?.version, step: flowStepKey, write: flowRun.write, context: state?.progress.context ?? {} }));
5471
+ }
5472
+ catch {
5473
+ /* private mode: no resume after a refresh */
5474
+ }
5475
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5476
+ }, [hasFlows, flowRun?.key, flowRun?.write, flowStepKey, organizationId, suite]);
5477
+ const flowResumed = React.useRef(false);
5478
+ React.useEffect(() => {
5479
+ if (flowResumed.current || !pagesLive || !state || !hasFlows)
5480
+ return;
5481
+ // The missions resume first.
5482
+ if (missionsLive && !resumed.current)
5483
+ return;
5484
+ flowResumed.current = true;
5485
+ const k = flowMirrorKey(organizationId, suite);
5486
+ let saved = null;
5487
+ try {
5488
+ saved = JSON.parse(sessionStorage.getItem(k) ?? "null");
5489
+ }
5490
+ catch {
5491
+ saved = null;
5492
+ }
5493
+ if (!saved?.flow || !saved.step)
5494
+ return;
5495
+ const flow = findFlow(suite, saved.flow);
5496
+ const ok = !!flow && flow.version === saved.version && stateRef.current?.screen.name === "idle" && inFlowChain(flow, { code, pathname });
5497
+ if (!ok) {
5498
+ try {
5499
+ sessionStorage.removeItem(k);
5500
+ }
5501
+ catch {
5502
+ /* ignore */
5503
+ }
5504
+ return;
5505
+ }
5506
+ const context = {};
5507
+ for (const [ck, cv] of Object.entries(saved.context ?? {}))
5508
+ if (typeof cv === "string")
5509
+ context[ck] = cv;
5510
+ dispatchRef.current({ type: "resumeFlow", flow: saved.flow, step: saved.step, context, write: saved.write === true });
5511
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5512
+ }, [pagesLive, !!state, hasFlows, missionsLive]);
4694
5513
  if (!live || !state || !layer || !ctxIdle)
4695
5514
  return null;
4696
5515
  /* ---------------- render ---------------- */
@@ -4713,16 +5532,24 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4713
5532
  const missionNo = missionNumber(suite, mission.key, ctxIdle);
4714
5533
  const kind = stepKind(step);
4715
5534
  const bridge = screen.bridge;
4716
- const pos = stepPosition(mission, screen.index, env(), state.progress.context);
5535
+ const pos = stepPosition(mission, screen.index, env(), state.progress.context, autoPassedOf(state, mission.key));
4717
5536
  const page = screen.page ? findPage(suite, screen.page) : undefined;
4718
- const pageBits = page
5537
+ const section = screen.section ? findSection(suite, screen.section) : undefined;
5538
+ const flow = screen.flow ? findFlow(suite, screen.flow) : undefined;
5539
+ const read = page ? { heading: `คู่มือหน้านี้ · ${page.title}`, exitLabel: "ปิดคู่มือ" } : section ? { heading: `อธิบายส่วนนี้ · ${section.title}`, exitLabel: "ปิดคำอธิบาย" } : null;
5540
+ const pageBits = read
4719
5541
  ? {
4720
- heading: `คู่มือหน้านี้ · ${page.title}`,
4721
- exitLabel: "ปิดคู่มือ",
5542
+ ...read,
4722
5543
  showBack: kind === "view" && !!backTarget(mission, screen.index, env(), state.progress.context),
4723
5544
  nextLabel: pos.no >= pos.total ? "เสร็จ" : undefined,
4724
5545
  }
4725
- : {};
5546
+ : flow
5547
+ ? {
5548
+ heading: `พาทำ · ${flow.title}`,
5549
+ exitLabel: "ออกจากการพาทำ",
5550
+ nextLabel: kind === "view" && pos.no >= pos.total ? "เสร็จ" : undefined,
5551
+ }
5552
+ : {};
4726
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: {
4727
5554
  missionNo,
4728
5555
  missionTitle: mission.title,
@@ -4739,6 +5566,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4739
5566
  ...pageBits,
4740
5567
  }, 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" }), onBack: () => dispatch({ type: "back" }), onEndWithoutSave: () => dispatch({ type: "endWithoutSave" }), onRetry: () => dispatch({ type: "retry" }), onGoResume: () => dispatch({ type: "goResume" }), onExitConfirm: () => dispatch({ type: "exitConfirm" }), onExitCancel: () => dispatch({ type: "exitCancel" }) }));
4741
5568
  }
5569
+ else if (screen.name === "pageOffer" && pagesLive && screen.flow) {
5570
+ const flow = findFlow(suite, screen.page);
5571
+ const mission = flow ? findMission(suite, FLOW_PREFIX + flow.key) : undefined;
5572
+ if (flow && mission) {
5573
+ content = (jsxRuntime.jsx(PageOfferCard, { eyebrow: "\u0E1E\u0E32\u0E17\u0E33\u0E07\u0E32\u0E19", title: flow.title, steps: stepPosition(mission, 0, env(), {}).total, onStart: () => dispatch({ type: "pageOfferStart" }), onLater: () => dispatch({ type: "pageOfferLater" }) }));
5574
+ }
5575
+ }
4742
5576
  else if (screen.name === "pageOffer" && pagesLive) {
4743
5577
  const page = findPage(suite, screen.page);
4744
5578
  if (page) {
@@ -4782,12 +5616,13 @@ function GuideRuntimeInner({ config, organizationId, code, routing, progress: pr
4782
5616
  const showChip = missionsLive && chipVisible(state, suite, ctxIdle);
4783
5617
  // The panel: the missions (when this person has them, and not "คู่มือทุกหน้า"),
4784
5618
  // then the page guides they have.
4785
- const panelMissions = missionsLive && total > 0 && state.panelSection !== "pages";
4786
- const pageItems = pagesLive
4787
- ? listed.map(p => ({ key: p.key, title: p.title, seen: pageRecorded(state, p) }))
5619
+ const panelMissions = missionsLive && total > 0 && state.panelSection === "all";
5620
+ const pageItems = pagesLive && state.panelSection !== "flows" ? listed.map(p => ({ key: p.key, title: p.title, seen: pageRecorded(state, p) })) : [];
5621
+ const flowItems = pagesLive && state.panelSection !== "pages"
5622
+ ? flowsListed.map(f => ({ key: f.key, title: f.title, minutes: f.minutes, done: flowRecorded(state, f) && state.flows[f.key]?.status === "completed" }))
4788
5623
  : [];
4789
- const panelShown = state.panelOpen && screen.name === "idle" && (panelMissions || pageItems.length > 0);
4790
- return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("style", { children: GUIDE_CSS }), jsxRuntime.jsx("div", { className: "eg-sr", "aria-live": "polite", role: "status", children: liveText }), content, showChip ? jsxRuntime.jsx(QuestChip, { done: done, total: total, pulse: state.chipPulse, onOpen: () => dispatch({ type: "openPanel" }) }) : null, panelShown ? (jsxRuntime.jsx(QuestPanel, { items: items, done: done, total: total, justDone: state.justDone, gate: gate && gateFailing ? { label: gate.action.label, pending: gate.pending, href: gateHref } : null, canHideChip: !state.progress.chip_hidden && state.progress.status !== "completed", onClose: () => dispatch({ type: "closePanel" }), onStart: (key, replayIt) => dispatch({ type: "start", mission: key, replay: replayIt }), onDismiss: key => dispatch({ type: "dismissMission", mission: key }), onHideChip: () => dispatch({ type: "hideChip" }), pages: pageItems, showMissions: panelMissions, onOpenPage: openListedPage })) : null, toast ? jsxRuntime.jsx(GuideToast, { text: toast }) : null] }), layer);
5624
+ const panelShown = state.panelOpen && screen.name === "idle" && (panelMissions || pageItems.length > 0 || flowItems.length > 0);
5625
+ return reactDom.createPortal(jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("style", { children: GUIDE_CSS }), jsxRuntime.jsx("div", { className: "eg-sr", "aria-live": "polite", role: "status", children: liveText }), content, showChip ? jsxRuntime.jsx(QuestChip, { done: done, total: total, pulse: state.chipPulse, onOpen: () => dispatch({ type: "openPanel" }) }) : null, panelShown ? (jsxRuntime.jsx(QuestPanel, { items: items, done: done, total: total, justDone: state.justDone, gate: gate && gateFailing ? { label: gate.action.label, pending: gate.pending, href: gateHref } : null, canHideChip: !state.progress.chip_hidden && state.progress.status !== "completed", onClose: () => dispatch({ type: "closePanel" }), onStart: (key, replayIt) => dispatch({ type: "start", mission: key, replay: replayIt }), onDismiss: key => dispatch({ type: "dismissMission", mission: key }), onHideChip: () => dispatch({ type: "hideChip" }), pages: pageItems, showMissions: panelMissions, onOpenPage: openListedPage, flows: flowItems, onOpenFlow: openListedFlow })) : null, toast ? jsxRuntime.jsx(GuideToast, { text: toast }) : null] }), layer);
4791
5626
  }
4792
5627
  /** A stable key for the facts object, so effects re-run when they change. */
4793
5628
  function useFactsKey(facts) {
@@ -4801,28 +5636,134 @@ function useFactsKey(facts) {
4801
5636
  }, [facts]);
4802
5637
  }
4803
5638
 
5639
+ const DEFAULT_STYLE = {
5640
+ display: "inline-grid",
5641
+ placeItems: "center",
5642
+ width: 22,
5643
+ height: 22,
5644
+ padding: 0,
5645
+ border: 0,
5646
+ borderRadius: 999,
5647
+ background: "transparent",
5648
+ color: "inherit",
5649
+ opacity: 0.6,
5650
+ cursor: "pointer",
5651
+ flex: "none",
5652
+ };
5653
+ function SectionGuideButton({ guideKey, values, className }) {
5654
+ useSectionGuideValues(guideKey, values);
5655
+ const { sections } = usePageGuides();
5656
+ const here = (sections ?? []).find(s => s.key === guideKey);
5657
+ if (!here)
5658
+ return null;
5659
+ return (jsxRuntime.jsx("button", { type: "button", className: className, style: className ? undefined : DEFAULT_STYLE, "aria-label": `อธิบายส่วนนี้ ${here.title}`, title: "\u0E2D\u0E18\u0E34\u0E1A\u0E32\u0E22\u0E2A\u0E48\u0E27\u0E19\u0E19\u0E35\u0E49", "data-guide-section-button": guideKey, onClick: e => {
5660
+ // A header that opens or sorts on press must not hear it.
5661
+ e.stopPropagation();
5662
+ e.preventDefault();
5663
+ openSectionGuide(guideKey);
5664
+ }, children: jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10" }), jsxRuntime.jsx("path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }), jsxRuntime.jsx("path", { d: "M12 17h.01" })] }) }));
5665
+ }
5666
+
5667
+ /**
5668
+ * Small formatters for section guides' copy (shell 0.1.75): the app passes
5669
+ * its own computed numbers, and the copy says them the way the page does —
5670
+ * "ม.ค. รับ 500,000 − จ่าย 320,000 = คงเหลือ 180,000".
5671
+ *
5672
+ * Pure: the tests run it in Node.
5673
+ */
5674
+ const THAI_MONTHS_SHORT = ["ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค."];
5675
+ const grouped = new Map();
5676
+ function nf(decimals) {
5677
+ let f = grouped.get(decimals);
5678
+ if (!f) {
5679
+ f = new Intl.NumberFormat("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
5680
+ grouped.set(decimals, f);
5681
+ }
5682
+ return f;
5683
+ }
5684
+ /**
5685
+ * A baht amount with thousands separators, no currency sign: 500000 →
5686
+ * "500,000"; 1234.5 → "1,234.50"; -320000 → "-320,000". Whole amounts
5687
+ * have no decimals unless `decimals` says so; others have two. Not a
5688
+ * finite number → "" (a step whose value is missing is skipped anyway).
5689
+ */
5690
+ function formatBaht(n, opts = {}) {
5691
+ const v = typeof n === "string" && n.trim() !== "" ? Number(n) : n;
5692
+ if (typeof v !== "number" || !Number.isFinite(v))
5693
+ return "";
5694
+ const rounded = Math.round(v * 100) / 100;
5695
+ const decimals = opts.decimals ?? (Number.isInteger(rounded) ? 0 : 2);
5696
+ const out = nf(decimals).format(rounded);
5697
+ // No "-0".
5698
+ return out === "-0" || /^-0(\.0+)?$/.test(out) ? out.slice(1) : out;
5699
+ }
5700
+ /** The month's number 1–12 from "2026-01", "2026-01-15", a Date, or 1–12; null when none. */
5701
+ function monthOf(m) {
5702
+ if (m instanceof Date)
5703
+ return Number.isNaN(m.getTime()) ? null : { month: m.getMonth() + 1, year: m.getFullYear() };
5704
+ if (typeof m === "number")
5705
+ return Number.isInteger(m) && m >= 1 && m <= 12 ? { month: m } : null;
5706
+ if (typeof m === "string") {
5707
+ const iso = /^(\d{4})-(\d{1,2})(?:-\d{1,2})?/.exec(m.trim());
5708
+ if (iso) {
5709
+ const month = Number(iso[2]);
5710
+ return month >= 1 && month <= 12 ? { month, year: Number(iso[1]) } : null;
5711
+ }
5712
+ }
5713
+ return null;
5714
+ }
5715
+ /** "2026-01" (or a Date, or 1) → "ม.ค."; "" when it is not a month. */
5716
+ function thaiMonth(m) {
5717
+ const x = monthOf(m);
5718
+ return x ? THAI_MONTHS_SHORT[x.month - 1] : "";
5719
+ }
5720
+ /**
5721
+ * "2026-01" → "ม.ค. 69" (the Buddhist year's last two digits), or with
5722
+ * `{ year: "full" }` "ม.ค. 2569"; just the month when no year is known.
5723
+ */
5724
+ function thaiMonthYear(m, opts = {}) {
5725
+ const x = monthOf(m);
5726
+ if (!x)
5727
+ return "";
5728
+ const name = THAI_MONTHS_SHORT[x.month - 1];
5729
+ if (x.year === undefined)
5730
+ return name;
5731
+ const be = x.year + 543;
5732
+ return `${name} ${opts.year === "full" ? be : String(be).slice(-2)}`;
5733
+ }
5734
+
4804
5735
  exports.API_EVENT = API_EVENT;
4805
5736
  exports.GUIDE_EVENT_PREFIX = GUIDE_EVENT_PREFIX;
4806
5737
  exports.GUIDE_LAYER_ATTRIBUTE = GUIDE_LAYER_ATTRIBUTE;
4807
5738
  exports.GuideErrorBoundary = GuideErrorBoundary;
4808
5739
  exports.GuideProgressConflict = GuideProgressConflict;
4809
5740
  exports.GuideRuntime = GuideRuntime;
5741
+ exports.SectionGuideButton = SectionGuideButton;
5742
+ exports.THAI_MONTHS_SHORT = THAI_MONTHS_SHORT;
4810
5743
  exports.apiCallDetail = apiCallDetail;
4811
5744
  exports.emitGuideEvent = emitGuideEvent;
4812
5745
  exports.fillRoute = fillRoute;
5746
+ exports.formatBaht = formatBaht;
4813
5747
  exports.guideAnchor = guideAnchor;
4814
5748
  exports.guideMenuItems = guideMenuItems;
4815
5749
  exports.guideMenuShown = guideMenuShown;
4816
5750
  exports.loadGuides = loadGuides;
4817
5751
  exports.matchApiPath = matchApiPath;
4818
5752
  exports.matchRoute = matchRoute;
5753
+ exports.openFlow = openFlow;
5754
+ exports.openFlowsPanel = openFlowsPanel;
4819
5755
  exports.openGuidePanel = openGuidePanel;
4820
5756
  exports.openPageGuide = openPageGuide;
4821
5757
  exports.openPagesPanel = openPagesPanel;
5758
+ exports.openSectionGuide = openSectionGuide;
4822
5759
  exports.reportApiCall = reportApiCall;
4823
5760
  exports.reportApiEvent = reportApiEvent;
5761
+ exports.setSectionGuideValues = setSectionGuideValues;
4824
5762
  exports.storeGuides = storeGuides;
5763
+ exports.thaiMonth = thaiMonth;
5764
+ exports.thaiMonthYear = thaiMonthYear;
4825
5765
  exports.useGuideEntry = useGuideEntry;
4826
5766
  exports.usePageGuideEntry = usePageGuideEntry;
4827
5767
  exports.usePageGuides = usePageGuides;
4828
- //# sourceMappingURL=guide-runtime-YS2nUhwS.cjs.map
5768
+ exports.useSectionGuideValues = useSectionGuideValues;
5769
+ //# sourceMappingURL=format-BVoVc7Yk.cjs.map