@eintrek/erp-shell 0.1.67 → 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";
@@ -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";
@@ -71,6 +71,11 @@ export interface GuideState {
71
71
  * are theirs, and ending the replay leaves them alone (see pushAbort).
72
72
  */
73
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[];
74
79
  }
75
80
  export interface ReplayPage {
76
81
  mission: string;
@@ -183,6 +188,15 @@ export type GuideAction = {
183
188
  } | {
184
189
  type: "serverRow";
185
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";
186
200
  };
187
201
  export type GuideEffect = {
188
202
  type: "save";
@@ -224,6 +238,16 @@ export declare const COPY: {
224
238
  export declare const BRIDGE_TARGET = "shell.menu-toggle";
225
239
  export declare function initialGuideState(audience: GuideAudience, progress: GuideProgress): GuideState;
226
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;
227
251
  export declare function guideContext(env: GuideEnv, data: Readonly<Record<string, string>>, params?: RouteParams): GuideContext;
228
252
  export declare function resolveText(t: GuideText | undefined, ctx: GuideContext): string;
229
253
  export declare function triggersOf(step: GuideStep): GuideTrigger[];
@@ -266,6 +290,7 @@ export declare function abortActionsLeft(actions: GuideAbortAction[], before: Re
266
290
  /** Status of each mission as the panel shows it. */
267
291
  export type MissionView = "todo" | "next" | "active" | "skipped" | "done" | "already" | "locked" | "waiting";
268
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). */
269
294
  export declare function settledCount(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): number;
270
295
  export declare function gateState(suite: GuideSuite, ctx: GuideContext): boolean | undefined;
271
296
  export declare function missionViews(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): Array<{
@@ -275,10 +300,16 @@ export declare function missionViews(suite: GuideSuite, progress: GuideProgress,
275
300
  }>;
276
301
  /** The mission the celebration offers next: the first one not settled and not locked. */
277
302
  export declare function nextMissionKey(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): string | null;
278
- /** Whether the welcome should open by itself. */
279
- 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;
280
305
  /** Whether the floating "ภารกิจ n/N" chip shows. */
281
306
  export declare function chipVisible(state: GuideState, suite: GuideSuite, ctx: GuideContext): boolean;
282
307
  /** "กด [บันทึก]" → "กด บันทึก" (for screen readers). */
283
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[];
284
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
  }
@@ -263,6 +266,24 @@ export interface GuideMission {
263
266
  * "ดูวิธีทำ" (a replay). undefined = not known yet.
264
267
  */
265
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[];
266
287
  /** The celebration. Title defaults to "<title>แล้ว". */
267
288
  done?: {
268
289
  title?: GuideText;
@@ -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":";;;;;"}