@eintrek/erp-shell 0.1.73 → 0.1.75

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,12 @@
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, GuideSection, 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 { guideMenuShown, openGuidePanel, useGuideEntry, type GuideEntry } from "./guide-store";
15
+ export { guideMenuItems, guideMenuShown, openGuidePanel, openPageGuide, openPagesPanel, openSectionGuide, setSectionGuideValues, useGuideEntry, usePageGuideEntry, usePageGuides, useSectionGuideValues, type GuideEntry, type GuideMenuItem, type PageGuidesEntry, } from "./guide-store";
16
+ export { SectionGuideButton, type SectionGuideButtonProps } from "./section-button";
17
+ export { THAI_MONTHS_SHORT, formatBaht, thaiMonth, thaiMonthYear } from "./format";
16
18
  export { GuideRuntime, type GuideMenu, type GuideRouting, type GuideRuntimeProps, } from "./guide-runtime";
17
19
  export { GuideProgressConflict, loadGuides, storeGuides, type GuideProgressAdapter, type GuideProgressRequest, type GuideProgressResponse, } from "./progress-sync";
18
- export type { GuideAudience, GuideMode } from "./progress";
20
+ export type { GuideAudience, GuideMode, PagesMode } from "./progress";
19
21
  export { matchApiPath, matchRoute, fillRoute } from "./patterns";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Small formatters for section guides' copy (shell 0.1.75): the app passes
3
+ * its own computed numbers, and the copy says them the way the page does —
4
+ * "ม.ค. รับ 500,000 − จ่าย 320,000 = คงเหลือ 180,000".
5
+ *
6
+ * Pure: the tests run it in Node.
7
+ */
8
+ export declare const THAI_MONTHS_SHORT: readonly ["ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค."];
9
+ /**
10
+ * A baht amount with thousands separators, no currency sign: 500000 →
11
+ * "500,000"; 1234.5 → "1,234.50"; -320000 → "-320,000". Whole amounts
12
+ * have no decimals unless `decimals` says so; others have two. Not a
13
+ * finite number → "" (a step whose value is missing is skipped anyway).
14
+ */
15
+ export declare function formatBaht(n: unknown, opts?: {
16
+ decimals?: number;
17
+ }): string;
18
+ /** "2026-01" (or a Date, or 1) → "ม.ค."; "" when it is not a month. */
19
+ export declare function thaiMonth(m: unknown): string;
20
+ /**
21
+ * "2026-01" → "ม.ค. 69" (the Buddhist year's last two digits), or with
22
+ * `{ year: "full" }` "ม.ค. 2569"; just the month when no year is known.
23
+ */
24
+ export declare function thaiMonthYear(m: unknown, opts?: {
25
+ year?: "short" | "full";
26
+ }): string;
@@ -11,14 +11,80 @@ export interface GuideEntry {
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
13
  /**
14
- * Whether the "?" menu offers "ไกด์เริ่มต้น": a tour whose missions are all
15
- * hidden for this user (`visible`, e.g. Plannex without an employee record)
16
- * has nothing to open, and "0/0" led to an empty panel. The entry itself
17
- * stays published: apps wait on it before they load the facts that make
18
- * missions visible.
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.
19
16
  */
20
- export declare function guideMenuShown(e: GuideEntry | null): e is GuideEntry;
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
+ * Section guides on screen here that this person has ("อธิบายส่วนนี้ ·
26
+ * <title>"), in the suite's order. Shell ≥ 0.1.75.
27
+ */
28
+ sections?: ReadonlyArray<{
29
+ key: string;
30
+ title: string;
31
+ }>;
32
+ }
33
+ /** Called by the host: this page's guide, the listed count, the sections on screen, and how to open them. */
34
+ export declare function publishPageGuides(next: PageGuidesEntry | null, openPage: (() => void) | null, openPages: (() => void) | null, openSection?: ((key: string) => void) | null): void;
35
+ /** The "?" menu's items, in order (pure: the menu and its tests share it). "sections": one entry per section guide. */
36
+ export type GuideMenuItem = "page" | "sections" | "missions" | "pages" | "help";
37
+ export declare function guideMenuItems(e: GuideEntry | null, p?: PageGuidesEntry): GuideMenuItem[];
38
+ /**
39
+ * Whether the "?" press opens a menu rather than the help dialog: a tour
40
+ * with missions shown, a guide for this page, or page guides to list. A tour
41
+ * whose missions are all hidden for this user (`visible`, e.g. Plannex
42
+ * without an employee record) has nothing to open, and "0/0" led to an
43
+ * empty panel. The entry itself stays published: apps wait on it before
44
+ * they load the facts that make missions visible.
45
+ */
46
+ export declare function guideMenuShown(e: GuideEntry | null, p?: PageGuidesEntry): boolean;
21
47
  /** Opens the tour's quest panel, when this user has a tour here. */
22
48
  export declare function openGuidePanel(): void;
23
49
  /** The "?" menu's entry, or null when there is no tour for this user. */
24
50
  export declare function useGuideEntry(): GuideEntry | null;
51
+ /** Opens this page's guide ("คู่มือหน้านี้"), when there is one. */
52
+ export declare function openPageGuide(): void;
53
+ /** Opens the panel on the page guides alone ("คู่มือทุกหน้า"). */
54
+ export declare function openPagesPanel(): void;
55
+ /** Page guides here (this page's, and the listed count); stable until they change. */
56
+ export declare function usePageGuides(): PageGuidesEntry;
57
+ /**
58
+ * This page's guide: `{ title, open }`, or null when the page (or the
59
+ * sub-screen, `when`) has none for this person. For an app that draws its
60
+ * own "?" (LIFF's page head): draw it only when this is not null.
61
+ */
62
+ export declare function usePageGuideEntry(): {
63
+ title: string;
64
+ open: () => void;
65
+ } | null;
66
+ /**
67
+ * Opens a section guide ("อธิบายส่วนนี้"), when it is on screen here for this
68
+ * person. The values it shows are the ones the app last rendered
69
+ * (`useSectionGuideValues`).
70
+ */
71
+ export declare function openSectionGuide(key: string): void;
72
+ type Values = Readonly<Record<string, unknown>>;
73
+ /**
74
+ * Sets (or with undefined, clears) a section guide's live values. The
75
+ * engine reads the latest whenever it draws a step, so a page that
76
+ * re-renders mid-guide never leaves an old number on screen.
77
+ */
78
+ export declare function setSectionGuideValues(key: string, values: Values | undefined): void;
79
+ /** A section guide's live values now (empty when the app has given none). */
80
+ export declare function getSectionGuideValues(key: string | null | undefined): Values;
81
+ /** Re-renders when any section guide's values change (the engine's own use). */
82
+ export declare function useSectionValuesVersion(): number;
83
+ /**
84
+ * Hands a section guide the numbers its copy uses, from the component that
85
+ * computes them: `useSectionGuideValues("cash-flow", { month: "ม.ค.", cashIn,
86
+ * … })`. Call it on every render (the latest wins); they go when the
87
+ * component unmounts. undefined values: nothing.
88
+ */
89
+ export declare function useSectionGuideValues(key: string, values: Values | undefined): void;
90
+ export {};
@@ -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-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-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";
@@ -60,6 +60,17 @@ export interface BubbleModel {
60
60
  status?: string | null;
61
61
  showNext: boolean;
62
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;
63
74
  }
64
75
  export interface StepLayerProps {
65
76
  /** The spotlight (padded target), or null while there is none to show. */
@@ -101,6 +112,8 @@ export interface StepLayerProps {
101
112
  moving: boolean;
102
113
  onExit: () => void;
103
114
  onNext: () => void;
115
+ /** ย้อนกลับ (BubbleModel.showBack). */
116
+ onBack?: () => void;
104
117
  onEndWithoutSave: () => void;
105
118
  onRetry: () => void;
106
119
  onGoResume: () => void;
@@ -170,6 +183,23 @@ export declare function CompleteModal(p: {
170
183
  reduced: boolean;
171
184
  onFinish: () => void;
172
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
+ }
173
203
  export declare function QuestChip({ done, total, pulse, onOpen }: {
174
204
  done: number;
175
205
  total: number;
@@ -201,6 +231,14 @@ export declare function QuestPanel(p: {
201
231
  onStart: (key: string, replay: boolean) => void;
202
232
  onDismiss: (key: string) => void;
203
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;
204
242
  }): import("react/jsx-runtime").JSX.Element;
205
243
  export declare function GuideToast({ text }: {
206
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"
@@ -55,6 +56,15 @@ export type GuideScreen = {
55
56
  * need not change a real record to go on. Shell ≥ 0.1.73.
56
57
  */
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
+ /** A section guide's step: the section key (the mission is `section:<key>`). Shell ≥ 0.1.75. */
62
+ section?: string;
63
+ }
64
+ /** The first-visit offer of this page's guide: a small card, not modal. */
65
+ | {
66
+ name: "pageOffer";
67
+ page: string;
58
68
  } | {
59
69
  name: "celebrate";
60
70
  mission: string;
@@ -84,7 +94,44 @@ export interface GuideState {
84
94
  * the next idle moment (never mid-step). Not saved: a reload drops it.
85
95
  */
86
96
  pendingCelebrate: string[];
97
+ /** Who gets page guides ("optional" for every member while they are on). */
98
+ pageAudience: GuideAudience;
99
+ /** How page guides are offered: off, from "?" only, or also on a first visit. */
100
+ pagesMode: PagesMode;
101
+ /** Page rows by page key, as the server holds them (or as just written). */
102
+ pages: Record<string, PageRow>;
103
+ /** Section rows by section key (`<app>.section.<key>`). Shell ≥ 0.1.75. */
104
+ sections: Record<string, PageRow>;
105
+ /**
106
+ * The page or section guide being played: `write` when its row is still
107
+ * to be written (the offer was taken, or this person has never ended this
108
+ * version); a replay of a recorded one writes nothing.
109
+ */
110
+ pageRun: {
111
+ key: string;
112
+ write: boolean;
113
+ kind?: ReadKind;
114
+ } | null;
115
+ /** What the panel shows: everything, or the page guides alone ("คู่มือทุกหน้า"). */
116
+ panelSection: "all" | "pages";
117
+ /**
118
+ * Steps of the mission being played that passed by themselves (a click
119
+ * whose outcome already held): not counted in "ขั้นที่ x/y". Shell ≥ 0.1.75.
120
+ */
121
+ autoPassed: {
122
+ mission: string;
123
+ steps: number[];
124
+ } | null;
87
125
  }
126
+ /** The page-guide part of the state, from the progress answer. */
127
+ export interface GuidePagesInit {
128
+ pageAudience: GuideAudience;
129
+ pagesMode: PagesMode;
130
+ pages: Record<string, PageRow>;
131
+ sections?: Record<string, PageRow>;
132
+ }
133
+ /** A read-only guide: a page guide or a section guide. */
134
+ export type ReadKind = "page" | "section";
88
135
  export interface ReplayPage {
89
136
  mission: string;
90
137
  /** The mission's abort anchors that were on the page at the start. */
@@ -107,6 +154,8 @@ export interface GuideEnv {
107
154
  targetValue: () => string;
108
155
  /** An app dialog (not the tour's) is open. */
109
156
  dialogOpen?: () => boolean;
157
+ /** The running (or opening) section guide's live values (GuideContext.values). */
158
+ values?: Readonly<Record<string, unknown>>;
110
159
  }
111
160
  export type GuideSignal = {
112
161
  kind: "api";
@@ -190,6 +239,7 @@ export type GuideAction = {
190
239
  type: "dismissComplete";
191
240
  } | {
192
241
  type: "openPanel";
242
+ section?: "all" | "pages";
193
243
  } | {
194
244
  type: "closePanel";
195
245
  } | {
@@ -209,6 +259,40 @@ export type GuideAction = {
209
259
  /** The host is idle: show a waiting `completeOn` celebration. */
210
260
  | {
211
261
  type: "idleCelebrate";
262
+ }
263
+ /** The host found this page's guide due (pageOfferDue): show the offer card. */
264
+ | {
265
+ type: "pageOffer";
266
+ page: string;
267
+ }
268
+ /** The offer's [เริ่ม]. */
269
+ | {
270
+ type: "pageOfferStart";
271
+ }
272
+ /** The offer's [ไม่ตอนนี้]: recorded, never offered again (this version). */
273
+ | {
274
+ type: "pageOfferLater";
275
+ }
276
+ /** "คู่มือหน้านี้" from "?", or a page in the panel's list (once on its page). */
277
+ | {
278
+ type: "startPage";
279
+ page: string;
280
+ }
281
+ /** ย้อนกลับ on a page guide's read step. */
282
+ | {
283
+ type: "back";
284
+ }
285
+ /** A page row came back from the server. */
286
+ | {
287
+ type: "pageRow";
288
+ key: string;
289
+ row: PageRow;
290
+ kind?: ReadKind;
291
+ }
292
+ /** "อธิบายส่วนนี้": a section guide on screen here (its button, or "?"). */
293
+ | {
294
+ type: "startSection";
295
+ section: string;
212
296
  };
213
297
  export type GuideEffect = {
214
298
  type: "save";
@@ -230,6 +314,14 @@ export type GuideEffect = {
230
314
  | {
231
315
  type: "abort";
232
316
  actions: GuideAbortAction[];
317
+ }
318
+ /** Write a page (or section) guide's row, once: `<app>.page.<key>` / `<app>.section.<key>`. */
319
+ | {
320
+ type: "savePage";
321
+ key: string;
322
+ version: number;
323
+ status: "completed" | "skipped";
324
+ kind?: ReadKind;
233
325
  };
234
326
  export interface GuideResult {
235
327
  state: GuideState;
@@ -244,11 +336,13 @@ export declare const COPY: {
244
336
  readonly replayEnded: "จบการทบทวนแล้ว";
245
337
  readonly hiddenToast: "ซ่อนแล้ว";
246
338
  readonly aborted: "ไกด์ไปต่อไม่ได้ ลองอีกครั้งภายหลัง";
339
+ readonly pageDone: "จบคู่มือหน้านี้";
340
+ readonly sectionDone: "จบคำอธิบายส่วนนี้";
247
341
  readonly bridgeTitle: "กดปุ่มเมนู";
248
342
  readonly bridgeWhy: (app: string) => string;
249
343
  };
250
344
  export declare const BRIDGE_TARGET = "shell.menu-toggle";
251
- export declare function initialGuideState(audience: GuideAudience, progress: GuideProgress): GuideState;
345
+ export declare function initialGuideState(audience: GuideAudience, progress: GuideProgress, pages?: GuidePagesInit): GuideState;
252
346
  export declare function findMission(suite: GuideSuite, key: string | null | undefined): GuideMission | undefined;
253
347
  /**
254
348
  * Whether the user has this mission (GuideMission.visible). Without the
@@ -321,10 +415,14 @@ export declare function chipVisible(state: GuideState, suite: GuideSuite, ctx: G
321
415
  * whose `requires` they lack or whose `skipIf` holds is left out (the step on
322
416
  * screen always counts).
323
417
  */
324
- export declare function stepPosition(mission: GuideMission, index: number, env: GuideEnv, context: Readonly<Record<string, string>>): {
418
+ export declare function stepPosition(mission: GuideMission, index: number, env: GuideEnv, context: Readonly<Record<string, string>>, autoPassed?: readonly number[]): {
325
419
  no: number;
326
420
  total: number;
327
421
  };
422
+ /** A step entered now would pass at once: a click or select step whose route or visible outcome holds. */
423
+ export declare function wouldAutoPass(step: GuideStep, env: GuideEnv): boolean;
424
+ /** The steps of this mission that passed by themselves in this run. */
425
+ export declare function autoPassedOf(state: GuideState, mission: string): readonly number[];
328
426
  /** "กด [บันทึก]" → "กด บันทึก" (for screen readers). */
329
427
  export declare function plainTitle(t: string): string;
330
428
  /** The step's status line (GuideStep.status): trimmed text, or null. */
@@ -342,4 +440,42 @@ export declare function completeTriggersOf(mission: GuideMission): GuideTrigger[
342
440
  export declare function passiveEventNames(suite: GuideSuite): string[];
343
441
  /** Missions whose `completeOn` this signal meets ({ api } and { event } only). */
344
442
  export declare function passiveMatches(suite: GuideSuite, sig: GuideSignal): GuideMission[];
443
+ /** This page's guide for this person (the most specific match, if theirs). */
444
+ export declare function pageHere(suite: GuideSuite, env: GuideEnv): ReturnType<typeof currentPage>;
445
+ /** The page guide's row says this person has ended this version of it. */
446
+ export declare function pageRecorded(state: GuideState, page: {
447
+ key: string;
448
+ version: number;
449
+ }): boolean;
450
+ /** Whether the first step this person sees has its target on screen (or none). */
451
+ export declare function firstStepReady(suite: GuideSuite, pageKey: string, env: GuideEnv): boolean;
452
+ /**
453
+ * The page guide to offer now, or null. Due when: pages mode is "offer" and
454
+ * this person has page guides; nothing else is up (no step, card, panel or
455
+ * celebration waiting); this page's guide is theirs, may be offered, and has
456
+ * no row of this version; its first step's target is on screen; no app
457
+ * dialog is open. The host adds the rest: the welcome not due on this
458
+ * route, a moment of nothing loading, the primary tab, not hidden.
459
+ */
460
+ export declare function pageOfferDue(state: GuideState, suite: GuideSuite, env: GuideEnv): string | null;
461
+ /** The section guide's row says this person has ended this version of it. */
462
+ export declare function sectionRecorded(state: GuideState, section: {
463
+ key: string;
464
+ version: number;
465
+ }): boolean;
466
+ /** The section guides on screen here that this person has (the "?" entries, the buttons). */
467
+ export declare function sectionsAvailable(suite: GuideSuite, env: GuideEnv): string[];
468
+ /** The step before this one that this person sees, or -1. */
469
+ export declare function previousShownIndex(mission: GuideMission, index: number, env: GuideEnv, context: Readonly<Record<string, string>>): number;
470
+ /**
471
+ * Where ย้อนกลับ goes from a page guide's step, or null. The step before it
472
+ * that this person sees; from inside what a click opened (not a
473
+ * checkpoint) back to that click, closing it first (the step's abort,
474
+ * Escape by default). A click step whose result is still on screen (a tab
475
+ * switched) is passed over: entering it again would pass it at once.
476
+ */
477
+ export declare function backTarget(mission: GuideMission, index: number, env: GuideEnv, context: Readonly<Record<string, string>>): {
478
+ index: number;
479
+ close: boolean;
480
+ } | null;
345
481
  export declare function reduceGuide(state: GuideState, action: GuideAction, suite: GuideSuite, env: GuideEnv): GuideResult;
@@ -0,0 +1,79 @@
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, GuideSection, 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[];
57
+ /** The synthetic mission key of a section guide: `section:<key>`. */
58
+ export declare const SECTION_PREFIX = "section:";
59
+ export declare function isSectionMissionKey(key: string | null | undefined): boolean;
60
+ export declare function sectionKeyOf(missionKey: string | null | undefined): string | undefined;
61
+ /** Why a section guide cannot run, or null when it can. */
62
+ export declare function sectionProblem(app: string, section: GuideSection): string | null;
63
+ /** The suite's section guides that can run (as validPages: the rest dropped, with a warning in development). */
64
+ export declare function validSections(suite: GuideSuite): GuideSection[];
65
+ export declare function findSection(suite: GuideSuite, key: string | null | undefined): GuideSection | undefined;
66
+ /** A live value is missing: undefined, null, "" or a number that is not finite. */
67
+ export declare function valueMissing(v: unknown): boolean;
68
+ /**
69
+ * The section guide as a mission the machine plays: `section:<key>`, read
70
+ * steps only, checkpoints unless they say otherwise. A step is skipped while
71
+ * a value it `needs` is missing, or when its title comes out empty.
72
+ */
73
+ export declare function sectionMission(section: GuideSection): GuideMission;
74
+ /** Whether the section guide applies here: one of its routes matches and its root is on screen. */
75
+ export declare function sectionApplies(section: GuideSection, where: PageWhere): boolean;
76
+ /** Whether this person has the section guide: `visible` (only true) and `requires`. */
77
+ export declare function sectionShown(section: GuideSection, ctx: GuideContext): boolean;
78
+ /** The section guides on screen here that this person has, in the suite's order. */
79
+ export declare function sectionsHere(suite: GuideSuite, where: PageWhere, ctx: GuideContext): GuideSection[];
@@ -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,12 @@ 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>`), or a section guide's
71
+ * (`<app>.section.<key>`, kind "section"), once: the minimal body,
72
+ * with the same 409 handling as the tour's row. Another tab that already
73
+ * recorded this version wins (its row is returned, nothing is sent again);
74
+ * an older version's row is replaced with its rev; a newer one stops. Returns
75
+ * the stored row, or null when it could not be saved.
76
+ */
77
+ export declare function storePageRow(adapter: GuideProgressAdapter, app: string, key: string, version: number, status: "completed" | "skipped", rev: number, kind?: "page" | "section"): 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,23 @@ 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>;
58
+ /** Section rows by section key (the part after `<app>.section.`). Shell ≥ 0.1.75. */
59
+ sections: Record<string, PageRow>;
44
60
  /** The suite's row; a fresh one when the user has none or an older version. */
45
61
  progress: GuideProgress;
46
62
  /** The server holds a newer version than this app's tour: do not run. */
@@ -58,6 +74,17 @@ export declare function parseGuidesAnswer(payload: unknown, suite: {
58
74
  version: number;
59
75
  app: string;
60
76
  }): GuidesAnswer | null;
77
+ /** The row key of a page guide, `<app>.page.<key>`, or a section guide, `<app>.section.<key>`. */
78
+ export declare function pageRowKey(app: string, key: string, kind?: "page" | "section"): string;
79
+ /** Page (or section) rows among the list's rows, by key. Off-shape rows are left out. */
80
+ export declare function parsePageRows(rows: unknown[], app: string, kind?: "page" | "section"): Record<string, PageRow>;
81
+ /** One page row (a list entry, a PUT answer, a 409's `current`); null when off-shape. */
82
+ export declare function parsePageRow(raw: unknown): PageRow | null;
83
+ /**
84
+ * A page row's PUT body: the least the server takes. Written once per page
85
+ * and version (finished: completed; not now, exited or left: skipped).
86
+ */
87
+ export declare function pageBody(app: string, version: number, rev: number, status: "completed" | "skipped"): Record<string, unknown>;
61
88
  /** The PUT body for the client's state. Drops keys the server would refuse. */
62
89
  export declare function putBody(p: GuideProgress, app: string): Record<string, unknown>;
63
90
  /**
@@ -0,0 +1,12 @@
1
+ export interface SectionGuideButtonProps {
2
+ /** The GuideSection's key. */
3
+ guideKey: string;
4
+ /**
5
+ * The numbers the guide's copy uses (`ctx.values`), from this render.
6
+ * Optional: the section may call `useSectionGuideValues` itself instead.
7
+ */
8
+ values?: Readonly<Record<string, unknown>>;
9
+ /** Replaces the default look (a 22px round icon button in the text colour). */
10
+ className?: string;
11
+ }
12
+ export declare function SectionGuideButton({ guideKey, values, className }: SectionGuideButtonProps): import("react/jsx-runtime").JSX.Element | null;