@eintrek/erp-shell 0.1.66 → 0.1.68

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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@eintrek/erp-shell/guide-core`: the guided-tour engine without the shell.
3
+ *
4
+ * For an app that does not use AppLayoutShell (erp-liff): no React Query,
5
+ * axios, zustand, next-auth, next-themes or sidebar behind it — only React,
6
+ * react-dom and @floating-ui/dom. The app mounts GuideRuntime with adapters
7
+ * (routing, isBusy, progress, menu, hidden). Authors:
8
+ * _claude-work/guides-r2/GUIDE-CORE.md and _claude-work/peoplx-guide/ENGINE-API.md.
9
+ *
10
+ * The main entry re-exports the same modules, so a tour defined against
11
+ * either entry is the same data, and the bus and the "?" entry are shared.
12
+ */
13
+ export type { GuideAbortAction, GuideAnchorKey, GuideApiFailure, GuideApiMatch, GuideAppId, GuideCaptureEvent, GuideConfig, GuideContext, GuideFacts, GuideGate, GuideMission, GuideNothingToSave, GuideOnMissing, GuideStep, GuideStepKind, GuideSuite, GuideText, GuideTrigger, } from "./types";
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";
16
+ export { GuideRuntime, type GuideMenu, type GuideRouting, type GuideRuntimeProps, } from "./guide-runtime";
17
+ export { GuideProgressConflict, loadGuides, storeGuides, type GuideProgressAdapter, type GuideProgressRequest, type GuideProgressResponse, } from "./progress-sync";
18
+ export type { GuideAudience, GuideMode } from "./progress";
19
+ export { matchApiPath, matchRoute, fillRoute } from "./patterns";
@@ -38,6 +38,8 @@ export declare const OPEN_DIALOG_SELECTOR = "[role=\"dialog\"][data-state=\"open
38
38
  * behind it.
39
39
  */
40
40
  export declare function coveredByDialog(el: Element | null): boolean;
41
+ /** An app dialog (not the tour's) is open on the page. */
42
+ export declare function appDialogOpen(): boolean;
41
43
  export declare function openPoppers(): HTMLElement[];
42
44
  export declare function inGuideLayer(node: EventTarget | null): boolean;
43
45
  export declare function prefersReducedMotion(): boolean;
@@ -22,3 +22,28 @@ export declare function emitGuideEvent(name: string, detail?: Record<string, str
22
22
  export declare function apiEventDetail(responseOrError: unknown): ApiEventDetail | null;
23
23
  /** Puts an axios answer (or error) on the bus. Safe to call with anything. */
24
24
  export declare function reportApiEvent(responseOrError: unknown): void;
25
+ /** One answered request, from a client that is not axios (plain `fetch`). */
26
+ export interface ApiCallReport {
27
+ /** "POST"; any case. Default "GET". */
28
+ method?: string;
29
+ /** The request URL, absolute or relative (any query is dropped). */
30
+ url: string;
31
+ /** The base the URL is relative to, if any. */
32
+ baseURL?: string;
33
+ /** The HTTP status; 0 for a network failure. */
34
+ status: number;
35
+ /**
36
+ * The parsed response body, if at hand. Only the saved record's id (on a
37
+ * 2xx: `id` or `data.id`) and the server's message (otherwise) are read
38
+ * from it; the body itself never goes on the bus.
39
+ */
40
+ data?: unknown;
41
+ }
42
+ /** The bus detail for a fetch-style report; null when it has no URL. */
43
+ export declare function apiCallDetail(call: ApiCallReport): ApiEventDetail | null;
44
+ /**
45
+ * Puts a request answered through plain `fetch` on the bus (`{ api }`
46
+ * triggers and `completeOn`). Call it once per answer, refusals and network
47
+ * failures (status 0) included. Safe to call with anything.
48
+ */
49
+ export declare function reportApiCall(call: ApiCallReport): void;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * GuideRuntime: runs an app's tour against the real page, with everything
3
+ * app-specific passed in as adapters — routing, "is the page loading",
4
+ * where progress is kept, the menu, when to stay hidden. It loads progress,
5
+ * decides who sees what from the answer, runs the state machine (machine.ts)
6
+ * and draws the layer (guide-ui.tsx) in a portal marked data-erp-guide-layer.
7
+ *
8
+ * Two hosts build the adapters: the shell's GuideHost (AppLayoutShell:
9
+ * Next router, React Query, the sidebar, platform-api through axios) and an
10
+ * app without the shell, through `@eintrek/erp-shell/guide-core` (erp-liff:
11
+ * its own fetch client and peoplx-api). No React Query, axios, zustand,
12
+ * next-auth, next-themes or Next import here.
13
+ *
14
+ * Fail open, always: an API error, an odd answer, a predicate or a render
15
+ * that throws — the tour does not show and the app is untouched.
16
+ */
17
+ import * as React from "react";
18
+ import type { GuideConfig } from "./types";
19
+ import { type GuideProgressAdapter } from "./progress-sync";
20
+ /** Where the user is, and how the tour sends them somewhere (route triggers, resume, ทำต่อ). */
21
+ export interface GuideRouting {
22
+ /** The current path, without the query: Next's usePathname(). */
23
+ pathname: string;
24
+ /** The current query without "?": useSearchParams().toString(). */
25
+ search: string;
26
+ /** Go to an in-app path ("/me/leave"); absolute URLs are opened by the engine itself. */
27
+ navigate: (href: string) => void;
28
+ }
29
+ /** The app's navigation menu, for the phone's "กดปุ่มเมนู" step before a `nav.*` target. */
30
+ export interface GuideMenu {
31
+ /** The menu (sidebar or drawer) is open now. */
32
+ open: boolean;
33
+ }
34
+ export interface GuideRuntimeProps {
35
+ config: GuideConfig;
36
+ /** What progress is kept per: the organisation id. A change starts over. */
37
+ organizationId: string;
38
+ /**
39
+ * Fills `[code]` in routes and is the only value `[code]`/`[[code]]` may
40
+ * match. The org code in the URL; "" where the URL has none (LIFF `/me/…`).
41
+ */
42
+ code: string;
43
+ routing: GuideRouting;
44
+ /** Where progress is read and saved (progress-sync.ts). */
45
+ progress: GuideProgressAdapter;
46
+ /**
47
+ * Whether the page is still loading data. A step waiting for its target
48
+ * counts only time when this is false; the welcome and a waiting
49
+ * celebration open after a moment of it being false. Polled. Default: never.
50
+ */
51
+ isBusy?: () => boolean;
52
+ /** The app's menu; null or omitted: none (no menu step; do not target `nav.*`). */
53
+ menu?: GuideMenu | null;
54
+ /** True keeps the tour out of sight (sign-in, a picker, a gate of the app's). */
55
+ hidden?: boolean;
56
+ /**
57
+ * Sets `--erp-guide-bottom-offset` on the layer: how far the chip and the
58
+ * bottom cards sit from the bottom (above a bottom nav), e.g.
59
+ * "calc(64px + env(safe-area-inset-bottom))". Default: the CSS variable as
60
+ * the page sets it, else 16px (22px beside the docked layout on desktop).
61
+ */
62
+ bottomOffset?: string;
63
+ }
64
+ /** Hides the tour (and only the tour) after any error under it. */
65
+ export declare class GuideErrorBoundary extends React.Component<{
66
+ children: React.ReactNode;
67
+ }, {
68
+ failed: boolean;
69
+ }> {
70
+ state: {
71
+ failed: boolean;
72
+ };
73
+ static getDerivedStateFromError(): {
74
+ failed: boolean;
75
+ };
76
+ componentDidCatch(error: unknown): void;
77
+ render(): React.ReactNode;
78
+ }
79
+ /**
80
+ * Runs a tour with the app's adapters. Mount it once, above the pages (so a
81
+ * route change does not unmount it), only where the tour may run.
82
+ */
83
+ export declare function GuideRuntime(props: GuideRuntimeProps): import("react/jsx-runtime").JSX.Element;
@@ -3,5 +3,11 @@
3
3
  * its dist for Tailwind classes, which cannot carry keyframes; so the layer
4
4
  * brings its own sheet, namespaced `eg-`, drawn from the theme's tokens
5
5
  * (--primary carries each app's brand accent; light and dark follow).
6
+ *
7
+ * `--erp-guide-bottom-offset` (set by the app on :root/body, or through
8
+ * GuideRuntime's `bottomOffset`) lifts the chip, the bottom card, the toast
9
+ * and the desktop panel above a bottom nav. Unset, they sit where they did
10
+ * before 0.1.68: 16px from the bottom (+ the safe area) on a phone, 22px on
11
+ * desktop.
6
12
  */
7
- 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:28px;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: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:82px;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:84px;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: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(12px + env(safe-area-inset-bottom))}\n .eg-toast{bottom:calc(76px + env(safe-area-inset-bottom))}\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-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";
@@ -44,6 +44,9 @@ export type GuideScreen = {
44
44
  /** A `hidden` trigger's anchor has been seen on this step, so its
45
45
  * going away counts. */
46
46
  armed: boolean;
47
+ /** A commit step shown in its read form: nothing to save
48
+ * (GuideStep.nothingToSave). */
49
+ nothingToSave: boolean;
47
50
  } | {
48
51
  name: "celebrate";
49
52
  mission: string;
@@ -63,6 +66,23 @@ export interface GuideState {
63
66
  nudge: number;
64
67
  /** The gate card was put away for this visit ("ไว้ทีหลัง"). */
65
68
  gateDismissed: boolean;
69
+ /**
70
+ * What the user already had open when the current replay started: those
71
+ * are theirs, and ending the replay leaves them alone (see pushAbort).
72
+ */
73
+ replayPage: ReplayPage | null;
74
+ /**
75
+ * Missions done outside the tour (`completeOn`) whose celebration waits for
76
+ * the next idle moment (never mid-step). Not saved: a reload drops it.
77
+ */
78
+ pendingCelebrate: string[];
79
+ }
80
+ export interface ReplayPage {
81
+ mission: string;
82
+ /** The mission's abort anchors that were on the page at the start. */
83
+ anchors: string[];
84
+ /** An app dialog was open at the start. */
85
+ dialog: boolean;
66
86
  }
67
87
  /** What the machine may ask about the page. */
68
88
  export interface GuideEnv {
@@ -77,6 +97,8 @@ export interface GuideEnv {
77
97
  sidebarOpen: boolean;
78
98
  /** The current value of the step target's field. */
79
99
  targetValue: () => string;
100
+ /** An app dialog (not the tour's) is open. */
101
+ dialogOpen?: () => boolean;
80
102
  }
81
103
  export type GuideSignal = {
82
104
  kind: "api";
@@ -166,6 +188,15 @@ export type GuideAction = {
166
188
  } | {
167
189
  type: "serverRow";
168
190
  progress: GuideProgress;
191
+ }
192
+ /** An API answer or app event, whenever the host is mounted: `completeOn`. */
193
+ | {
194
+ type: "passive";
195
+ signal: GuideSignal;
196
+ }
197
+ /** The host is idle: show a waiting `completeOn` celebration. */
198
+ | {
199
+ type: "idleCelebrate";
169
200
  };
170
201
  export type GuideEffect = {
171
202
  type: "save";
@@ -207,12 +238,32 @@ export declare const COPY: {
207
238
  export declare const BRIDGE_TARGET = "shell.menu-toggle";
208
239
  export declare function initialGuideState(audience: GuideAudience, progress: GuideProgress): GuideState;
209
240
  export declare function findMission(suite: GuideSuite, key: string | null | undefined): GuideMission | undefined;
241
+ /**
242
+ * Whether the user has this mission (GuideMission.visible). Without the
243
+ * predicate, yes; with it, only `true` (undefined — not known yet — and a
244
+ * throw hide it).
245
+ */
246
+ export declare function missionShown(m: GuideMission, ctx: GuideContext): boolean;
247
+ /** The missions this user has, in order: what the panel draws and n/N counts. */
248
+ export declare function shownMissions(suite: GuideSuite, ctx: GuideContext): GuideMission[];
249
+ /** The mission's number among the shown ones (1-based); its place in the suite when hidden. */
250
+ export declare function missionNumber(suite: GuideSuite, key: string, ctx: GuideContext): number;
210
251
  export declare function guideContext(env: GuideEnv, data: Readonly<Record<string, string>>, params?: RouteParams): GuideContext;
211
252
  export declare function resolveText(t: GuideText | undefined, ctx: GuideContext): string;
212
253
  export declare function triggersOf(step: GuideStep): GuideTrigger[];
213
254
  export declare function stepKind(step: GuideStep): import("./types").GuideStepKind;
214
255
  /** The last checkpoint at or before `index` (a mission's first step always is one). */
215
256
  export declare function checkpointAtOrBefore(steps: GuideStep[], index: number): number;
257
+ /**
258
+ * A commit step with nothing to save: its button is not on the page, and what
259
+ * is saved is (GuideStep.nothingToSave).
260
+ */
261
+ export declare function nothingToSaveHolds(step: GuideStep, env: GuideEnv, ctx: GuideContext): boolean;
262
+ /**
263
+ * The step as it is shown: a commit step with nothing to save is a read step
264
+ * on what is saved, with its own title (ถัดไป moves on).
265
+ */
266
+ export declare function shownStep(step: GuideStep, nothingToSave: boolean): GuideStep;
216
267
  /** Whether the step's target (or the bridge's) is on screen. */
217
268
  export declare function stepTargetVisible(step: GuideStep, env: GuideEnv, ctx: GuideContext, bridge: boolean): boolean;
218
269
  /** The anchor the spotlight is on for the current step. */
@@ -228,9 +279,18 @@ export declare function spotlightTarget(step: GuideStep, ctx: GuideContext, brid
228
279
  export declare function stepHomeRoute(steps: GuideStep[], index: number): string | undefined;
229
280
  /** What puts the page back when a replay ends at this step (see GuideStep.abort). */
230
281
  export declare function abortActionsFor(mission: GuideMission, index: number): GuideAbortAction[];
282
+ /** Every anchor the mission's abort actions press. */
283
+ export declare function abortAnchorsOf(mission: GuideMission): string[];
284
+ /**
285
+ * What the replay may do to put the page back: not press what the user had
286
+ * open before it started, not Escape out of a dialog that was already open,
287
+ * and no app event while anything of theirs was open.
288
+ */
289
+ export declare function abortActionsLeft(actions: GuideAbortAction[], before: ReplayPage | null): GuideAbortAction[];
231
290
  /** Status of each mission as the panel shows it. */
232
291
  export type MissionView = "todo" | "next" | "active" | "skipped" | "done" | "already" | "locked" | "waiting";
233
292
  export declare function missionSettled(m: GuideMission, entry: MissionEntry | undefined, ctx: GuideContext): boolean;
293
+ /** Settled missions among the shown ones (the n of n/N). */
234
294
  export declare function settledCount(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): number;
235
295
  export declare function gateState(suite: GuideSuite, ctx: GuideContext): boolean | undefined;
236
296
  export declare function missionViews(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): Array<{
@@ -240,10 +300,16 @@ export declare function missionViews(suite: GuideSuite, progress: GuideProgress,
240
300
  }>;
241
301
  /** The mission the celebration offers next: the first one not settled and not locked. */
242
302
  export declare function nextMissionKey(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): string | null;
243
- /** Whether the welcome should open by itself. */
244
- export declare function welcomeDue(state: GuideState, suite: GuideSuite): boolean;
303
+ /** Whether the welcome should open by itself (with `ctx`: only when a mission is shown). */
304
+ export declare function welcomeDue(state: GuideState, suite: GuideSuite, ctx?: GuideContext): boolean;
245
305
  /** Whether the floating "ภารกิจ n/N" chip shows. */
246
306
  export declare function chipVisible(state: GuideState, suite: GuideSuite, ctx: GuideContext): boolean;
247
307
  /** "กด [บันทึก]" → "กด บันทึก" (for screen readers). */
248
308
  export declare function plainTitle(t: string): string;
309
+ /** A mission's `completeOn` triggers, as a list. */
310
+ export declare function completeTriggersOf(mission: GuideMission): GuideTrigger[];
311
+ /** Every app event name a suite's `completeOn` listens for. */
312
+ export declare function passiveEventNames(suite: GuideSuite): string[];
313
+ /** Missions whose `completeOn` this signal meets ({ api } and { event } only). */
314
+ export declare function passiveMatches(suite: GuideSuite, sig: GuideSignal): GuideMission[];
249
315
  export declare function reduceGuide(state: GuideState, action: GuideAction, suite: GuideSuite, env: GuideEnv): GuideResult;
@@ -7,10 +7,14 @@ export type RouteParams = Record<string, string>;
7
7
  /**
8
8
  * Matches a Next-style page pattern ("/[code]/employee/[id]", optionally
9
9
  * "?tab=job") against a path and query. `[code]`, when given, must be the
10
- * organisation code. Returns the params, or null.
10
+ * organisation code. `[[x]]` is a segment that may be absent. Returns the
11
+ * params, or null.
11
12
  */
12
13
  export declare function matchRoute(pattern: string, pathname: string, search?: URLSearchParams | string | null, code?: string): RouteParams | null;
13
- /** Fills a page pattern's params ("/[code]/x/[id]" → "/O1/x/42"); null if one is missing. */
14
+ /**
15
+ * Fills a page pattern's params ("/[code]/x/[id]" → "/O1/x/42"); null if one
16
+ * is missing. A `[[x]]` segment without a value is left out.
17
+ */
14
18
  export declare function fillRoute(pattern: string, params: Readonly<Record<string, string | undefined>>): string | null;
15
19
  /**
16
20
  * The path of a request, without origin, base URL query or hash:
@@ -1,11 +1,12 @@
1
1
  import type { GuideSuite } from "./types";
2
- import { type GuideProgress, type GuidesAnswer } from "./progress";
2
+ import type { GuideProgress, GuidesAnswer } from "./progress";
3
+ import { type GuideProgressAdapter } from "./progress-sync";
4
+ /** platform-api's `…/organizations/:id/guides/me` as a progress adapter. */
5
+ export declare function platformGuideProgress(organizationId: string): GuideProgressAdapter;
3
6
  /** GET …/guides/me?app=…; null (show nothing) on any failure or odd answer. */
4
7
  export declare function fetchGuides(organizationId: string, suite: GuideSuite, signal?: AbortSignal): Promise<GuidesAnswer | null>;
5
8
  /**
6
- * PUT the whole state. On 409 the server's row comes back: merge (keeping
7
- * whatever is further along) and try once more with its rev. Returns the
8
- * stored row, or null when it could not be saved (the tour goes on; the
9
- * session copy keeps the place).
9
+ * PUT the whole state; on 409 merge and try once more (progress-sync.ts).
10
+ * Returns the stored row, or null when it could not be saved.
10
11
  */
11
12
  export declare function saveGuides(organizationId: string, suite: GuideSuite, progress: GuideProgress): Promise<GuideProgress | null>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Loading and saving tour progress through an adapter, so the rules stay in
3
+ * one place whatever carries the request: platform-api through the shell's
4
+ * axios (GuideHost), or peoplx-api's `/public/line/me/guides` through LIFF's
5
+ * fetch client (guide-core). The adapter only moves bytes; parsing, the
6
+ * fail-open rule and the 409 merge are here.
7
+ *
8
+ * No runtime imports beyond sibling pure modules: the tests run it in Node.
9
+ */
10
+ import type { GuideSuite } from "./types";
11
+ import { type GuideProgress, type GuidesAnswer } from "./progress";
12
+ /** What a progress call is about. */
13
+ export interface GuideProgressRequest {
14
+ /** The suite's app ("peoplx", "liff"). */
15
+ app: string;
16
+ /** The suite's key, the row on the server ("liff.first-run"). */
17
+ guideKey: string;
18
+ }
19
+ /** An HTTP answer as the adapter got it. */
20
+ export interface GuideProgressResponse {
21
+ /** The HTTP status. */
22
+ status: number;
23
+ /** The parsed JSON body (the API's envelope, or the row itself). */
24
+ data: unknown;
25
+ }
26
+ /**
27
+ * How the engine reaches the progress API. Both calls carry the user's
28
+ * credentials and the organisation, as the app's own client does.
29
+ *
30
+ * - `fetchProgress` answers what `GET …/guides/me?app=` answers (API.md §3):
31
+ * the envelope `{ data: { app, mode, audience, guides } }` or its `data`.
32
+ * Throw, or return anything else, and the tour does not show.
33
+ * - `saveProgress` sends `body` as the whole row (API.md §4) and returns the
34
+ * status and parsed body — a 409 too, with its `current`, so the engine
35
+ * merges and retries. A client that throws on non-2xx may instead throw
36
+ * `new GuideProgressConflict(current)` (or anything with `status: 409` and
37
+ * `current`): it is read as the same 409. Throw anything else (or return
38
+ * another status) on any other failure: the tour goes on and the session
39
+ * copy keeps the place.
40
+ */
41
+ export interface GuideProgressAdapter {
42
+ fetchProgress(req: GuideProgressRequest & {
43
+ signal?: AbortSignal;
44
+ }): Promise<unknown>;
45
+ saveProgress(req: GuideProgressRequest & {
46
+ body: Record<string, unknown>;
47
+ }): Promise<GuideProgressResponse>;
48
+ }
49
+ /**
50
+ * A 409 from the progress API, for a client that throws on non-2xx: throw
51
+ * it from `saveProgress` with the answer's `current` (the stored row, or
52
+ * null) and the engine merges as if `{ status: 409, data: { current } }` had
53
+ * been returned.
54
+ */
55
+ export declare class GuideProgressConflict extends Error {
56
+ readonly current: unknown;
57
+ readonly code: string;
58
+ readonly status = 409;
59
+ constructor(current: unknown, code?: string);
60
+ }
61
+ /** The GET, parsed for this suite; null (show nothing) on any failure or odd answer. */
62
+ export declare function loadGuides(adapter: GuideProgressAdapter, suite: GuideSuite, signal?: AbortSignal): Promise<GuidesAnswer | null>;
63
+ /**
64
+ * PUT the whole state. On 409 the server's row comes back: merge (keeping
65
+ * whatever is further along) and try once more with its rev. Returns the
66
+ * stored row, or null when it could not be saved.
67
+ */
68
+ export declare function storeGuides(adapter: GuideProgressAdapter, suite: GuideSuite, progress: GuideProgress): Promise<GuideProgress | null>;
@@ -10,7 +10,9 @@
10
10
  * The full guide for tour authors is _claude-work/peoplx-guide/ENGINE-API.md.
11
11
  */
12
12
  /** Which app a tour belongs to (the same ids as the help channel). */
13
- export type GuideAppId = "platform" | "accounx" | "invenx" | "plannex" | "peoplx" | "sellx" | "carex";
13
+ export type GuideAppId = "platform" | "accounx" | "invenx" | "plannex" | "peoplx" | "sellx" | "carex"
14
+ /** The employee app inside LINE (erp-liff), through `guide-core`. */
15
+ | "liff";
14
16
  /**
15
17
  * A `data-guide` key, "<app>.<area>.<element>" — "peoplx.shift.edit".
16
18
  * The shell's own: "nav.<menu item id>", "shell.menu-toggle", "shell.help".
@@ -48,7 +50,8 @@ export type GuideText = string | ((ctx: GuideContext) => string);
48
50
  */
49
51
  export type GuideTrigger =
50
52
  /** The page is this route. Next-style pattern; an optional `?k=v` must be
51
- * in the query. `/[code]/setting?tab=department` */
53
+ * in the query. `/[code]/setting?tab=department`. `[[x]]` is a segment
54
+ * that may be absent: `/me/[[org]]/attendance` (shell ≥ 0.1.68). */
52
55
  {
53
56
  route: string;
54
57
  }
@@ -180,6 +183,17 @@ export interface GuideStep {
180
183
  * or Escape to the open dialog. Overrides the mission's `abort`.
181
184
  */
182
185
  abort?: GuideAbortAction | GuideAbortAction[];
186
+ /**
187
+ * "commit" steps whose button only shows after a change (a save bar that
188
+ * appears once the form is edited). When `target` is not on the page but
189
+ * `nothingToSave.target` is (what is saved is on screen and nothing is
190
+ * changed), the step is a read step on that anchor, with ถัดไป, which moves
191
+ * on as if saved. The moment `target` shows (the user changed something)
192
+ * it is the commit step again. Name an anchor that only exists when the
193
+ * saved state is real, so a first run never passes without a save that
194
+ * was needed (shell ≥ 0.1.67).
195
+ */
196
+ nothingToSave?: GuideNothingToSave;
183
197
  /**
184
198
  * "commit" steps: what a refused save means. Return the key of the step to
185
199
  * go back to (a field) and/or the line to show. Default: stay, show the
@@ -190,10 +204,20 @@ export interface GuideStep {
190
204
  message?: string;
191
205
  } | void;
192
206
  }
207
+ /** A commit step's read form when there is nothing to save (GuideStep.nothingToSave). */
208
+ export interface GuideNothingToSave {
209
+ /** What is saved, on screen (the placed point's card, the saved list). */
210
+ target: GuideAnchorKey;
211
+ title: GuideText;
212
+ why?: GuideText;
213
+ /** Anchors the user may use meanwhile; a change there brings the save back. */
214
+ allow?: GuideAnchorKey[];
215
+ }
193
216
  /**
194
217
  * One thing the engine does to put the page back when a replay ends early.
195
218
  * Run in order, after the tour has let go of the page; an anchor that is not
196
- * on the page is passed over.
219
+ * on the page is passed over, and so is one that was already on the page
220
+ * when the replay started (the user's own open form: shell ≥ 0.1.67).
197
221
  */
198
222
  export type GuideAbortAction =
199
223
  /** Press this anchor (the form's ยกเลิก, a dialog's close). */
@@ -242,6 +266,24 @@ export interface GuideMission {
242
266
  * "ดูวิธีทำ" (a replay). undefined = not known yet.
243
267
  */
244
268
  alreadyDone?: (ctx: GuideContext) => boolean | undefined;
269
+ /**
270
+ * Whether this user has the mission at all (a menu the org lacks, a plan
271
+ * without LINE). A hidden mission is not drawn in the panel or the welcome,
272
+ * does not count in n/N and is never "next". Without it the mission shows;
273
+ * with it, only `true` shows it: undefined (not known yet) and a throw hide
274
+ * it, so a mission the user may not have never flashes in. A hidden
275
+ * mission named in another's `after` does not lock it (shell ≥ 0.1.68).
276
+ */
277
+ visible?: (ctx: GuideContext) => boolean | undefined;
278
+ /**
279
+ * Done without the tour: while the engine is mounted (not only inside a
280
+ * step), this `{ api }` or `{ event }` trigger marks the mission done with
281
+ * the ordinary save, and its celebration shows at the next idle moment,
282
+ * never in the middle of a step. Other trigger kinds are ignored here.
283
+ * E.g. the first real punch: `{ api: { method: "POST", path:
284
+ * "/public/line/me/attendance/punch" } }` (shell ≥ 0.1.68).
285
+ */
286
+ completeOn?: GuideTrigger | GuideTrigger[];
245
287
  /** The celebration. Title defaults to "<title>แล้ว". */
246
288
  done?: {
247
289
  title?: GuideText;
@@ -251,6 +293,9 @@ export interface GuideMission {
251
293
  * How to put the page back when a replay of this mission ends without
252
294
  * saving (a step's own `abort` wins). Default: Escape, and only from a step
253
295
  * that is not a checkpoint (inside the dialog or form the tour opened).
296
+ * Whatever was already open when the replay started is the user's and is
297
+ * left alone: an anchor on the page then is never pressed, and no Escape
298
+ * is sent when an app dialog was open (shell ≥ 0.1.67).
254
299
  */
255
300
  abort?: GuideAbortAction | GuideAbortAction[];
256
301
  steps: GuideStep[];
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var guideRuntime = require('./chunks/guide-runtime-B2RjoQeb.cjs');
5
+ require('react/jsx-runtime');
6
+ require('react');
7
+ require('react-dom');
8
+ require('@floating-ui/dom');
9
+
10
+
11
+
12
+ exports.GUIDE_API_EVENT = guideRuntime.API_EVENT;
13
+ exports.GUIDE_EVENT_PREFIX = guideRuntime.GUIDE_EVENT_PREFIX;
14
+ exports.GUIDE_LAYER_ATTRIBUTE = guideRuntime.GUIDE_LAYER_ATTRIBUTE;
15
+ exports.GuideProgressConflict = guideRuntime.GuideProgressConflict;
16
+ exports.GuideRuntime = guideRuntime.GuideRuntime;
17
+ exports.apiCallDetail = guideRuntime.apiCallDetail;
18
+ exports.emitGuideEvent = guideRuntime.emitGuideEvent;
19
+ exports.fillRoute = guideRuntime.fillRoute;
20
+ exports.guideAnchor = guideRuntime.guideAnchor;
21
+ exports.loadGuides = guideRuntime.loadGuides;
22
+ exports.matchApiPath = guideRuntime.matchApiPath;
23
+ exports.matchRoute = guideRuntime.matchRoute;
24
+ exports.openGuidePanel = guideRuntime.openGuidePanel;
25
+ exports.reportApiCall = guideRuntime.reportApiCall;
26
+ exports.reportApiEvent = guideRuntime.reportApiEvent;
27
+ exports.storeGuides = guideRuntime.storeGuides;
28
+ exports.useGuideEntry = guideRuntime.useGuideEntry;
29
+ //# sourceMappingURL=guide-core.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guide-core.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ export { A as GUIDE_API_EVENT, G as GUIDE_EVENT_PREFIX, a as GUIDE_LAYER_ATTRIBUTE, c as GuideProgressConflict, d as GuideRuntime, e as apiCallDetail, f as emitGuideEvent, g as fillRoute, h as guideAnchor, l as loadGuides, m as matchApiPath, i as matchRoute, o as openGuidePanel, r as reportApiCall, j as reportApiEvent, s as storeGuides, u as useGuideEntry } from './chunks/guide-runtime-BT7rN-5p.esm.js';
3
+ import 'react/jsx-runtime';
4
+ import 'react';
5
+ import 'react-dom';
6
+ import '@floating-ui/dom';
7
+ //# sourceMappingURL=guide-core.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guide-core.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;"}