@eintrek/erp-shell 0.1.62 → 0.1.64
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.
- package/README.md +32 -0
- package/dist/guide/dom.d.ts +103 -0
- package/dist/guide/events.d.ts +24 -0
- package/dist/guide/guide-host.d.ts +7 -0
- package/dist/guide/guide-store.d.ts +16 -0
- package/dist/guide/guide-styles.d.ts +7 -0
- package/dist/guide/guide-ui.d.ts +191 -0
- package/dist/guide/machine.d.ts +237 -0
- package/dist/guide/patterns.d.ts +28 -0
- package/dist/guide/progress-client.d.ts +11 -0
- package/dist/guide/progress.d.ts +81 -0
- package/dist/guide/types.d.ts +288 -0
- package/dist/help/help-button.d.ts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +3353 -147
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3357 -142
- package/dist/index.js.map +1 -1
- package/dist/navigation/app-layout-shell.d.ts +8 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -144,6 +144,36 @@ Phase 3a ✅ — Pure / session-only hooks + jwt-utils
|
|
|
144
144
|
Consumer apps' `@/shared/lib/jwt-utils`, `@/shared/hooks/use-session-refresh`,
|
|
145
145
|
`@/shared/hooks/use-keycloak-roles` shims now re-export from this package.
|
|
146
146
|
|
|
147
|
+
## Guided tour (ไกด์เริ่มต้น), from 0.1.63
|
|
148
|
+
|
|
149
|
+
A forced, game-style spotlight tour, written by each app as data and run by
|
|
150
|
+
the shell. Authors' guide: `_claude-work/peoplx-guide/ENGINE-API.md`; the
|
|
151
|
+
progress API: `API.md` next to it.
|
|
152
|
+
|
|
153
|
+
- `AppLayoutShell guides={{ suite, facts, can, ready }}` mounts `GuideHost`
|
|
154
|
+
once the URL's organisation is active (never on the not-member refusal).
|
|
155
|
+
Who sees it (forced / optional / none) comes from platform-api
|
|
156
|
+
`GET /organizations/:id/guides/me?app=`; any failure shows nothing.
|
|
157
|
+
- Steps advance on outcomes: a route, an anchor appearing or going away, an
|
|
158
|
+
API answer (the shell's axios reports `erp:api`; an app's own instance calls
|
|
159
|
+
`reportApiEvent`), an app event (`emitGuideEvent` → `erp:guide:<name>`), a
|
|
160
|
+
press, a valid value.
|
|
161
|
+
- Anchors: `guideAnchor("peoplx.x.y")` → `data-guide`. The sidebar anchors
|
|
162
|
+
every row as `nav.<item id>`; the menu button is `shell.menu-toggle`, the
|
|
163
|
+
"?" button `shell.help`.
|
|
164
|
+
- The "?" button becomes a menu ("ไกด์เริ่มต้น n/N", "ติดต่อทีมช่วยเหลือ")
|
|
165
|
+
for a user with a tour.
|
|
166
|
+
- The layer is a portal marked `data-erp-guide-layer` with its own
|
|
167
|
+
stylesheet (`eg-` classes, theme tokens). Radix overlays must ignore it:
|
|
168
|
+
`@eintrek/erp-theme` ≥ 1.4.21 does; the engine also stops its own presses
|
|
169
|
+
and focus from reaching an open dialog.
|
|
170
|
+
- New dependency: `@floating-ui/dom` (external in the bundle; Radix already
|
|
171
|
+
installs it in every app).
|
|
172
|
+
- Code: `src/guide/` — `types.ts` (definitions), `machine.ts` (pure state
|
|
173
|
+
machine), `patterns.ts`, `progress.ts` / `progress-client.ts` (API),
|
|
174
|
+
`dom.ts`, `guide-host.tsx`, `guide-ui.tsx`, `guide-styles.ts`. Tests:
|
|
175
|
+
`src/guide/*.test.ts`.
|
|
176
|
+
|
|
147
177
|
## Roadmap
|
|
148
178
|
|
|
149
179
|
| Phase | Scope | Files to move from each app |
|
|
@@ -170,6 +200,8 @@ Requires Node 22.13+ and pnpm 11 (same as other ERP packages).
|
|
|
170
200
|
```bash
|
|
171
201
|
pnpm install
|
|
172
202
|
pnpm build # rollup → dist/{index.js,index.esm.js,index.d.ts}
|
|
203
|
+
pnpm test # node --test (pnpm 11 may refuse to run scripts over
|
|
204
|
+
# ignored builds; then: node --test "src/**/*.test.ts")
|
|
173
205
|
pnpm dev # watch
|
|
174
206
|
pnpm pack # produce .tgz for local consumer testing
|
|
175
207
|
```
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export interface Rect {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function anchorSelector(anchor: string, id?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* On screen: connected, with a size, not hidden by style, not inside
|
|
10
|
+
* aria-hidden or inert (other than the guide's own), not a closed Radix part.
|
|
11
|
+
*
|
|
12
|
+
* aria-hidden that Radix put there (data-aria-hidden: everything outside an
|
|
13
|
+
* open dialog, select or menu is hidden from screen readers) does not count:
|
|
14
|
+
* a dialog's field is still on screen while its select's list is open.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPresent(el: Element | null | undefined): el is HTMLElement;
|
|
17
|
+
/** The first matching anchor that is on screen. */
|
|
18
|
+
export declare function findAnchor(anchor: string, id?: string): HTMLElement | null;
|
|
19
|
+
export declare function anchorVisible(anchor: string, id?: string): boolean;
|
|
20
|
+
/** The field to type into: the anchor itself, or the first field inside it. */
|
|
21
|
+
export declare function fieldOf(el: Element | null): HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | HTMLElement | null;
|
|
22
|
+
export declare function fieldValue(el: Element | null): string;
|
|
23
|
+
export declare const FOCUSABLE = "a[href],button:not([disabled]),input:not([disabled]):not([type=hidden]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1']),[contenteditable=''],[contenteditable=true]";
|
|
24
|
+
export declare function focusablesIn(root: Element | null): HTMLElement[];
|
|
25
|
+
export declare function rectOf(el: Element): Rect;
|
|
26
|
+
export declare function padRect(r: Rect, pad: number): Rect;
|
|
27
|
+
export declare function sameRect(a: Rect | null, b: Rect | null): boolean;
|
|
28
|
+
/** Open Radix poppers (a select's list, a menu): usable while a step runs. */
|
|
29
|
+
export declare const POPPER_SELECTOR = "[data-radix-popper-content-wrapper]";
|
|
30
|
+
/** Toasts stay usable above the tour. */
|
|
31
|
+
export declare const TOASTER_SELECTOR = "[data-sonner-toaster]";
|
|
32
|
+
/** Open dialogs (Radix dialog, alert dialog, sheet, popover content). */
|
|
33
|
+
export declare const OPEN_DIALOG_SELECTOR = "[role=\"dialog\"][data-state=\"open\"],[role=\"alertdialog\"][data-state=\"open\"]";
|
|
34
|
+
/**
|
|
35
|
+
* Whether an open dialog of the app's covers the target: the target is
|
|
36
|
+
* neither in it nor around it (a confirm opened by the step's own button).
|
|
37
|
+
* The ring and the hand would be drawn over the dialog, pointing at a button
|
|
38
|
+
* behind it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function coveredByDialog(el: Element | null): boolean;
|
|
41
|
+
export declare function openPoppers(): HTMLElement[];
|
|
42
|
+
export declare function inGuideLayer(node: EventTarget | null): boolean;
|
|
43
|
+
export declare function prefersReducedMotion(): boolean;
|
|
44
|
+
export declare function isPhoneWidth(): boolean;
|
|
45
|
+
/** The overlap of two rectangles, or null when they do not meet. */
|
|
46
|
+
export declare function intersectRect(a: Rect, b: Rect): Rect | null;
|
|
47
|
+
/** The part of `rect` inside every clip (scroll boxes, the viewport); null when none is. */
|
|
48
|
+
export declare function visibleRect(rect: Rect, clips: Rect[]): Rect | null;
|
|
49
|
+
/**
|
|
50
|
+
* The ancestors that cut the element off: anything with overflow other than
|
|
51
|
+
* visible on an axis. The body and the root scroll the page and are covered by
|
|
52
|
+
* the viewport. Read once per target; their rectangles are read every frame.
|
|
53
|
+
*/
|
|
54
|
+
export declare function clipAncestors(el: Element): Array<{
|
|
55
|
+
el: Element;
|
|
56
|
+
x: boolean;
|
|
57
|
+
y: boolean;
|
|
58
|
+
}>;
|
|
59
|
+
/** An ancestor's clip as a rectangle: unbounded on an axis it does not clip. */
|
|
60
|
+
export declare function clipRectOf(c: {
|
|
61
|
+
el: Element;
|
|
62
|
+
x: boolean;
|
|
63
|
+
y: boolean;
|
|
64
|
+
}): Rect;
|
|
65
|
+
export declare function viewportRect(): Rect;
|
|
66
|
+
/**
|
|
67
|
+
* The part of the element a person can see: its box cut by every scroll box
|
|
68
|
+
* around it and by the viewport. The spotlight, its hole, the ring and the
|
|
69
|
+
* hand use this, so a tab half out of a sideways-scrolling row of tabs is not
|
|
70
|
+
* spotlit where it cannot be pressed. Null when none of it is on screen.
|
|
71
|
+
*/
|
|
72
|
+
export declare function visibleRectOf(el: Element, ancestors?: {
|
|
73
|
+
el: Element;
|
|
74
|
+
x: boolean;
|
|
75
|
+
y: boolean;
|
|
76
|
+
}[]): Rect | null;
|
|
77
|
+
/** Inside a box that scrolls sideways (with somewhere to scroll) that is itself on screen. */
|
|
78
|
+
export declare function inSidewaysScroller(el: Element): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* How to bring a target into view, or null when it already is: to the middle
|
|
81
|
+
* when it is above or below the comfortable band (under the header, at the
|
|
82
|
+
* bottom edge), the nearest edge when only a side is cut off (a tab out of a
|
|
83
|
+
* sideways-scrolling row, on a phone).
|
|
84
|
+
*/
|
|
85
|
+
export declare function revealPlan(rect: Rect, visible: Rect | null, viewportHeight: number): {
|
|
86
|
+
block: "center" | "nearest";
|
|
87
|
+
inline: "nearest";
|
|
88
|
+
} | null;
|
|
89
|
+
/** Scrolls the target into view (instant under reduced motion). */
|
|
90
|
+
export declare function revealTarget(el: Element): void;
|
|
91
|
+
/**
|
|
92
|
+
* The scrim's blocking area: the screen (with a 10 px margin) minus the union
|
|
93
|
+
* of the holes, as rectangles that do not overlap. Cutting each hole out with
|
|
94
|
+
* an even-odd fill made the place where two holes overlap (a field and the
|
|
95
|
+
* select list open over it) solid again, and the option there could not be
|
|
96
|
+
* pressed.
|
|
97
|
+
*
|
|
98
|
+
* Every hole edge splits the screen into a grid; a cell is blocked when no
|
|
99
|
+
* hole covers it. Cells in a row are joined, and identical rows stacked.
|
|
100
|
+
*/
|
|
101
|
+
export declare function blockedRects(holes: Rect[], width: number, height: number): Rect[];
|
|
102
|
+
/** Whether a point is in any of the rectangles (edges excluded). */
|
|
103
|
+
export declare function inRects(rects: Rect[], x: number, y: number): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const GUIDE_ATTRIBUTE = "data-guide";
|
|
2
|
+
export declare const GUIDE_ID_ATTRIBUTE = "data-guide-id";
|
|
3
|
+
export declare const GUIDE_LAYER_ATTRIBUTE = "data-erp-guide-layer";
|
|
4
|
+
export declare const API_EVENT = "erp:api";
|
|
5
|
+
export declare const GUIDE_EVENT_PREFIX = "erp:guide:";
|
|
6
|
+
export interface GuideAnchorProps {
|
|
7
|
+
"data-guide": string;
|
|
8
|
+
"data-guide-id"?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Spread onto the element a tour step points at. */
|
|
11
|
+
export declare function guideAnchor(key: string, id?: string | number | null): GuideAnchorProps;
|
|
12
|
+
export interface ApiEventDetail {
|
|
13
|
+
method: string;
|
|
14
|
+
path: string;
|
|
15
|
+
status: number;
|
|
16
|
+
id?: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Announces an app event a step can wait for (`advanceOn: { event: name }`). */
|
|
20
|
+
export declare function emitGuideEvent(name: string, detail?: Record<string, string | number>): void;
|
|
21
|
+
/** The bus detail for an axios answer or error; null when it is neither. */
|
|
22
|
+
export declare function apiEventDetail(responseOrError: unknown): ApiEventDetail | null;
|
|
23
|
+
/** Puts an axios answer (or error) on the bus. Safe to call with anything. */
|
|
24
|
+
export declare function reportApiEvent(responseOrError: unknown): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the rest of the shell needs to know about the tour, outside the
|
|
3
|
+
* host's tree: whether this user has one here (the "?" menu's
|
|
4
|
+
* "ไกด์เริ่มต้น n/N" entry) and a way to open its panel. A tiny external
|
|
5
|
+
* store, like the help dialog's.
|
|
6
|
+
*/
|
|
7
|
+
export interface GuideEntry {
|
|
8
|
+
done: number;
|
|
9
|
+
total: number;
|
|
10
|
+
}
|
|
11
|
+
/** Called by the host: the entry to show (null: none), and how to open the panel. */
|
|
12
|
+
export declare function publishGuideEntry(next: GuideEntry | null, openPanel: (() => void) | null): void;
|
|
13
|
+
/** Opens the tour's quest panel, when this user has a tour here. */
|
|
14
|
+
export declare function openGuidePanel(): void;
|
|
15
|
+
/** The "?" menu's entry, or null when there is no tour for this user. */
|
|
16
|
+
export declare function useGuideEntry(): GuideEntry | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tour's stylesheet. The shell has no CSS pipeline and the apps only scan
|
|
3
|
+
* its dist for Tailwind classes, which cannot carry keyframes; so the layer
|
|
4
|
+
* brings its own sheet, namespaced `eg-`, drawn from the theme's tokens
|
|
5
|
+
* (--primary carries each app's brand accent; light and dark follow).
|
|
6
|
+
*/
|
|
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";
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { type Placement } from "@floating-ui/dom";
|
|
2
|
+
import { type Rect } from "./dom";
|
|
3
|
+
import type { MissionView, StepError } from "./machine";
|
|
4
|
+
/** "26 ต.ค. 2569 13:04" — day, month, Buddhist year, 24 h. */
|
|
5
|
+
export declare function formatThaiDateTime(iso: string | null | undefined): string;
|
|
6
|
+
/** "กด [บันทึก]": the bracketed word is a chip in the brand colour. */
|
|
7
|
+
export declare function GuideTitle({ text }: {
|
|
8
|
+
text: string;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare const IconCheck: () => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare const IconX: () => import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export declare const IconFlag: () => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare const IconTrophy: () => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* The dim layer that eats every press outside the holes. The holes are a
|
|
16
|
+
* union: where two overlap (a field and the select list open over it) the
|
|
17
|
+
* page stays open.
|
|
18
|
+
*
|
|
19
|
+
* - The look: one rectangle masked by the rounded holes (a mask adds holes
|
|
20
|
+
* up, so an overlap is not filled back in).
|
|
21
|
+
* - The presses: a transparent path made of the screen minus the holes, as
|
|
22
|
+
* rectangles that do not overlap (blockedRects). A press inside a hole
|
|
23
|
+
* reaches the page underneath; any other lands on the path (marked
|
|
24
|
+
* data-eg-block) and is cancelled.
|
|
25
|
+
*/
|
|
26
|
+
export declare function Scrim({ holes, width, height }: {
|
|
27
|
+
holes: Rect[];
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
31
|
+
/** The smallest rectangle around all of them. */
|
|
32
|
+
export declare function boundingRect(rs: Rect[]): Rect;
|
|
33
|
+
/** Where the bubble may go, in order of preference. */
|
|
34
|
+
export declare const BUBBLE_PLACEMENTS: Placement[];
|
|
35
|
+
/**
|
|
36
|
+
* The first placement (in BUBBLE_PLACEMENTS order) whose bubble is on screen,
|
|
37
|
+
* off the target (with its open list), and off everything to keep clear;
|
|
38
|
+
* failing that, the first that is only on screen and off the target; -1 when
|
|
39
|
+
* none is.
|
|
40
|
+
*/
|
|
41
|
+
export declare function pickPlacement(results: Array<{
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
}>, bw: number, bh: number, anchor: Rect, clear: Rect[], bounds: Rect): number;
|
|
45
|
+
export interface BubbleModel {
|
|
46
|
+
missionNo: number;
|
|
47
|
+
missionTitle: string;
|
|
48
|
+
replay: boolean;
|
|
49
|
+
index: number;
|
|
50
|
+
total: number;
|
|
51
|
+
title: string;
|
|
52
|
+
why: string;
|
|
53
|
+
error: StepError | null;
|
|
54
|
+
showNext: boolean;
|
|
55
|
+
showEndWithoutSave: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface StepLayerProps {
|
|
58
|
+
/** The spotlight (padded target), or null while there is none to show. */
|
|
59
|
+
spot: Rect | null;
|
|
60
|
+
/** Other holes: the anchors the step also allows. */
|
|
61
|
+
holes: Rect[];
|
|
62
|
+
/**
|
|
63
|
+
* Open popper lists (a select's options, a menu): holes too. While one is
|
|
64
|
+
* open the hand is not drawn (the choice is in the list now), and the ring
|
|
65
|
+
* is not drawn over a list that covers the target.
|
|
66
|
+
*/
|
|
67
|
+
popovers?: Rect[];
|
|
68
|
+
/**
|
|
69
|
+
* An open dialog of the app's covers the target (a confirm its button
|
|
70
|
+
* opened): no ring, no hand and no hole there; the bubble stays.
|
|
71
|
+
*/
|
|
72
|
+
covered?: boolean;
|
|
73
|
+
/** False for a "view" step with nothing to point at: the bubble is centred. */
|
|
74
|
+
hasTarget: boolean;
|
|
75
|
+
viewport: {
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
offsetTop: number;
|
|
79
|
+
};
|
|
80
|
+
phone: boolean;
|
|
81
|
+
showHand: boolean;
|
|
82
|
+
bubble: BubbleModel;
|
|
83
|
+
phase: "waiting" | "ready" | "missing" | "away";
|
|
84
|
+
confirmExit: boolean;
|
|
85
|
+
nudge: number;
|
|
86
|
+
/** Animate the ring to its new place (a new step). */
|
|
87
|
+
moving: boolean;
|
|
88
|
+
onExit: () => void;
|
|
89
|
+
onNext: () => void;
|
|
90
|
+
onEndWithoutSave: () => void;
|
|
91
|
+
onRetry: () => void;
|
|
92
|
+
onGoResume: () => void;
|
|
93
|
+
onExitConfirm: () => void;
|
|
94
|
+
onExitCancel: () => void;
|
|
95
|
+
}
|
|
96
|
+
export declare const BUBBLE_TITLE_ID = "eg-bubble-title";
|
|
97
|
+
export declare const BUBBLE_WHY_ID = "eg-bubble-why";
|
|
98
|
+
export declare function StepLayer(p: StepLayerProps): import("react/jsx-runtime").JSX.Element;
|
|
99
|
+
/** The fingertip at ≈62% across and down the target; upside down near the bottom. */
|
|
100
|
+
export declare function handPosition(spot: Rect, viewportHeight: number): {
|
|
101
|
+
left: number;
|
|
102
|
+
top: number;
|
|
103
|
+
flip: boolean;
|
|
104
|
+
};
|
|
105
|
+
export declare function ConfirmModal(p: {
|
|
106
|
+
title: string;
|
|
107
|
+
line: string;
|
|
108
|
+
leaveLabel: string;
|
|
109
|
+
stayLabel: string;
|
|
110
|
+
onLeave: () => void;
|
|
111
|
+
onStay: () => void;
|
|
112
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
113
|
+
export declare function WelcomeModal(p: {
|
|
114
|
+
title: string;
|
|
115
|
+
subtitle: string;
|
|
116
|
+
missions: Array<{
|
|
117
|
+
title: string;
|
|
118
|
+
minutes: number;
|
|
119
|
+
}>;
|
|
120
|
+
confirm: boolean;
|
|
121
|
+
onStart: () => void;
|
|
122
|
+
onSkip: () => void;
|
|
123
|
+
onSkipConfirm: () => void;
|
|
124
|
+
onSkipCancel: () => void;
|
|
125
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
126
|
+
export declare function GateModal(p: {
|
|
127
|
+
title: string;
|
|
128
|
+
subtitle?: string;
|
|
129
|
+
actionLabel: string;
|
|
130
|
+
href: string;
|
|
131
|
+
onLater: () => void;
|
|
132
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
133
|
+
export declare function CelebrateModal(p: {
|
|
134
|
+
missionNo: number;
|
|
135
|
+
title: string;
|
|
136
|
+
subtitle: string;
|
|
137
|
+
done: number;
|
|
138
|
+
total: number;
|
|
139
|
+
nextTitle: string | null;
|
|
140
|
+
reduced: boolean;
|
|
141
|
+
onNext: () => void;
|
|
142
|
+
onRest: () => void;
|
|
143
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
144
|
+
export declare function CompleteModal(p: {
|
|
145
|
+
total: number;
|
|
146
|
+
title: string;
|
|
147
|
+
subtitle: string;
|
|
148
|
+
missions: string[];
|
|
149
|
+
actionLabel: string;
|
|
150
|
+
secondary: {
|
|
151
|
+
label: string;
|
|
152
|
+
href: string;
|
|
153
|
+
} | null;
|
|
154
|
+
reduced: boolean;
|
|
155
|
+
onFinish: () => void;
|
|
156
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
157
|
+
export declare function QuestChip({ done, total, pulse, onOpen }: {
|
|
158
|
+
done: number;
|
|
159
|
+
total: number;
|
|
160
|
+
pulse: number;
|
|
161
|
+
onOpen: () => void;
|
|
162
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
163
|
+
export interface QuestItem {
|
|
164
|
+
key: string;
|
|
165
|
+
title: string;
|
|
166
|
+
minutes: number;
|
|
167
|
+
view: MissionView;
|
|
168
|
+
optional: boolean;
|
|
169
|
+
completedAt?: string | null;
|
|
170
|
+
/** For "locked": the missions to do first. */
|
|
171
|
+
lockedBy?: string;
|
|
172
|
+
}
|
|
173
|
+
export declare function QuestPanel(p: {
|
|
174
|
+
items: QuestItem[];
|
|
175
|
+
done: number;
|
|
176
|
+
total: number;
|
|
177
|
+
justDone: string | null;
|
|
178
|
+
gate: {
|
|
179
|
+
label: string;
|
|
180
|
+
pending: string;
|
|
181
|
+
href: string;
|
|
182
|
+
} | null;
|
|
183
|
+
canHideChip: boolean;
|
|
184
|
+
onClose: () => void;
|
|
185
|
+
onStart: (key: string, replay: boolean) => void;
|
|
186
|
+
onDismiss: (key: string) => void;
|
|
187
|
+
onHideChip: () => void;
|
|
188
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
189
|
+
export declare function GuideToast({ text }: {
|
|
190
|
+
text: string;
|
|
191
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tour's state machine: pure. `reduceGuide(state, action, suite, env)`
|
|
3
|
+
* returns the next state and the effects for the host to carry out (save,
|
|
4
|
+
* announce, toast, navigate). Everything the machine needs to know about the
|
|
5
|
+
* page comes in through `env`, so the tests drive it with plain objects.
|
|
6
|
+
*
|
|
7
|
+
* No React, no DOM: the tests run it in Node (through the test loader).
|
|
8
|
+
*/
|
|
9
|
+
import type { GuideContext, GuideMission, GuideStep, GuideSuite, GuideText, GuideTrigger } from "./types";
|
|
10
|
+
import { type RouteParams } from "./patterns";
|
|
11
|
+
import type { GuideAudience, GuideProgress, MissionEntry } from "./progress";
|
|
12
|
+
export type StepPhase =
|
|
13
|
+
/** Looking for the target (dim page, "กำลังโหลด…"). */
|
|
14
|
+
"waiting"
|
|
15
|
+
/** Target shown, spotlight on. */
|
|
16
|
+
| "ready"
|
|
17
|
+
/** Target never came: "ไม่พบส่วนนี้บนหน้า" with ลองอีกครั้ง. */
|
|
18
|
+
| "missing"
|
|
19
|
+
/** The step happens on another page: the "ทำต่อ" card. */
|
|
20
|
+
| "away";
|
|
21
|
+
export interface StepError {
|
|
22
|
+
title: string;
|
|
23
|
+
detail?: string;
|
|
24
|
+
}
|
|
25
|
+
export type GuideScreen = {
|
|
26
|
+
name: "idle";
|
|
27
|
+
} | {
|
|
28
|
+
name: "welcome";
|
|
29
|
+
confirm: boolean;
|
|
30
|
+
} | {
|
|
31
|
+
name: "gate";
|
|
32
|
+
} | {
|
|
33
|
+
name: "step";
|
|
34
|
+
mission: string;
|
|
35
|
+
index: number;
|
|
36
|
+
replay: boolean;
|
|
37
|
+
/** The phone/closed-sidebar "กดปุ่มเมนู" step before a nav.* target. */
|
|
38
|
+
bridge: boolean;
|
|
39
|
+
phase: StepPhase;
|
|
40
|
+
error: StepError | null;
|
|
41
|
+
confirmExit: boolean;
|
|
42
|
+
/** Timeouts so far on this step. */
|
|
43
|
+
misses: number;
|
|
44
|
+
/** A `hidden` trigger's anchor has been seen on this step, so its
|
|
45
|
+
* going away counts. */
|
|
46
|
+
armed: boolean;
|
|
47
|
+
} | {
|
|
48
|
+
name: "celebrate";
|
|
49
|
+
mission: string;
|
|
50
|
+
} | {
|
|
51
|
+
name: "complete";
|
|
52
|
+
};
|
|
53
|
+
export interface GuideState {
|
|
54
|
+
audience: GuideAudience;
|
|
55
|
+
progress: GuideProgress;
|
|
56
|
+
screen: GuideScreen;
|
|
57
|
+
panelOpen: boolean;
|
|
58
|
+
/** A mission whose tick should pop in the panel. */
|
|
59
|
+
justDone: string | null;
|
|
60
|
+
/** Bumped to make the chip pulse twice. */
|
|
61
|
+
chipPulse: number;
|
|
62
|
+
/** Bumped to shake the bubble (a press outside the hole). */
|
|
63
|
+
nudge: number;
|
|
64
|
+
/** The gate card was put away for this visit ("ไว้ทีหลัง"). */
|
|
65
|
+
gateDismissed: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** What the machine may ask about the page. */
|
|
68
|
+
export interface GuideEnv {
|
|
69
|
+
code: string;
|
|
70
|
+
pathname: string;
|
|
71
|
+
search: URLSearchParams;
|
|
72
|
+
facts: Readonly<Record<string, unknown>>;
|
|
73
|
+
can: (policyKey: string) => boolean;
|
|
74
|
+
/** An anchor (optionally one row by data-guide-id) is on screen. */
|
|
75
|
+
isVisible: (anchor: string, id?: string) => boolean;
|
|
76
|
+
/** The sidebar is open (docked or drawer). */
|
|
77
|
+
sidebarOpen: boolean;
|
|
78
|
+
/** The current value of the step target's field. */
|
|
79
|
+
targetValue: () => string;
|
|
80
|
+
}
|
|
81
|
+
export type GuideSignal = {
|
|
82
|
+
kind: "api";
|
|
83
|
+
method: string;
|
|
84
|
+
path: string;
|
|
85
|
+
status: number;
|
|
86
|
+
id?: string;
|
|
87
|
+
message?: string;
|
|
88
|
+
} | {
|
|
89
|
+
kind: "event";
|
|
90
|
+
name: string;
|
|
91
|
+
detail?: Record<string, string>;
|
|
92
|
+
} | {
|
|
93
|
+
kind: "activate";
|
|
94
|
+
} | {
|
|
95
|
+
kind: "value";
|
|
96
|
+
value: string;
|
|
97
|
+
}
|
|
98
|
+
/** The user typed in the step's field (clears an error). */
|
|
99
|
+
| {
|
|
100
|
+
kind: "typing";
|
|
101
|
+
};
|
|
102
|
+
export type GuideAction = {
|
|
103
|
+
type: "welcome";
|
|
104
|
+
} | {
|
|
105
|
+
type: "welcomeSkip";
|
|
106
|
+
} | {
|
|
107
|
+
type: "welcomeSkipCancel";
|
|
108
|
+
} | {
|
|
109
|
+
type: "welcomeSkipConfirm";
|
|
110
|
+
}
|
|
111
|
+
/** "เริ่มภารกิจแรก" (and "เริ่มเลย" on the skip confirm). */
|
|
112
|
+
| {
|
|
113
|
+
type: "welcomeStart";
|
|
114
|
+
} | {
|
|
115
|
+
type: "gateLater";
|
|
116
|
+
} | {
|
|
117
|
+
type: "start";
|
|
118
|
+
mission: string;
|
|
119
|
+
replay?: boolean;
|
|
120
|
+
} | {
|
|
121
|
+
type: "resume";
|
|
122
|
+
}
|
|
123
|
+
/** Route, DOM or sidebar changed: re-evaluate the current step. */
|
|
124
|
+
| {
|
|
125
|
+
type: "sync";
|
|
126
|
+
} | {
|
|
127
|
+
type: "signal";
|
|
128
|
+
signal: GuideSignal;
|
|
129
|
+
}
|
|
130
|
+
/** ถัดไป, or Enter/Tab in an input step. */
|
|
131
|
+
| {
|
|
132
|
+
type: "next";
|
|
133
|
+
} | {
|
|
134
|
+
type: "targetLost";
|
|
135
|
+
} | {
|
|
136
|
+
type: "timeout";
|
|
137
|
+
} | {
|
|
138
|
+
type: "retry";
|
|
139
|
+
} | {
|
|
140
|
+
type: "blocked";
|
|
141
|
+
} | {
|
|
142
|
+
type: "exit";
|
|
143
|
+
} | {
|
|
144
|
+
type: "exitCancel";
|
|
145
|
+
} | {
|
|
146
|
+
type: "exitConfirm";
|
|
147
|
+
} | {
|
|
148
|
+
type: "endWithoutSave";
|
|
149
|
+
} | {
|
|
150
|
+
type: "goResume";
|
|
151
|
+
} | {
|
|
152
|
+
type: "celebrateNext";
|
|
153
|
+
} | {
|
|
154
|
+
type: "rest";
|
|
155
|
+
} | {
|
|
156
|
+
type: "finish";
|
|
157
|
+
} | {
|
|
158
|
+
type: "openPanel";
|
|
159
|
+
} | {
|
|
160
|
+
type: "closePanel";
|
|
161
|
+
} | {
|
|
162
|
+
type: "hideChip";
|
|
163
|
+
} | {
|
|
164
|
+
type: "dismissMission";
|
|
165
|
+
mission: string;
|
|
166
|
+
} | {
|
|
167
|
+
type: "serverRow";
|
|
168
|
+
progress: GuideProgress;
|
|
169
|
+
};
|
|
170
|
+
export type GuideEffect = {
|
|
171
|
+
type: "save";
|
|
172
|
+
immediate?: boolean;
|
|
173
|
+
} | {
|
|
174
|
+
type: "announce";
|
|
175
|
+
text: string;
|
|
176
|
+
} | {
|
|
177
|
+
type: "toast";
|
|
178
|
+
text: string;
|
|
179
|
+
} | {
|
|
180
|
+
type: "navigate";
|
|
181
|
+
href: string;
|
|
182
|
+
}
|
|
183
|
+
/** Close the app dialog the replay was in (Escape to Radix). */
|
|
184
|
+
| {
|
|
185
|
+
type: "closeAppDialog";
|
|
186
|
+
};
|
|
187
|
+
export interface GuideResult {
|
|
188
|
+
state: GuideState;
|
|
189
|
+
effects: GuideEffect[];
|
|
190
|
+
}
|
|
191
|
+
export declare const COPY: {
|
|
192
|
+
readonly fieldFix: "แก้ในช่องที่ไฮไลต์ แล้วกด ถัดไป";
|
|
193
|
+
readonly saveFailed: "บันทึกไม่สำเร็จ";
|
|
194
|
+
readonly blank: "ยังไม่ได้พิมพ์";
|
|
195
|
+
readonly exitToast: "ออกจากไกด์แล้ว";
|
|
196
|
+
readonly replayDone: "ทบทวนภารกิจเสร็จแล้ว";
|
|
197
|
+
readonly replayEnded: "จบการทบทวนแล้ว";
|
|
198
|
+
readonly hiddenToast: "ซ่อนแล้ว";
|
|
199
|
+
readonly aborted: "ไกด์ไปต่อไม่ได้ ลองอีกครั้งภายหลัง";
|
|
200
|
+
readonly bridgeTitle: "กดปุ่มเมนู";
|
|
201
|
+
readonly bridgeWhy: (app: string) => string;
|
|
202
|
+
};
|
|
203
|
+
export declare const BRIDGE_TARGET = "shell.menu-toggle";
|
|
204
|
+
export declare function initialGuideState(audience: GuideAudience, progress: GuideProgress): GuideState;
|
|
205
|
+
export declare function findMission(suite: GuideSuite, key: string | null | undefined): GuideMission | undefined;
|
|
206
|
+
export declare function guideContext(env: GuideEnv, data: Readonly<Record<string, string>>, params?: RouteParams): GuideContext;
|
|
207
|
+
export declare function resolveText(t: GuideText | undefined, ctx: GuideContext): string;
|
|
208
|
+
export declare function triggersOf(step: GuideStep): GuideTrigger[];
|
|
209
|
+
export declare function stepKind(step: GuideStep): import("./types").GuideStepKind;
|
|
210
|
+
/** The last checkpoint at or before `index` (a mission's first step always is one). */
|
|
211
|
+
export declare function checkpointAtOrBefore(steps: GuideStep[], index: number): number;
|
|
212
|
+
/** Whether the step's target (or the bridge's) is on screen. */
|
|
213
|
+
export declare function stepTargetVisible(step: GuideStep, env: GuideEnv, ctx: GuideContext, bridge: boolean): boolean;
|
|
214
|
+
/** The anchor the spotlight is on for the current step. */
|
|
215
|
+
export declare function spotlightTarget(step: GuideStep, ctx: GuideContext, bridge: boolean): {
|
|
216
|
+
anchor: string;
|
|
217
|
+
id?: string;
|
|
218
|
+
} | null;
|
|
219
|
+
/** Status of each mission as the panel shows it. */
|
|
220
|
+
export type MissionView = "todo" | "next" | "active" | "skipped" | "done" | "already" | "locked" | "waiting";
|
|
221
|
+
export declare function missionSettled(m: GuideMission, entry: MissionEntry | undefined, ctx: GuideContext): boolean;
|
|
222
|
+
export declare function settledCount(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): number;
|
|
223
|
+
export declare function gateState(suite: GuideSuite, ctx: GuideContext): boolean | undefined;
|
|
224
|
+
export declare function missionViews(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): Array<{
|
|
225
|
+
mission: GuideMission;
|
|
226
|
+
view: MissionView;
|
|
227
|
+
entry?: MissionEntry;
|
|
228
|
+
}>;
|
|
229
|
+
/** The mission the celebration offers next: the first one not settled and not locked. */
|
|
230
|
+
export declare function nextMissionKey(suite: GuideSuite, progress: GuideProgress, ctx: GuideContext): string | null;
|
|
231
|
+
/** Whether the welcome should open by itself. */
|
|
232
|
+
export declare function welcomeDue(state: GuideState, suite: GuideSuite): boolean;
|
|
233
|
+
/** Whether the floating "ภารกิจ n/N" chip shows. */
|
|
234
|
+
export declare function chipVisible(state: GuideState, suite: GuideSuite, ctx: GuideContext): boolean;
|
|
235
|
+
/** "กด [บันทึก]" → "กด บันทึก" (for screen readers). */
|
|
236
|
+
export declare function plainTitle(t: string): string;
|
|
237
|
+
export declare function reduceGuide(state: GuideState, action: GuideAction, suite: GuideSuite, env: GuideEnv): GuideResult;
|