@eintrek/erp-shell 0.1.72 → 0.1.74

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.
@@ -10,10 +10,10 @@
10
10
  * The main entry re-exports the same modules, so a tour defined against
11
11
  * either entry is the same data, and the bus and the "?" entry are shared.
12
12
  */
13
- export type { GuideAbortAction, GuideAnchorKey, GuideApiFailure, GuideApiMatch, GuideAppId, GuideCaptureEvent, GuideConfig, GuideContext, GuideFacts, GuideGate, GuideMission, GuideNothingToSave, GuideOnMissing, GuideStep, GuideStepKind, GuideSuite, GuideText, GuideTrigger, } from "./types";
13
+ export type { GuideAbortAction, GuideAnchorKey, GuideApiFailure, GuideApiMatch, GuideAppId, GuideCaptureEvent, GuideConfig, GuideContext, GuideFacts, GuideGate, GuideMission, GuideNothingToSave, GuideOnMissing, GuidePage, GuideStep, GuideStepKind, GuideSuite, GuideText, GuideTrigger, } from "./types";
14
14
  export { API_EVENT as GUIDE_API_EVENT, GUIDE_EVENT_PREFIX, GUIDE_LAYER_ATTRIBUTE, apiCallDetail, emitGuideEvent, guideAnchor, reportApiCall, reportApiEvent, type ApiCallReport, type ApiEventDetail, type GuideAnchorProps, } from "./events";
15
- export { openGuidePanel, useGuideEntry, type GuideEntry } from "./guide-store";
15
+ export { guideMenuItems, guideMenuShown, openGuidePanel, openPageGuide, openPagesPanel, useGuideEntry, usePageGuideEntry, usePageGuides, type GuideEntry, type GuideMenuItem, type PageGuidesEntry, } from "./guide-store";
16
16
  export { GuideRuntime, type GuideMenu, type GuideRouting, type GuideRuntimeProps, } from "./guide-runtime";
17
17
  export { GuideProgressConflict, loadGuides, storeGuides, type GuideProgressAdapter, type GuideProgressRequest, type GuideProgressResponse, } from "./progress-sync";
18
- export type { GuideAudience, GuideMode } from "./progress";
18
+ export type { GuideAudience, GuideMode, PagesMode } from "./progress";
19
19
  export { matchApiPath, matchRoute, fillRoute } from "./patterns";
@@ -10,7 +10,48 @@ export interface GuideEntry {
10
10
  }
11
11
  /** Called by the host: the entry to show (null: none), and how to open the panel. */
12
12
  export declare function publishGuideEntry(next: GuideEntry | null, openPanel: (() => void) | null): void;
13
+ /**
14
+ * Page guides here, for the "?" menu: this page's guide (its title) and how
15
+ * many page guides the panel lists ("คู่มือทุกหน้า"). Shell ≥ 0.1.74.
16
+ */
17
+ export interface PageGuidesEntry {
18
+ /** This page's guide for this person, or null. */
19
+ page: {
20
+ title: string;
21
+ } | null;
22
+ /** Page guides with an `href` this person has (the panel's list). */
23
+ listed: number;
24
+ }
25
+ /** Called by the host: this page's guide and the listed count, and how to open them. */
26
+ export declare function publishPageGuides(next: PageGuidesEntry | null, openPage: (() => void) | null, openPages: (() => void) | null): void;
27
+ /** The "?" menu's items, in order (pure: the menu and its tests share it). */
28
+ export type GuideMenuItem = "page" | "missions" | "pages" | "help";
29
+ export declare function guideMenuItems(e: GuideEntry | null, p?: PageGuidesEntry): GuideMenuItem[];
30
+ /**
31
+ * Whether the "?" press opens a menu rather than the help dialog: a tour
32
+ * with missions shown, a guide for this page, or page guides to list. A tour
33
+ * whose missions are all hidden for this user (`visible`, e.g. Plannex
34
+ * without an employee record) has nothing to open, and "0/0" led to an
35
+ * empty panel. The entry itself stays published: apps wait on it before
36
+ * they load the facts that make missions visible.
37
+ */
38
+ export declare function guideMenuShown(e: GuideEntry | null, p?: PageGuidesEntry): boolean;
13
39
  /** Opens the tour's quest panel, when this user has a tour here. */
14
40
  export declare function openGuidePanel(): void;
15
41
  /** The "?" menu's entry, or null when there is no tour for this user. */
16
42
  export declare function useGuideEntry(): GuideEntry | null;
43
+ /** Opens this page's guide ("คู่มือหน้านี้"), when there is one. */
44
+ export declare function openPageGuide(): void;
45
+ /** Opens the panel on the page guides alone ("คู่มือทุกหน้า"). */
46
+ export declare function openPagesPanel(): void;
47
+ /** Page guides here (this page's, and the listed count); stable until they change. */
48
+ export declare function usePageGuides(): PageGuidesEntry;
49
+ /**
50
+ * This page's guide: `{ title, open }`, or null when the page (or the
51
+ * sub-screen, `when`) has none for this person. For an app that draws its
52
+ * own "?" (LIFF's page head): draw it only when this is not null.
53
+ */
54
+ export declare function usePageGuideEntry(): {
55
+ title: string;
56
+ open: () => void;
57
+ } | null;
@@ -10,4 +10,4 @@
10
10
  * before 0.1.68: 16px from the bottom (+ the safe area) on a phone, 22px on
11
11
  * desktop.
12
12
  */
13
- export declare const GUIDE_CSS = "\n[data-erp-guide-layer]{position:fixed;inset:0;z-index:1000;pointer-events:none;font-family:inherit;color:var(--popover-foreground,#1c2634);\n --eg-scrim:rgba(12,20,34,.64);--eg-card:var(--popover,#fff);--eg-fg:var(--popover-foreground,#1c2634);\n --eg-muted:var(--muted-foreground,#6d7886);--eg-soft:var(--muted,#eef3f9);--eg-line:var(--border,#e8ecf1);\n --eg-primary:var(--primary,#3280dd);--eg-on-primary:var(--primary-foreground,#fff);\n --eg-tint:color-mix(in oklab,var(--eg-primary) 13%,var(--eg-card));--eg-strong:color-mix(in oklab,var(--eg-primary) 82%,#000);\n --eg-danger:var(--destructive,#d23b4b);--eg-ok:#1d9a5f;--eg-gold:#e6a417;\n --eg-shadow:0 18px 50px -12px rgba(16,32,64,.35),0 4px 12px rgba(16,32,64,.08);line-height:1.5;font-size:14px}\n[data-erp-guide-layer] *{box-sizing:border-box}\n[data-erp-guide-layer] button{font:inherit;color:inherit;cursor:pointer}\n.eg-i{pointer-events:auto}\n.eg-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\n.eg-scrim-svg{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}\n.eg-scrim-svg .eg-scrim-fill{fill:var(--eg-scrim);pointer-events:none}\n.eg-scrim-svg .eg-scrim-hit{fill:transparent;pointer-events:all}\n.eg-scrim{position:fixed;inset:0;background:var(--eg-scrim);pointer-events:auto}\n.eg-scrim.eg-light{background:rgba(12,20,34,.45)}\n.eg-scrim.eg-faint{background:rgba(12,20,34,.35)}\n.eg-ring{position:fixed;border-radius:12px;pointer-events:none;box-shadow:0 0 0 3px #fff,0 0 0 6px var(--eg-primary)}\n.eg-ring.eg-move{transition:left .26s ease,top .26s ease,width .26s ease,height .26s ease}\n.eg-ring::after{content:\"\";position:absolute;inset:-6px;border-radius:15px;border:3px solid var(--eg-primary);animation:eg-pulse 1.6s ease-out infinite}\n.eg-ring.eg-flash::after{animation:eg-flash .5s ease 1}\n@keyframes eg-pulse{0%{opacity:.9;transform:scale(1)}100%{opacity:0;transform:scale(1.12)}}\n@keyframes eg-flash{0%,100%{opacity:1;transform:none}50%{opacity:.2}}\n.eg-hand{position:fixed;width:46px;height:46px;pointer-events:none;filter:drop-shadow(0 4px 6px rgba(0,0,0,.35));animation:eg-tap 1.1s ease-in-out infinite}\n.eg-hand svg{width:46px;height:46px;fill:#fff;stroke:#1c2634;stroke-width:1.4;stroke-linejoin:round;stroke-linecap:round}\n.eg-hand.eg-flip svg{transform:scaleY(-1)}\n@keyframes eg-tap{0%,100%{transform:translate(6px,8px)}50%{transform:translate(0,0)}}\n.eg-bubble{position:fixed;left:0;top:0;width:320px;background:var(--eg-card);color:var(--eg-fg);border-radius:14px;box-shadow:var(--eg-shadow);padding:14px 16px 12px;border-top:4px solid var(--eg-primary);outline:none;pointer-events:auto}\n.eg-bubble.eg-err{border-top-color:var(--eg-danger)}\n.eg-bubble.eg-nudge{animation:eg-nudge .38s ease}\n@keyframes eg-nudge{0%,100%{translate:0}20%{translate:-7px}40%{translate:6px}60%{translate:-4px}80%{translate:2px}}\n.eg-b-top{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:12px;color:var(--eg-muted)}\n.eg-b-top .eg-mis{font-weight:600;color:var(--eg-strong);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.eg-b-top .eg-cnt{white-space:nowrap;font-weight:600}\n.eg-pips{display:flex;gap:4px;margin:8px 0 10px}\n.eg-pips i{flex:1;height:4px;border-radius:2px;background:var(--eg-soft)}\n.eg-pips i.eg-d{background:var(--eg-primary)}\n.eg-pips i.eg-c{background:color-mix(in oklab,var(--eg-primary) 45%,var(--eg-card))}\n.eg-b-t{margin:0;font-size:16px;font-weight:600;line-height:1.45}\n.eg-b-w{margin:2px 0 0;font-size:13.5px;color:var(--eg-muted)}\n.eg-bubble.eg-err .eg-b-t{color:var(--eg-danger)}\n.eg-k{display:inline-block;background:var(--eg-tint);color:var(--eg-strong);border-radius:7px;padding:0 7px;font-weight:600}\n.eg-b-f{display:flex;align-items:center;justify-content:space-between;margin-top:12px;gap:8px}\n.eg-b-f .eg-btn{height:34px;padding:0 14px;font-size:13.5px}\n.eg-exit{border:0;background:none;color:var(--eg-muted);font-size:12.5px;padding:4px 0;text-decoration:underline;text-underline-offset:3px}\n.eg-exit:hover{color:var(--eg-fg)}\n.eg-arrow{position:absolute;width:14px;height:14px;background:var(--eg-card);transform:rotate(45deg);border-radius:2px}\n.eg-arrow.eg-top{background:var(--eg-primary)}\n.eg-bubble.eg-err .eg-arrow.eg-top{background:var(--eg-danger)}\n.eg-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:40px;padding:0 16px;border-radius:10px;border:1px solid transparent;font-size:14px;font-weight:600;white-space:nowrap}\n.eg-btn:focus-visible,.eg-exit:focus-visible,.eg-chip:focus-visible,.eg-x:focus-visible{outline:2px solid var(--eg-primary);outline-offset:2px}\n.eg-primary{background:var(--eg-primary);color:var(--eg-on-primary)!important}\n.eg-primary:hover{background:var(--eg-strong)}\n.eg-primary[disabled]{opacity:.55;cursor:default}\n.eg-ghost{background:var(--eg-card);border-color:var(--eg-line)}\n.eg-ghost:hover{background:var(--eg-soft)}\n.eg-ghost[disabled]{opacity:.55;cursor:default}\n.eg-link{background:none;border:0;color:var(--eg-muted);font-weight:500;padding:0 6px;height:32px}\n.eg-link:hover{color:var(--eg-fg);text-decoration:underline}\n.eg-modal{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:min(440px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;background:var(--eg-card);color:var(--eg-fg);border-radius:20px;box-shadow:var(--eg-shadow);padding:28px 28px 22px;text-align:center;outline:none;pointer-events:auto}\n.eg-modal.eg-pop{animation:eg-pop .32s cubic-bezier(.2,.9,.3,1.3)}\n@keyframes eg-pop{from{opacity:0;transform:translate(-50%,-46%) scale(.94)}}\n.eg-modal h2{margin:6px 0 4px;font-size:22px;font-weight:700;letter-spacing:-.2px;line-height:1.3}\n.eg-modal .eg-sub{margin:0;color:var(--eg-muted);font-size:14.5px}\n.eg-eyebrow{margin:0;font-size:12.5px;font-weight:600;letter-spacing:.3px;color:var(--eg-strong)}\n.eg-actions{display:grid;gap:6px;margin-top:20px}\n.eg-actions .eg-btn{height:44px}\n.eg-badge{width:64px;height:64px;border-radius:18px;margin:0 auto 12px;display:grid;place-items:center;background:var(--eg-tint);color:var(--eg-strong)}\n.eg-badge svg.eg-ic{width:30px;height:30px}\n.eg-quests{list-style:none;padding:0;margin:18px 0 0;text-align:left;display:grid;gap:6px}\n.eg-quests li{display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:11px;background:var(--eg-soft);font-size:14px}\n.eg-quests li .eg-n{width:26px;height:26px;border-radius:50%;background:var(--eg-card);border:1.5px solid var(--eg-line);display:grid;place-items:center;font-size:12px;font-weight:700;color:var(--eg-muted);flex:none}\n.eg-quests li.eg-first{background:var(--eg-tint)}\n.eg-quests li.eg-first .eg-n{border-color:var(--eg-primary);color:var(--eg-strong)}\n.eg-quests li.eg-ok .eg-n{background:var(--eg-ok);border-color:var(--eg-ok);color:#fff}\n.eg-quests li .eg-n svg.eg-ic{width:15px;height:15px;stroke-width:3}\n.eg-quests li .eg-t{flex:1}\n.eg-quests li small{color:var(--eg-muted);font-size:12px;white-space:nowrap}\n.eg-modal.eg-confirm{text-align:left;width:min(380px,calc(100vw - 32px));padding:22px}\n.eg-modal.eg-confirm h2{font-size:18px;margin-top:0}\n.eg-modal.eg-confirm .eg-actions{grid-template-columns:1fr 1fr}\n.eg-medal{width:84px;height:84px;border-radius:50%;margin:4px auto 10px;display:grid;place-items:center;color:#fff;background:radial-gradient(circle at 35% 30%,#ffd666,var(--eg-gold));box-shadow:0 0 0 8px #fff4d6;animation:eg-stamp .5s cubic-bezier(.2,.9,.3,1.4) .1s both}\n.eg-medal svg.eg-ic{width:42px;height:42px;stroke-width:2.6}\n@keyframes eg-stamp{from{transform:scale(.3) rotate(-25deg);opacity:0}}\n.eg-prog{display:flex;align-items:center;gap:10px;margin-top:16px;font-size:13px;color:var(--eg-muted);font-weight:600}\n.eg-prog .eg-bar{flex:1;height:8px;border-radius:4px;background:var(--eg-soft);overflow:hidden}\n.eg-prog .eg-bar i{display:block;height:100%;background:var(--eg-primary);border-radius:4px;transition:width .9s cubic-bezier(.2,.8,.2,1) .35s}\n.eg-burst{position:absolute;left:50%;top:70px;width:0;height:0;pointer-events:none}\n.eg-burst span{position:absolute;width:8px;height:12px;border-radius:2px;opacity:0;animation:eg-conf 1.1s cubic-bezier(.15,.7,.3,1) forwards}\n@keyframes eg-conf{0%{opacity:1;transform:translate(0,0) rotate(0)}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}\n.eg-card{position:fixed;left:50%;bottom:calc(var(--erp-guide-bottom-offset,16px) + 12px);transform:translateX(-50%);width:min(360px,calc(100vw - 32px));background:var(--eg-card);color:var(--eg-fg);border-radius:14px;box-shadow:var(--eg-shadow);padding:14px 16px;border-top:4px solid var(--eg-primary);pointer-events:auto;outline:none}\n.eg-card .eg-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px}\n.eg-loading{display:flex;align-items:center;gap:10px;font-weight:600}\n.eg-spin{width:18px;height:18px;border-radius:50%;border:2.5px solid var(--eg-soft);border-top-color:var(--eg-primary);animation:eg-spin .8s linear infinite;flex:none}\n@keyframes eg-spin{to{transform:rotate(360deg)}}\n.eg-chip{position:fixed;right:22px;bottom:var(--erp-guide-bottom-offset,22px);display:flex;align-items:center;gap:10px;height:48px;padding:0 18px 0 8px;border-radius:24px;border:1px solid var(--eg-line);background:var(--eg-card);color:var(--eg-fg);box-shadow:0 8px 24px -8px rgba(16,32,64,.3);font-weight:600;font-size:14px;pointer-events:auto}\n.eg-chip svg{width:34px;height:34px}\n.eg-chip.eg-hi{animation:eg-hi 1.2s ease 2}\n@keyframes eg-hi{50%{box-shadow:0 0 0 6px color-mix(in oklab,var(--eg-primary) 35%,transparent),0 8px 24px -8px rgba(16,32,64,.3)}}\n.eg-panel{position:fixed;right:22px;bottom:calc(var(--erp-guide-bottom-offset,22px) + 60px);width:360px;max-height:calc(100vh - 110px);overflow:auto;background:var(--eg-card);color:var(--eg-fg);border:1px solid var(--eg-line);border-radius:16px;box-shadow:var(--eg-shadow);outline:none;pointer-events:auto}\n.eg-panel-bd{display:none}\n.eg-q-h{display:flex;align-items:center;gap:10px;padding:16px 12px 4px 18px}\n.eg-q-h h2{margin:0;font-size:16px;font-weight:700;flex:1}\n.eg-q-h .eg-n{font-size:13px;color:var(--eg-muted);font-weight:600}\n.eg-x{width:34px;height:34px;border-radius:10px;border:0;background:none;display:grid;place-items:center;color:var(--eg-muted)}\n.eg-x:hover{background:var(--eg-soft)}\n.eg-x svg.eg-ic{width:18px;height:18px}\n.eg-panel .eg-prog{margin:6px 18px 8px}\n.eg-q-list{list-style:none;margin:0;padding:4px 10px 6px;display:grid;gap:2px}\n.eg-q-item{display:flex;align-items:center;gap:12px;padding:10px 8px;border-radius:11px}\n.eg-q-item.eg-next{background:var(--eg-tint)}\n.eg-q-item .eg-n{width:30px;height:30px;border-radius:50%;border:1.5px solid var(--eg-line);display:grid;place-items:center;font-size:12.5px;font-weight:700;color:var(--eg-muted);flex:none;background:var(--eg-card)}\n.eg-q-item.eg-next .eg-n{border-color:var(--eg-primary);color:var(--eg-strong)}\n.eg-q-item.eg-done .eg-n{background:var(--eg-ok);border-color:var(--eg-ok);color:#fff}\n.eg-q-item.eg-done .eg-n svg.eg-ic{width:16px;height:16px;stroke-width:3}\n.eg-q-item.eg-dim .eg-n{opacity:.55}\n.eg-q-item.eg-just .eg-n{animation:eg-stamp .55s cubic-bezier(.2,.9,.3,1.4) .2s both}\n.eg-q-item .eg-t{flex:1;min-width:0}\n.eg-q-item .eg-t b{display:block;font-size:14px;font-weight:600}\n.eg-q-item.eg-done .eg-t b{color:var(--eg-muted)}\n.eg-q-item .eg-t small{display:block;font-size:12px;color:var(--eg-muted)}\n.eg-q-item .eg-t small.eg-sk{color:#b06a00}\n.eg-q-item .eg-t small.eg-lock{color:var(--eg-fg);font-weight:500}\n.eg-q-item .eg-btn{height:32px;padding:0 12px;font-size:13px}\n.eg-q-acts{display:flex;flex-direction:column;align-items:flex-end;gap:2px}\n.eg-q-acts .eg-link{height:22px;font-size:12px;padding:0 2px}\n.eg-q-f{border-top:1px solid var(--eg-line);padding:8px 12px;display:flex;justify-content:flex-end}\n.eg-toast{position:fixed;left:50%;bottom:calc(var(--erp-guide-bottom-offset,22px) + 62px);transform:translateX(-50%);background:#1c2634;color:#fff;padding:10px 16px;border-radius:10px;font-size:13.5px;box-shadow:var(--eg-shadow);max-width:calc(100vw - 32px);pointer-events:none}\nsvg.eg-ic{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:none}\n@media (max-width:767px){\n .eg-bubble{width:auto}\n .eg-arrow{display:none}\n .eg-modal{top:auto;bottom:12px;transform:translateX(-50%);width:calc(100vw - 24px);padding:24px 20px 18px}\n .eg-modal.eg-pop{animation:eg-up .3s cubic-bezier(.2,.9,.3,1.1)}\n .eg-modal .eg-actions .eg-btn{height:46px}\n .eg-chip{right:16px;bottom:var(--erp-guide-bottom-offset,calc(16px + env(safe-area-inset-bottom)))}\n .eg-panel-bd{display:block;position:fixed;inset:0;background:rgba(15,23,42,.38);pointer-events:auto}\n .eg-panel{left:0;right:0;bottom:0;width:100%;max-height:85vh;border-radius:18px 18px 0 0;padding-bottom:env(safe-area-inset-bottom)}\n .eg-q-item{padding:12px 8px}\n .eg-card{bottom:calc(var(--erp-guide-bottom-offset,calc(16px + env(safe-area-inset-bottom))) - 4px)}\n .eg-toast{bottom:calc(var(--erp-guide-bottom-offset,calc(16px + env(safe-area-inset-bottom))) + 60px)}\n}\n@keyframes eg-up{from{opacity:0;transform:translate(-50%,30px)}}\n@media (prefers-reduced-motion:reduce){\n [data-erp-guide-layer] *,[data-erp-guide-layer] *::before,[data-erp-guide-layer] *::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}\n .eg-ring::after{animation:none!important;opacity:1;transform:none}\n .eg-ring.eg-flash::after{animation:eg-flash .5s ease 1!important;animation-duration:.5s!important}\n .eg-hand{animation:none!important;transform:translate(4px,6px)}\n .eg-burst{display:none}\n .eg-bubble.eg-nudge{animation:none!important;border-top-color:var(--eg-danger)}\n}\n";
13
+ export declare const GUIDE_CSS = "\n[data-erp-guide-layer]{position:fixed;inset:0;z-index:1000;pointer-events:none;font-family:inherit;color:var(--popover-foreground,#1c2634);\n --eg-scrim:rgba(12,20,34,.64);--eg-card:var(--popover,#fff);--eg-fg:var(--popover-foreground,#1c2634);\n --eg-muted:var(--muted-foreground,#6d7886);--eg-soft:var(--muted,#eef3f9);--eg-line:var(--border,#e8ecf1);\n --eg-primary:var(--primary,#3280dd);--eg-on-primary:var(--primary-foreground,#fff);\n --eg-tint:color-mix(in oklab,var(--eg-primary) 13%,var(--eg-card));--eg-strong:color-mix(in oklab,var(--eg-primary) 82%,#000);\n --eg-danger:var(--destructive,#d23b4b);--eg-ok:#1d9a5f;--eg-gold:#e6a417;\n --eg-shadow:0 18px 50px -12px rgba(16,32,64,.35),0 4px 12px rgba(16,32,64,.08);line-height:1.5;font-size:14px}\n[data-erp-guide-layer] *{box-sizing:border-box}\n[data-erp-guide-layer] button{font:inherit;color:inherit;cursor:pointer}\n.eg-i{pointer-events:auto}\n.eg-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\n.eg-scrim-svg{position:fixed;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible}\n.eg-scrim-svg .eg-scrim-fill{fill:var(--eg-scrim);pointer-events:none}\n.eg-scrim-svg .eg-scrim-hit{fill:transparent;pointer-events:all}\n.eg-scrim{position:fixed;inset:0;background:var(--eg-scrim);pointer-events:auto}\n.eg-scrim.eg-light{background:rgba(12,20,34,.45)}\n.eg-scrim.eg-faint{background:rgba(12,20,34,.35)}\n.eg-ring{position:fixed;border-radius:12px;pointer-events:none;box-shadow:0 0 0 3px #fff,0 0 0 6px var(--eg-primary)}\n.eg-ring.eg-move{transition:left .26s ease,top .26s ease,width .26s ease,height .26s ease}\n.eg-ring::after{content:\"\";position:absolute;inset:-6px;border-radius:15px;border:3px solid var(--eg-primary);animation:eg-pulse 1.6s ease-out infinite}\n.eg-ring.eg-flash::after{animation:eg-flash .5s ease 1}\n@keyframes eg-pulse{0%{opacity:.9;transform:scale(1)}100%{opacity:0;transform:scale(1.12)}}\n@keyframes eg-flash{0%,100%{opacity:1;transform:none}50%{opacity:.2}}\n.eg-hand{position:fixed;width:46px;height:46px;pointer-events:none;filter:drop-shadow(0 4px 6px rgba(0,0,0,.35));animation:eg-tap 1.1s ease-in-out infinite}\n.eg-hand svg{width:46px;height:46px;fill:#fff;stroke:#1c2634;stroke-width:1.4;stroke-linejoin:round;stroke-linecap:round}\n.eg-hand.eg-flip svg{transform:scaleY(-1)}\n@keyframes eg-tap{0%,100%{transform:translate(6px,8px)}50%{transform:translate(0,0)}}\n.eg-bubble{position:fixed;left:0;top:0;width:320px;background:var(--eg-card);color:var(--eg-fg);border-radius:14px;box-shadow:var(--eg-shadow);padding:14px 16px 12px;border-top:4px solid var(--eg-primary);outline:none;pointer-events:auto}\n.eg-bubble.eg-err{border-top-color:var(--eg-danger)}\n.eg-bubble.eg-nudge{animation:eg-nudge .38s ease}\n@keyframes eg-nudge{0%,100%{translate:0}20%{translate:-7px}40%{translate:6px}60%{translate:-4px}80%{translate:2px}}\n.eg-b-top{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:12px;color:var(--eg-muted)}\n.eg-b-top .eg-mis{font-weight:600;color:var(--eg-strong);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.eg-b-top .eg-cnt{white-space:nowrap;font-weight:600}\n.eg-pips{display:flex;gap:4px;margin:8px 0 10px}\n.eg-pips i{flex:1;height:4px;border-radius:2px;background:var(--eg-soft)}\n.eg-pips i.eg-d{background:var(--eg-primary)}\n.eg-pips i.eg-c{background:color-mix(in oklab,var(--eg-primary) 45%,var(--eg-card))}\n.eg-b-t{margin:0;font-size:16px;font-weight:600;line-height:1.45}\n.eg-b-w{margin:2px 0 0;font-size:13.5px;color:var(--eg-muted)}\n.eg-bubble.eg-err .eg-b-t{color:var(--eg-danger)}\n.eg-b-s{margin:6px 0 0;font-size:13px;font-weight:600;color:var(--eg-danger)}\n.eg-k{display:inline-block;background:var(--eg-tint);color:var(--eg-strong);border-radius:7px;padding:0 7px;font-weight:600}\n.eg-b-f{display:flex;align-items:center;justify-content:space-between;margin-top:12px;gap:8px}\n.eg-b-f .eg-btn{height:34px;padding:0 14px;font-size:13.5px}\n.eg-exit{border:0;background:none;color:var(--eg-muted);font-size:12.5px;padding:4px 0;text-decoration:underline;text-underline-offset:3px}\n.eg-exit:hover{color:var(--eg-fg)}\n.eg-arrow{position:absolute;width:14px;height:14px;background:var(--eg-card);transform:rotate(45deg);border-radius:2px}\n.eg-arrow.eg-top{background:var(--eg-primary)}\n.eg-bubble.eg-err .eg-arrow.eg-top{background:var(--eg-danger)}\n.eg-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:40px;padding:0 16px;border-radius:10px;border:1px solid transparent;font-size:14px;font-weight:600;white-space:nowrap}\n.eg-btn:focus-visible,.eg-exit:focus-visible,.eg-chip:focus-visible,.eg-x:focus-visible{outline:2px solid var(--eg-primary);outline-offset:2px}\n.eg-primary{background:var(--eg-primary);color:var(--eg-on-primary)!important}\n.eg-primary:hover{background:var(--eg-strong)}\n.eg-primary[disabled]{opacity:.55;cursor:default}\n.eg-ghost{background:var(--eg-card);border-color:var(--eg-line)}\n.eg-ghost:hover{background:var(--eg-soft)}\n.eg-ghost[disabled]{opacity:.55;cursor:default}\n.eg-link{background:none;border:0;color:var(--eg-muted);font-weight:500;padding:0 6px;height:32px}\n.eg-link:hover{color:var(--eg-fg);text-decoration:underline}\n.eg-modal{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:min(440px,calc(100vw - 32px));max-height:calc(100vh - 32px);overflow:auto;background:var(--eg-card);color:var(--eg-fg);border-radius:20px;box-shadow:var(--eg-shadow);padding:28px 28px 22px;text-align:center;outline:none;pointer-events:auto}\n.eg-modal.eg-pop{animation:eg-pop .32s cubic-bezier(.2,.9,.3,1.3)}\n@keyframes eg-pop{from{opacity:0;transform:translate(-50%,-46%) scale(.94)}}\n.eg-modal h2{margin:6px 0 4px;font-size:22px;font-weight:700;letter-spacing:-.2px;line-height:1.3}\n.eg-modal .eg-sub{margin:0;color:var(--eg-muted);font-size:14.5px}\n.eg-eyebrow{margin:0;font-size:12.5px;font-weight:600;letter-spacing:.3px;color:var(--eg-strong)}\n.eg-actions{display:grid;gap:6px;margin-top:20px}\n.eg-actions .eg-btn{height:44px}\n.eg-badge{width:64px;height:64px;border-radius:18px;margin:0 auto 12px;display:grid;place-items:center;background:var(--eg-tint);color:var(--eg-strong)}\n.eg-badge svg.eg-ic{width:30px;height:30px}\n.eg-quests{list-style:none;padding:0;margin:18px 0 0;text-align:left;display:grid;gap:6px}\n.eg-quests li{display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:11px;background:var(--eg-soft);font-size:14px}\n.eg-quests li .eg-n{width:26px;height:26px;border-radius:50%;background:var(--eg-card);border:1.5px solid var(--eg-line);display:grid;place-items:center;font-size:12px;font-weight:700;color:var(--eg-muted);flex:none}\n.eg-quests li.eg-first{background:var(--eg-tint)}\n.eg-quests li.eg-first .eg-n{border-color:var(--eg-primary);color:var(--eg-strong)}\n.eg-quests li.eg-ok .eg-n{background:var(--eg-ok);border-color:var(--eg-ok);color:#fff}\n.eg-quests li .eg-n svg.eg-ic{width:15px;height:15px;stroke-width:3}\n.eg-quests li .eg-t{flex:1}\n.eg-quests li small{color:var(--eg-muted);font-size:12px;white-space:nowrap}\n.eg-modal.eg-confirm{text-align:left;width:min(380px,calc(100vw - 32px));padding:22px}\n.eg-modal.eg-confirm h2{font-size:18px;margin-top:0}\n.eg-modal.eg-confirm .eg-actions{grid-template-columns:1fr 1fr}\n.eg-medal{width:84px;height:84px;border-radius:50%;margin:4px auto 10px;display:grid;place-items:center;color:#fff;background:radial-gradient(circle at 35% 30%,#ffd666,var(--eg-gold));box-shadow:0 0 0 8px #fff4d6;animation:eg-stamp .5s cubic-bezier(.2,.9,.3,1.4) .1s both}\n.eg-medal svg.eg-ic{width:42px;height:42px;stroke-width:2.6}\n@keyframes eg-stamp{from{transform:scale(.3) rotate(-25deg);opacity:0}}\n.eg-prog{display:flex;align-items:center;gap:10px;margin-top:16px;font-size:13px;color:var(--eg-muted);font-weight:600}\n.eg-prog .eg-bar{flex:1;height:8px;border-radius:4px;background:var(--eg-soft);overflow:hidden}\n.eg-prog .eg-bar i{display:block;height:100%;background:var(--eg-primary);border-radius:4px;transition:width .9s cubic-bezier(.2,.8,.2,1) .35s}\n.eg-burst{position:absolute;left:50%;top:70px;width:0;height:0;pointer-events:none}\n.eg-burst span{position:absolute;width:8px;height:12px;border-radius:2px;opacity:0;animation:eg-conf 1.1s cubic-bezier(.15,.7,.3,1) forwards}\n@keyframes eg-conf{0%{opacity:1;transform:translate(0,0) rotate(0)}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}\n.eg-card{position:fixed;left:50%;bottom:calc(var(--erp-guide-bottom-offset,16px) + 12px);transform:translateX(-50%);width:min(360px,calc(100vw - 32px));background:var(--eg-card);color:var(--eg-fg);border-radius:14px;box-shadow:var(--eg-shadow);padding:14px 16px;border-top:4px solid var(--eg-primary);pointer-events:auto;outline:none}\n.eg-card .eg-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px}\n.eg-loading{display:flex;align-items:center;gap:10px;font-weight:600}\n.eg-spin{width:18px;height:18px;border-radius:50%;border:2.5px solid var(--eg-soft);border-top-color:var(--eg-primary);animation:eg-spin .8s linear infinite;flex:none}\n@keyframes eg-spin{to{transform:rotate(360deg)}}\n.eg-chip{position:fixed;right:22px;bottom:var(--erp-guide-bottom-offset,22px);display:flex;align-items:center;gap:10px;height:48px;padding:0 18px 0 8px;border-radius:24px;border:1px solid var(--eg-line);background:var(--eg-card);color:var(--eg-fg);box-shadow:0 8px 24px -8px rgba(16,32,64,.3);font-weight:600;font-size:14px;pointer-events:auto}\n.eg-chip svg{width:34px;height:34px}\n.eg-chip.eg-hi{animation:eg-hi 1.2s ease 2}\n@keyframes eg-hi{50%{box-shadow:0 0 0 6px color-mix(in oklab,var(--eg-primary) 35%,transparent),0 8px 24px -8px rgba(16,32,64,.3)}}\n.eg-panel{position:fixed;right:22px;bottom:calc(var(--erp-guide-bottom-offset,22px) + 60px);width:360px;max-height:calc(100vh - 110px);overflow:auto;background:var(--eg-card);color:var(--eg-fg);border:1px solid var(--eg-line);border-radius:16px;box-shadow:var(--eg-shadow);outline:none;pointer-events:auto}\n.eg-panel-bd{display:none}\n.eg-q-h{display:flex;align-items:center;gap:10px;padding:16px 12px 4px 18px}\n.eg-q-h h2{margin:0;font-size:16px;font-weight:700;flex:1}\n.eg-q-h .eg-n{font-size:13px;color:var(--eg-muted);font-weight:600}\n.eg-x{width:34px;height:34px;border-radius:10px;border:0;background:none;display:grid;place-items:center;color:var(--eg-muted)}\n.eg-x:hover{background:var(--eg-soft)}\n.eg-x svg.eg-ic{width:18px;height:18px}\n.eg-panel .eg-prog{margin:6px 18px 8px}\n.eg-q-list{list-style:none;margin:0;padding:4px 10px 6px;display:grid;gap:2px}\n.eg-q-item{display:flex;align-items:center;gap:12px;padding:10px 8px;border-radius:11px}\n.eg-q-item.eg-next{background:var(--eg-tint)}\n.eg-q-item .eg-n{width:30px;height:30px;border-radius:50%;border:1.5px solid var(--eg-line);display:grid;place-items:center;font-size:12.5px;font-weight:700;color:var(--eg-muted);flex:none;background:var(--eg-card)}\n.eg-q-item.eg-next .eg-n{border-color:var(--eg-primary);color:var(--eg-strong)}\n.eg-q-item.eg-done .eg-n{background:var(--eg-ok);border-color:var(--eg-ok);color:#fff}\n.eg-q-item.eg-done .eg-n svg.eg-ic{width:16px;height:16px;stroke-width:3}\n.eg-q-item.eg-dim .eg-n{opacity:.55}\n.eg-q-item.eg-just .eg-n{animation:eg-stamp .55s cubic-bezier(.2,.9,.3,1.4) .2s both}\n.eg-q-item .eg-t{flex:1;min-width:0}\n.eg-q-item .eg-t b{display:block;font-size:14px;font-weight:600}\n.eg-q-item.eg-done .eg-t b{color:var(--eg-muted)}\n.eg-q-item .eg-t small{display:block;font-size:12px;color:var(--eg-muted)}\n.eg-q-item .eg-t small.eg-sk{color:#b06a00}\n.eg-q-item .eg-t small.eg-lock{color:var(--eg-fg);font-weight:500}\n.eg-q-item .eg-btn{height:32px;padding:0 12px;font-size:13px}\n.eg-q-acts{display:flex;flex-direction:column;align-items:flex-end;gap:2px}\n.eg-q-acts .eg-link{height:22px;font-size:12px;padding:0 2px}\n.eg-q-f{border-top:1px solid var(--eg-line);padding:8px 12px;display:flex;justify-content:flex-end}\n.eg-q-sub{margin:6px 0 0;padding:12px 18px 2px;border-top:1px solid var(--eg-line);font-size:13px;font-weight:700;color:var(--eg-muted)}\n.eg-pg-item{padding:8px}\n.eg-pg-item .eg-t small.eg-new{color:var(--eg-strong);font-weight:600}\n.eg-card.eg-offer{left:auto;right:22px;transform:none;bottom:var(--erp-guide-bottom-offset,22px);width:min(320px,calc(100vw - 32px))}\n.eg-offer .eg-po-e{margin:0 0 2px;font-size:12px;font-weight:600;color:var(--eg-strong)}\n.eg-offer .eg-row .eg-btn{height:34px;padding:0 14px;font-size:13.5px}\n.eg-toast{position:fixed;left:50%;bottom:calc(var(--erp-guide-bottom-offset,22px) + 62px);transform:translateX(-50%);background:#1c2634;color:#fff;padding:10px 16px;border-radius:10px;font-size:13.5px;box-shadow:var(--eg-shadow);max-width:calc(100vw - 32px);pointer-events:none}\nsvg.eg-ic{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;flex:none}\n@media (max-width:767px){\n .eg-bubble{width:auto}\n .eg-arrow{display:none}\n .eg-modal{top:auto;bottom:12px;transform:translateX(-50%);width:calc(100vw - 24px);padding:24px 20px 18px}\n .eg-modal.eg-pop{animation:eg-up .3s cubic-bezier(.2,.9,.3,1.1)}\n .eg-modal .eg-actions .eg-btn{height:46px}\n .eg-chip{right:16px;bottom:var(--erp-guide-bottom-offset,calc(16px + env(safe-area-inset-bottom)))}\n .eg-panel-bd{display:block;position:fixed;inset:0;background:rgba(15,23,42,.38);pointer-events:auto}\n .eg-panel{left:0;right:0;bottom:0;width:100%;max-height:85vh;border-radius:18px 18px 0 0;padding-bottom:env(safe-area-inset-bottom)}\n .eg-q-item{padding:12px 8px}\n .eg-card{bottom:calc(var(--erp-guide-bottom-offset,calc(16px + env(safe-area-inset-bottom))) - 4px)}\n .eg-card.eg-offer{left:16px;right:16px;width:auto;bottom:calc(var(--erp-guide-bottom-offset,calc(16px + env(safe-area-inset-bottom))) - 4px)}\n .eg-toast{bottom:calc(var(--erp-guide-bottom-offset,calc(16px + env(safe-area-inset-bottom))) + 60px)}\n}\n@keyframes eg-up{from{opacity:0;transform:translate(-50%,30px)}}\n@media (prefers-reduced-motion:reduce){\n [data-erp-guide-layer] *,[data-erp-guide-layer] *::before,[data-erp-guide-layer] *::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}\n .eg-ring::after{animation:none!important;opacity:1;transform:none}\n .eg-ring.eg-flash::after{animation:eg-flash .5s ease 1!important;animation-duration:.5s!important}\n .eg-hand{animation:none!important;transform:translate(4px,6px)}\n .eg-burst{display:none}\n .eg-bubble.eg-nudge{animation:none!important;border-top-color:var(--eg-danger)}\n}\n";
@@ -56,8 +56,21 @@ export interface BubbleModel {
56
56
  title: string;
57
57
  why: string;
58
58
  error: StepError | null;
59
+ /** The step's status line (GuideStep.status), shown under the instruction. */
60
+ status?: string | null;
59
61
  showNext: boolean;
60
62
  showEndWithoutSave: boolean;
63
+ /**
64
+ * The bubble's top line in place of "ภารกิจ n · title": a page guide's
65
+ * "คู่มือหน้านี้ · <title>". Shell ≥ 0.1.74.
66
+ */
67
+ heading?: string;
68
+ /** The way out, in place of "จบการทบทวน" / "ออกจากไกด์" (a page guide: "ปิดคู่มือ"). */
69
+ exitLabel?: string;
70
+ /** ย้อนกลับ: a page guide's read step with a step before it. */
71
+ showBack?: boolean;
72
+ /** ถัดไป's label in place of "ถัดไป" (a page guide's last step: "เสร็จ"). */
73
+ nextLabel?: string;
61
74
  }
62
75
  export interface StepLayerProps {
63
76
  /** The spotlight (padded target), or null while there is none to show. */
@@ -99,6 +112,8 @@ export interface StepLayerProps {
99
112
  moving: boolean;
100
113
  onExit: () => void;
101
114
  onNext: () => void;
115
+ /** ย้อนกลับ (BubbleModel.showBack). */
116
+ onBack?: () => void;
102
117
  onEndWithoutSave: () => void;
103
118
  onRetry: () => void;
104
119
  onGoResume: () => void;
@@ -168,6 +183,23 @@ export declare function CompleteModal(p: {
168
183
  reduced: boolean;
169
184
  onFinish: () => void;
170
185
  }): import("react/jsx-runtime").JSX.Element;
186
+ /**
187
+ * The offer of this page's guide on a first visit: a small card in the
188
+ * corner, not modal (the page stays usable; focus is not taken). Shell ≥ 0.1.74.
189
+ */
190
+ export declare function PageOfferCard(p: {
191
+ title: string;
192
+ steps: number;
193
+ onStart: () => void;
194
+ onLater: () => void;
195
+ }): import("react/jsx-runtime").JSX.Element;
196
+ /** One page guide in the panel's "คู่มือแต่ละหน้า" list. */
197
+ export interface PageItem {
198
+ key: string;
199
+ title: string;
200
+ /** This person has ended this version (ดูแล้ว); otherwise ใหม่. */
201
+ seen: boolean;
202
+ }
171
203
  export declare function QuestChip({ done, total, pulse, onOpen }: {
172
204
  done: number;
173
205
  total: number;
@@ -199,6 +231,14 @@ export declare function QuestPanel(p: {
199
231
  onStart: (key: string, replay: boolean) => void;
200
232
  onDismiss: (key: string) => void;
201
233
  onHideChip: () => void;
234
+ /**
235
+ * The second section, "คู่มือแต่ละหน้า": the page guides this person has
236
+ * (shell ≥ 0.1.74). Omitted or empty: no section.
237
+ */
238
+ pages?: PageItem[];
239
+ /** false: the page guides alone (no missions for this person, or "คู่มือทุกหน้า"). */
240
+ showMissions?: boolean;
241
+ onOpenPage?: (key: string) => void;
202
242
  }): import("react/jsx-runtime").JSX.Element;
203
243
  export declare function GuideToast({ text }: {
204
244
  text: string;
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import type { GuideAbortAction, GuideContext, GuideMission, GuideStep, GuideSuite, GuideText, GuideTrigger } from "./types";
10
10
  import { type RouteParams } from "./patterns";
11
- import type { GuideAudience, GuideProgress, MissionEntry } from "./progress";
11
+ import type { GuideAudience, GuideProgress, MissionEntry, PageRow, PagesMode } from "./progress";
12
+ import { currentPage } from "./pages";
12
13
  export type StepPhase =
13
14
  /** Looking for the target (dim page, "กำลังโหลด…"). */
14
15
  "waiting"
@@ -47,6 +48,21 @@ export type GuideScreen = {
47
48
  /** A commit step shown in its read form: nothing to save
48
49
  * (GuideStep.nothingToSave). */
49
50
  nothingToSave: boolean;
51
+ /** The step's status line now (GuideStep.status), or null. */
52
+ status: string | null;
53
+ /**
54
+ * A replay's "select" step offers ถัดไป: what comes next is already on
55
+ * the page (the save button of a form that holds a value), so a review
56
+ * need not change a real record to go on. Shell ≥ 0.1.73.
57
+ */
58
+ replayNext: boolean;
59
+ /** A page guide's step: the page key (the mission is `page:<key>`). Shell ≥ 0.1.74. */
60
+ page?: string;
61
+ }
62
+ /** The first-visit offer of this page's guide: a small card, not modal. */
63
+ | {
64
+ name: "pageOffer";
65
+ page: string;
50
66
  } | {
51
67
  name: "celebrate";
52
68
  mission: string;
@@ -76,6 +92,29 @@ export interface GuideState {
76
92
  * the next idle moment (never mid-step). Not saved: a reload drops it.
77
93
  */
78
94
  pendingCelebrate: string[];
95
+ /** Who gets page guides ("optional" for every member while they are on). */
96
+ pageAudience: GuideAudience;
97
+ /** How page guides are offered: off, from "?" only, or also on a first visit. */
98
+ pagesMode: PagesMode;
99
+ /** Page rows by page key, as the server holds them (or as just written). */
100
+ pages: Record<string, PageRow>;
101
+ /**
102
+ * The page guide being played: `write` when its row is still to be
103
+ * written (the offer was taken, or this person has never ended this
104
+ * version); a replay from "?" of a recorded page writes nothing.
105
+ */
106
+ pageRun: {
107
+ key: string;
108
+ write: boolean;
109
+ } | null;
110
+ /** What the panel shows: everything, or the page guides alone ("คู่มือทุกหน้า"). */
111
+ panelSection: "all" | "pages";
112
+ }
113
+ /** The page-guide part of the state, from the progress answer. */
114
+ export interface GuidePagesInit {
115
+ pageAudience: GuideAudience;
116
+ pagesMode: PagesMode;
117
+ pages: Record<string, PageRow>;
79
118
  }
80
119
  export interface ReplayPage {
81
120
  mission: string;
@@ -182,6 +221,7 @@ export type GuideAction = {
182
221
  type: "dismissComplete";
183
222
  } | {
184
223
  type: "openPanel";
224
+ section?: "all" | "pages";
185
225
  } | {
186
226
  type: "closePanel";
187
227
  } | {
@@ -201,6 +241,34 @@ export type GuideAction = {
201
241
  /** The host is idle: show a waiting `completeOn` celebration. */
202
242
  | {
203
243
  type: "idleCelebrate";
244
+ }
245
+ /** The host found this page's guide due (pageOfferDue): show the offer card. */
246
+ | {
247
+ type: "pageOffer";
248
+ page: string;
249
+ }
250
+ /** The offer's [เริ่ม]. */
251
+ | {
252
+ type: "pageOfferStart";
253
+ }
254
+ /** The offer's [ไม่ตอนนี้]: recorded, never offered again (this version). */
255
+ | {
256
+ type: "pageOfferLater";
257
+ }
258
+ /** "คู่มือหน้านี้" from "?", or a page in the panel's list (once on its page). */
259
+ | {
260
+ type: "startPage";
261
+ page: string;
262
+ }
263
+ /** ย้อนกลับ on a page guide's read step. */
264
+ | {
265
+ type: "back";
266
+ }
267
+ /** A page row came back from the server. */
268
+ | {
269
+ type: "pageRow";
270
+ key: string;
271
+ row: PageRow;
204
272
  };
205
273
  export type GuideEffect = {
206
274
  type: "save";
@@ -222,6 +290,13 @@ export type GuideEffect = {
222
290
  | {
223
291
  type: "abort";
224
292
  actions: GuideAbortAction[];
293
+ }
294
+ /** Write a page guide's row, once: `<app>.page.<key>`. */
295
+ | {
296
+ type: "savePage";
297
+ key: string;
298
+ version: number;
299
+ status: "completed" | "skipped";
225
300
  };
226
301
  export interface GuideResult {
227
302
  state: GuideState;
@@ -236,11 +311,12 @@ export declare const COPY: {
236
311
  readonly replayEnded: "จบการทบทวนแล้ว";
237
312
  readonly hiddenToast: "ซ่อนแล้ว";
238
313
  readonly aborted: "ไกด์ไปต่อไม่ได้ ลองอีกครั้งภายหลัง";
314
+ readonly pageDone: "จบคู่มือหน้านี้";
239
315
  readonly bridgeTitle: "กดปุ่มเมนู";
240
316
  readonly bridgeWhy: (app: string) => string;
241
317
  };
242
318
  export declare const BRIDGE_TARGET = "shell.menu-toggle";
243
- export declare function initialGuideState(audience: GuideAudience, progress: GuideProgress): GuideState;
319
+ export declare function initialGuideState(audience: GuideAudience, progress: GuideProgress, pages?: GuidePagesInit): GuideState;
244
320
  export declare function findMission(suite: GuideSuite, key: string | null | undefined): GuideMission | undefined;
245
321
  /**
246
322
  * Whether the user has this mission (GuideMission.visible). Without the
@@ -319,10 +395,50 @@ export declare function stepPosition(mission: GuideMission, index: number, env:
319
395
  };
320
396
  /** "กด [บันทึก]" → "กด บันทึก" (for screen readers). */
321
397
  export declare function plainTitle(t: string): string;
398
+ /** The step's status line (GuideStep.status): trimmed text, or null. */
399
+ export declare function stepStatus(step: GuideStep, ctx: GuideContext): string | null;
400
+ /**
401
+ * In a replay, whether a "select" step may go on with ถัดไป: the next step
402
+ * this person sees has its target on the page already (or there is none).
403
+ * A pick that makes the next step's target (a save shown only once the
404
+ * right value is chosen) is still needed.
405
+ */
406
+ export declare function replayCanGoOn(mission: GuideMission, index: number, env: GuideEnv, context: Readonly<Record<string, string>>, nothingToSave?: boolean): boolean;
322
407
  /** A mission's `completeOn` triggers, as a list. */
323
408
  export declare function completeTriggersOf(mission: GuideMission): GuideTrigger[];
324
409
  /** Every app event name a suite's `completeOn` listens for. */
325
410
  export declare function passiveEventNames(suite: GuideSuite): string[];
326
411
  /** Missions whose `completeOn` this signal meets ({ api } and { event } only). */
327
412
  export declare function passiveMatches(suite: GuideSuite, sig: GuideSignal): GuideMission[];
413
+ /** This page's guide for this person (the most specific match, if theirs). */
414
+ export declare function pageHere(suite: GuideSuite, env: GuideEnv): ReturnType<typeof currentPage>;
415
+ /** The page guide's row says this person has ended this version of it. */
416
+ export declare function pageRecorded(state: GuideState, page: {
417
+ key: string;
418
+ version: number;
419
+ }): boolean;
420
+ /** Whether the first step this person sees has its target on screen (or none). */
421
+ export declare function firstStepReady(suite: GuideSuite, pageKey: string, env: GuideEnv): boolean;
422
+ /**
423
+ * The page guide to offer now, or null. Due when: pages mode is "offer" and
424
+ * this person has page guides; nothing else is up (no step, card, panel or
425
+ * celebration waiting); this page's guide is theirs, may be offered, and has
426
+ * no row of this version; its first step's target is on screen; no app
427
+ * dialog is open. The host adds the rest: the welcome not due on this
428
+ * route, a moment of nothing loading, the primary tab, not hidden.
429
+ */
430
+ export declare function pageOfferDue(state: GuideState, suite: GuideSuite, env: GuideEnv): string | null;
431
+ /** The step before this one that this person sees, or -1. */
432
+ export declare function previousShownIndex(mission: GuideMission, index: number, env: GuideEnv, context: Readonly<Record<string, string>>): number;
433
+ /**
434
+ * Where ย้อนกลับ goes from a page guide's step, or null. The step before it
435
+ * that this person sees; from inside what a click opened (not a
436
+ * checkpoint) back to that click, closing it first (the step's abort,
437
+ * Escape by default). A click step whose result is still on screen (a tab
438
+ * switched) is passed over: entering it again would pass it at once.
439
+ */
440
+ export declare function backTarget(mission: GuideMission, index: number, env: GuideEnv, context: Readonly<Record<string, string>>): {
441
+ index: number;
442
+ close: boolean;
443
+ } | null;
328
444
  export declare function reduceGuide(state: GuideState, action: GuideAction, suite: GuideSuite, env: GuideEnv): GuideResult;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Page guides ("คู่มือหน้านี้", shell 0.1.74): which page guides a suite
3
+ * really has (validation), which one is this page's (the most specific
4
+ * match), and how one runs as a mission. Pure: the tests run it in Node.
5
+ *
6
+ * A page guide runs through the missions' machinery as a synthetic mission
7
+ * `page:<key>` in replay mode, so nothing it does writes tour progress.
8
+ */
9
+ import type { GuideContext, GuideMission, GuidePage, GuideSuite } from "./types";
10
+ /** The synthetic mission key of a page guide: `page:<key>`. */
11
+ export declare const PAGE_PREFIX = "page:";
12
+ export declare function isPageMissionKey(key: string | null | undefined): boolean;
13
+ export declare function pageKeyOf(missionKey: string | null | undefined): string | undefined;
14
+ /** Why a page guide cannot run, or null when it can. */
15
+ export declare function pageProblem(app: string, page: GuidePage): string | null;
16
+ /**
17
+ * The suite's page guides that can run. A page guide with a commit, input or
18
+ * select step (it would save), a bad key or no route is dropped, with a
19
+ * warning in development; so is a second one with the same key.
20
+ */
21
+ export declare function validPages(suite: GuideSuite): GuidePage[];
22
+ export declare function findPage(suite: GuideSuite, key: string | null | undefined): GuidePage | undefined;
23
+ /**
24
+ * The page guide as a mission the machine plays: `page:<key>`. Steps lose
25
+ * any `route` (the guide is the page's, and ends when it is left) and are
26
+ * checkpoints unless they say otherwise.
27
+ */
28
+ export declare function pageMission(page: GuidePage): GuideMission;
29
+ /** What the page matching needs to know about where the person is. */
30
+ export interface PageWhere {
31
+ code: string;
32
+ pathname: string;
33
+ search: URLSearchParams;
34
+ isVisible: (anchor: string) => boolean;
35
+ }
36
+ /** How specific a route pattern is: its `?k=v` constraints, then its static segments. */
37
+ export declare function routeSpecificity(pattern: string): [number, number];
38
+ /**
39
+ * Whether the page guide applies here: one of its routes matches and its
40
+ * `when` anchor (if any) is on screen. The score ranks matches: `when`
41
+ * first, then query constraints, then static segments. Null when it does
42
+ * not apply.
43
+ */
44
+ export declare function pageMatchScore(page: GuidePage, where: PageWhere): [number, number, number] | null;
45
+ /** The most specific page guide that applies here, whoever it is for (first in the list on a tie). */
46
+ export declare function matchingPage(suite: GuideSuite, where: PageWhere): GuidePage | null;
47
+ /** Whether this person has the page guide: `visible` (only true) and `requires`. */
48
+ export declare function pageShown(page: GuidePage, ctx: GuideContext): boolean;
49
+ /**
50
+ * This page's guide for this person: the most specific match, when it is
51
+ * theirs. A more specific guide they do not have is not replaced by a less
52
+ * specific one (it would describe what is not on screen).
53
+ */
54
+ export declare function currentPage(suite: GuideSuite, where: PageWhere, ctx: GuideContext): GuidePage | null;
55
+ /** The page guides this person has, for the panel's list (those with an `href`). */
56
+ export declare function listedPages(suite: GuideSuite, ctx: GuideContext): GuidePage[];
@@ -8,7 +8,7 @@
8
8
  * No runtime imports beyond sibling pure modules: the tests run it in Node.
9
9
  */
10
10
  import type { GuideSuite } from "./types";
11
- import { type GuideProgress, type GuidesAnswer } from "./progress";
11
+ import { type GuideProgress, type GuidesAnswer, type PageRow } from "./progress";
12
12
  /** What a progress call is about. */
13
13
  export interface GuideProgressRequest {
14
14
  /** The suite's app ("peoplx", "liff"). */
@@ -66,3 +66,11 @@ export declare function loadGuides(adapter: GuideProgressAdapter, suite: GuideSu
66
66
  * stored row, or null when it could not be saved.
67
67
  */
68
68
  export declare function storeGuides(adapter: GuideProgressAdapter, suite: GuideSuite, progress: GuideProgress): Promise<GuideProgress | null>;
69
+ /**
70
+ * Writes a page guide's row (`<app>.page.<key>`) once: the minimal body,
71
+ * with the same 409 handling as the tour's row. Another tab that already
72
+ * recorded this version wins (its row is returned, nothing is sent again);
73
+ * an older version's row is replaced with its rev; a newer one stops. Returns
74
+ * the stored row, or null when it could not be saved.
75
+ */
76
+ export declare function storePageRow(adapter: GuideProgressAdapter, app: string, key: string, version: number, status: "completed" | "skipped", rev: number): Promise<PageRow | null>;
@@ -6,6 +6,8 @@
6
6
  * No runtime imports: the tests run this file in Node as it is.
7
7
  */
8
8
  export type GuideMode = "off" | "invite" | "forced";
9
+ /** How page guides are offered (`guides.<app>.pages`): off, from "?" only, or also on a first visit. */
10
+ export type PagesMode = "off" | "menu" | "offer";
9
11
  export type GuideAudience = "forced" | "optional" | "none";
10
12
  export type GuideRowStatus = "in_progress" | "completed" | "skipped" | "aborted";
11
13
  export type MissionStatus = "todo" | "active" | "done" | "skipped";
@@ -38,9 +40,21 @@ export interface GuideProgress {
38
40
  /** The status the server holds (to know when a PUT needs restart: true). */
39
41
  server_status: GuideRowStatus | null;
40
42
  }
43
+ /** A page guide's row (`<app>.page.<key>`), as the client keeps it. */
44
+ export interface PageRow {
45
+ version: number;
46
+ status: GuideRowStatus;
47
+ rev: number;
48
+ }
41
49
  export interface GuidesAnswer {
42
50
  mode: GuideMode;
43
51
  audience: GuideAudience;
52
+ /** The page guides' switch; "off" from an API without it. */
53
+ pagesMode: PagesMode;
54
+ /** Who gets page guides: "optional" for every member while pages are not off. */
55
+ pageAudience: GuideAudience;
56
+ /** Page rows by page key (the part after `<app>.page.`). */
57
+ pages: Record<string, PageRow>;
44
58
  /** The suite's row; a fresh one when the user has none or an older version. */
45
59
  progress: GuideProgress;
46
60
  /** The server holds a newer version than this app's tour: do not run. */
@@ -58,6 +72,17 @@ export declare function parseGuidesAnswer(payload: unknown, suite: {
58
72
  version: number;
59
73
  app: string;
60
74
  }): GuidesAnswer | null;
75
+ /** The row key of a page guide: `<app>.page.<key>`. */
76
+ export declare function pageRowKey(app: string, key: string): string;
77
+ /** Page rows among the list's rows, by page key. Off-shape rows are left out. */
78
+ export declare function parsePageRows(rows: unknown[], app: string): Record<string, PageRow>;
79
+ /** One page row (a list entry, a PUT answer, a 409's `current`); null when off-shape. */
80
+ export declare function parsePageRow(raw: unknown): PageRow | null;
81
+ /**
82
+ * A page row's PUT body: the least the server takes. Written once per page
83
+ * and version (finished: completed; not now, exited or left: skipped).
84
+ */
85
+ export declare function pageBody(app: string, version: number, rev: number, status: "completed" | "skipped"): Record<string, unknown>;
61
86
  /** The PUT body for the client's state. Drops keys the server would refuse. */
62
87
  export declare function putBody(p: GuideProgress, app: string): Record<string, unknown>;
63
88
  /**
@@ -161,6 +161,14 @@ export interface GuideStep {
161
161
  * `api.id` is the saved record's id (data.id or data.data.id).
162
162
  */
163
163
  capture?: (e: GuideCaptureEvent, ctx: GuideContext) => Record<string, string> | void;
164
+ /**
165
+ * A short status line under the instruction while it returns text, e.g. a
166
+ * wrong pick on a "select" step: "ยังไม่ใช่ชื่อคุณ". A label of what is on
167
+ * the page now, not help text. Read again on every page change during the
168
+ * step (use `ctx.visible` on an anchor the page shows for that state).
169
+ * Shell ≥ 0.1.73.
170
+ */
171
+ status?: (ctx: GuideContext) => string | null | undefined;
164
172
  /** Skip the step (silently) when true. Runs every time it is entered. */
165
173
  skipIf?: (ctx: GuideContext) => boolean;
166
174
  /** A policy key the step needs (checked with `guides.can`); skipped when
@@ -314,6 +322,62 @@ export interface GuideGate {
314
322
  /** The line on each mission in the panel while waiting: "ต้องมีพนักงาน 2 คน". */
315
323
  pending: string;
316
324
  }
325
+ /**
326
+ * A page guide ("คู่มือหน้านี้", shell ≥ 0.1.74): a read-only walk through one
327
+ * page's regions and buttons — what the page is for, what each column,
328
+ * filter and status means, what each button does. It never saves: only
329
+ * "view" and "click" steps (the engine drops a page guide with a "commit",
330
+ * "input" or "select" step, with a warning in development), and it runs as
331
+ * a replay, so nothing it touches writes tour progress. Authors:
332
+ * _claude-work/guides-r4/GUIDE-PAGES.md.
333
+ *
334
+ * Progress is one row per page per user, `<app>.page.<key>`, written once:
335
+ * when the first-visit offer is answered or ignored, or when the guide ends
336
+ * (finished → completed; exited or left → skipped). Opened again from "?",
337
+ * it writes nothing.
338
+ */
339
+ export interface GuidePage {
340
+ /** Stable, [a-z0-9-]: the row key is `${suite.app}.page.${key}` (≤ 64 characters). */
341
+ key: string;
342
+ /** Raise it to offer this page's guide again to everyone (this page only). */
343
+ version: number;
344
+ /**
345
+ * The page, as GuideTrigger.route ("/[code]/project/budgets",
346
+ * "/[code]/project/detail/[id]?view=gantt"); several for one page. When
347
+ * several page guides match, the most specific wins: one with `when`, then
348
+ * the most `?k=v` constraints, then the most static segments. If the
349
+ * winner is not for this user (`visible`, `requires`), there is none here.
350
+ */
351
+ route: string | string[];
352
+ /**
353
+ * An anchor that must be on screen for this guide to apply: sub-screens
354
+ * that share a URL (LIFF's leave list vs its form). The offer waits for it,
355
+ * and the guide ends when it goes.
356
+ */
357
+ when?: GuideAnchorKey;
358
+ /** "งบประมาณ": the offer card, "?" → "คู่มือหน้านี้" and the panel's list. ≤ 30 characters. */
359
+ title: string;
360
+ /**
361
+ * Where the panel's "คู่มือแต่ละหน้า" list opens it (`[code]` filled in).
362
+ * Omit it for pages that need an id (`[id]`): those are listed only on
363
+ * the page itself, through "คู่มือหน้านี้".
364
+ */
365
+ href?: GuideText;
366
+ /** Only `true` shows it (as GuideMission.visible): role, plan, module, menu. */
367
+ visible?: (ctx: GuideContext) => boolean | undefined;
368
+ /** A policy key the page needs (checked with `guides.can`). */
369
+ requires?: string;
370
+ /** false: never offered by itself, only opened from "?". Default true. */
371
+ offer?: boolean;
372
+ /**
373
+ * "view" and "click" steps only, ≤ 9 (the app's tests check the count).
374
+ * Steps take no `route`: the guide ends when the page is left. Every step
375
+ * is a checkpoint unless it says `checkpoint: false` (a step inside a
376
+ * dialog or menu a click opened: a closed dialog goes back to the click,
377
+ * and closing the guide there presses Escape).
378
+ */
379
+ steps: GuideStep[];
380
+ }
317
381
  /** One app's whole tour: the welcome, the missions, the finish. */
318
382
  export interface GuideSuite {
319
383
  /** Progress key on the server, "<app>.<name>": "peoplx.first-run". */
@@ -357,6 +421,8 @@ export interface GuideSuite {
357
421
  };
358
422
  };
359
423
  missions: GuideMission[];
424
+ /** Page guides ("คู่มือหน้านี้"), shell ≥ 0.1.74. */
425
+ pages?: GuidePage[];
360
426
  }
361
427
  /** AppLayoutShell's `guides` prop. */
362
428
  export interface GuideConfig {
@@ -370,4 +436,10 @@ export interface GuideConfig {
370
436
  * Default true.
371
437
  */
372
438
  ready?: boolean;
439
+ /**
440
+ * False holds the page guides alone; default: as `ready`. Lets pages run
441
+ * while the missions wait (Accounx's settings pages are open before
442
+ * `module.accounting` is known). Shell ≥ 0.1.74.
443
+ */
444
+ pagesReady?: boolean;
373
445
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var guideRuntime = require('./chunks/guide-runtime-DfnNdrjZ.cjs');
4
+ var guideRuntime = require('./chunks/guide-runtime-YS2nUhwS.cjs');
5
5
  require('react/jsx-runtime');
6
6
  require('react');
7
7
  require('react-dom');
@@ -18,12 +18,18 @@ exports.apiCallDetail = guideRuntime.apiCallDetail;
18
18
  exports.emitGuideEvent = guideRuntime.emitGuideEvent;
19
19
  exports.fillRoute = guideRuntime.fillRoute;
20
20
  exports.guideAnchor = guideRuntime.guideAnchor;
21
+ exports.guideMenuItems = guideRuntime.guideMenuItems;
22
+ exports.guideMenuShown = guideRuntime.guideMenuShown;
21
23
  exports.loadGuides = guideRuntime.loadGuides;
22
24
  exports.matchApiPath = guideRuntime.matchApiPath;
23
25
  exports.matchRoute = guideRuntime.matchRoute;
24
26
  exports.openGuidePanel = guideRuntime.openGuidePanel;
27
+ exports.openPageGuide = guideRuntime.openPageGuide;
28
+ exports.openPagesPanel = guideRuntime.openPagesPanel;
25
29
  exports.reportApiCall = guideRuntime.reportApiCall;
26
30
  exports.reportApiEvent = guideRuntime.reportApiEvent;
27
31
  exports.storeGuides = guideRuntime.storeGuides;
28
32
  exports.useGuideEntry = guideRuntime.useGuideEntry;
33
+ exports.usePageGuideEntry = guideRuntime.usePageGuideEntry;
34
+ exports.usePageGuides = guideRuntime.usePageGuides;
29
35
  //# sourceMappingURL=guide-core.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"guide-core.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"guide-core.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}