zigmini 1.0.0-beta.3 → 1.1.0-beta.1

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.
Files changed (138) hide show
  1. package/README.ko.md +27 -2
  2. package/README.md +27 -2
  3. package/assets/agent/gateway-request-rules.md +1 -1
  4. package/assets/agent/gateway-rules.md +6 -1
  5. package/assets/android/zigmini.apk +0 -0
  6. package/assets/android/zigmini.apk.json +3 -3
  7. package/assets/build.json +1 -1
  8. package/assets/document-reader/document-reader +0 -0
  9. package/assets/document-reader/host-selector +0 -0
  10. package/assets/document-reader/manifest.json +27 -0
  11. package/docs/manual.ko.md +114 -10
  12. package/docs/manual.md +117 -11
  13. package/host/document-reader/DocumentRead.swift +370 -0
  14. package/host/document-reader/HostSelector.swift +192 -0
  15. package/host/document-reader/Protocol.swift +647 -0
  16. package/host/document-reader/main.swift +22 -0
  17. package/host/omo-bridge/agent-aside-tool.ts +119 -0
  18. package/host/omo-bridge/agent-aside.ts +522 -0
  19. package/host/omo-bridge/agent-collaboration-files.ts +282 -0
  20. package/host/omo-bridge/agent-collaboration.ts +385 -0
  21. package/host/omo-bridge/agent-control.ts +400 -28
  22. package/host/omo-bridge/agent-execution.sb +42 -0
  23. package/host/omo-bridge/agent-execution.ts +158 -0
  24. package/host/omo-bridge/agent-tool-adapters.ts +388 -0
  25. package/host/omo-bridge/agent-tool-permissions.ts +96 -0
  26. package/host/omo-bridge/aside-tabs.ts +41 -0
  27. package/host/omo-bridge/attachment-read.ts +100 -0
  28. package/host/omo-bridge/bridge.ts +10 -0
  29. package/host/omo-bridge/managed-call.ts +46 -0
  30. package/host/omo-bridge/task-control.ts +39 -5
  31. package/host/omo-check/patches/attachments-293/README.md +138 -0
  32. package/host/omo-check/patches/attachments-293/document-content.js +234 -0
  33. package/host/omo-check/patches/attachments-293/document-content.types.txt +91 -0
  34. package/host/omo-check/patches/attachments-293/omo-5.1.51-documents.patch +831 -0
  35. package/host/omo-check/patches/attachments-293/omo-5.1.51-gateway-port.manifest.json +471 -0
  36. package/host/omo-check/patches/omo-5.1.51-agent-tool-authority.patch +471 -0
  37. package/host/omo-check/patches/omo-5.1.51-gateway-port.manifest.json +28 -6
  38. package/host/omo-check/patches/omo-5.1.51-memory-receipts.patch +5 -0
  39. package/package.json +5 -3
  40. package/src/agent/api.ts +345 -20
  41. package/src/agent/artifacts.ts +196 -0
  42. package/src/agent/aside-target-routes.ts +369 -0
  43. package/src/agent/aside-targets.ts +215 -0
  44. package/src/agent/collaboration-admission.ts +47 -0
  45. package/src/agent/collaboration-read.ts +117 -0
  46. package/src/agent/collaboration-session.ts +196 -0
  47. package/src/agent/collaboration-types.ts +258 -0
  48. package/src/agent/collaboration.ts +399 -0
  49. package/src/agent/definition.ts +65 -3
  50. package/src/agent/fields.ts +9 -3
  51. package/src/agent/file-core.ts +325 -0
  52. package/src/agent/files.ts +87 -8
  53. package/src/agent/list.ts +3 -3
  54. package/src/agent/managed-call.ts +144 -0
  55. package/src/agent/tool-catalog.ts +184 -0
  56. package/src/agent/tool-delivery.ts +359 -0
  57. package/src/agent/tool-permission-types.ts +81 -0
  58. package/src/agent/tool-permissions.ts +1366 -0
  59. package/src/attachments/api.ts +161 -0
  60. package/src/attachments/document-reader.ts +325 -0
  61. package/src/attachments/host-selector.ts +410 -0
  62. package/src/attachments/managed-binding.ts +83 -0
  63. package/src/attachments/prepare.ts +214 -0
  64. package/src/attachments/selector-port.ts +28 -0
  65. package/src/attachments/service.ts +729 -0
  66. package/src/attachments/store.ts +698 -0
  67. package/src/attachments/types.ts +123 -0
  68. package/src/cli/doctor.ts +30 -10
  69. package/src/decisions.ts +109 -10
  70. package/src/engine/apply-patch.ts +21 -0
  71. package/src/engine/compat.json +2 -1
  72. package/src/engine/install.ts +45 -4
  73. package/src/engine/sandbox-cell.ts +2 -0
  74. package/src/gc.ts +33 -225
  75. package/src/housekeeping/api.ts +163 -0
  76. package/src/housekeeping/audit.ts +61 -0
  77. package/src/housekeeping/collectors/agent-collaboration.ts +33 -0
  78. package/src/housekeeping/collectors/agent-drafts.ts +43 -0
  79. package/src/housekeeping/collectors/agent-file-trash.ts +36 -0
  80. package/src/housekeeping/collectors/agent-trash.ts +28 -0
  81. package/src/housekeeping/collectors/attachment-cleanup.ts +35 -0
  82. package/src/housekeeping/collectors/db-backup.ts +25 -0
  83. package/src/housekeeping/collectors/docker-build-cache.ts +29 -0
  84. package/src/housekeeping/collectors/docker-image.ts +135 -0
  85. package/src/housekeeping/collectors/omo-version.ts +32 -0
  86. package/src/housekeeping/collectors/orphan-test-process.ts +205 -0
  87. package/src/housekeeping/collectors/reconcile-work.ts +55 -0
  88. package/src/housekeeping/collectors/rpc-daemon.ts +39 -0
  89. package/src/housekeeping/collectors/shared.ts +149 -0
  90. package/src/housekeeping/collectors/temp-dir.ts +28 -0
  91. package/src/housekeeping/collectors/update-file.ts +25 -0
  92. package/src/housekeeping/collectors/worktree.ts +193 -0
  93. package/src/housekeeping/engine.ts +125 -0
  94. package/src/housekeeping/reconcile/work.ts +130 -0
  95. package/src/housekeeping/registry.ts +63 -0
  96. package/src/housekeeping/trash.ts +59 -0
  97. package/src/housekeeping/types.ts +31 -0
  98. package/src/hub.ts +1095 -41
  99. package/src/main.ts +228 -30
  100. package/src/message-references.ts +14 -5
  101. package/src/profile-prompt.ts +13 -1
  102. package/src/protocol.ts +41 -6
  103. package/src/security-allow.ts +79 -2
  104. package/src/security-check.ts +1 -1
  105. package/src/store.ts +1946 -23
  106. package/src/test-crash.ts +20 -0
  107. package/src/work-watch.ts +55 -7
  108. package/src/work.ts +192 -3
  109. package/web/DESIGN.md +33 -0
  110. package/web/agents.d.ts +189 -12
  111. package/web/agents.js +898 -39
  112. package/web/app.js +2634 -210
  113. package/web/attachments.d.ts +91 -0
  114. package/web/attachments.js +422 -0
  115. package/web/collaboration.d.ts +87 -0
  116. package/web/collaboration.js +290 -0
  117. package/web/decisions.d.ts +39 -2
  118. package/web/decisions.js +354 -2
  119. package/web/gc.d.ts +26 -5
  120. package/web/gc.js +113 -13
  121. package/web/i18n/agents.d.ts +4 -0
  122. package/web/i18n/agents.js +200 -8
  123. package/web/i18n/attachments.js +78 -0
  124. package/web/i18n/decisions.d.ts +4 -0
  125. package/web/i18n/decisions.js +68 -0
  126. package/web/i18n/gc.d.ts +4 -0
  127. package/web/i18n/gc.js +184 -2
  128. package/web/i18n/main.js +80 -8
  129. package/web/i18n/tour.d.ts +4 -0
  130. package/web/i18n/tour.js +37 -0
  131. package/web/index.html +20 -3
  132. package/web/profile.d.ts +2 -2
  133. package/web/profile.js +11 -6
  134. package/web/style.css +342 -21
  135. package/web/tooltip.d.ts +2 -0
  136. package/web/tooltip.js +12 -0
  137. package/web/tour.d.ts +57 -0
  138. package/web/tour.js +450 -0
package/web/tour.js ADDED
@@ -0,0 +1,450 @@
1
+ import { getLanguage, onLanguageChange, registerStrings, t } from "./i18n.js";
2
+ import strings from "./i18n/tour.js";
3
+ import { TIP_QUIET_ATTR } from "./tooltip.js";
4
+
5
+ registerStrings(strings);
6
+
7
+ // Plan agent-ux todo 23: the first-run tour of the whole app, an interactive walkthrough of seven steps in the
8
+ // operator's order. Each step frames one real element in a hole of a dimmed overlay and the user acts on that element to
9
+ // go on: typing in the composer (nothing is sent), a click on a sidebar row, a header button or "+ 에이전트". Only the
10
+ // 새 세션 step ends with a 다음 button, because the real click would start a new engine session. Esc skips the rest.
11
+ // A target behind a dialog, or the sidebar a phone never shows, is revealed first (dialogs closed, the sidebar opened
12
+ // as a drawer) and the page is put back when the step ends; a target that does not exist is skipped. While the tour
13
+ // runs it marks the body (F3 V2), so the page draws no hover or focus tip over the bubble (web/tooltip.js).
14
+ // The tour starts once per browser (localStorage zgw.tour.done=1, no server record) and again from the settings
15
+ // button 안내 투어 다시 보기.
16
+ // The decisions (progress, done flag, hole and bubble geometry, advance mode) are pure functions below; installTour
17
+ // binds them to the page.
18
+
19
+ export const TOUR_DONE_KEY = "zgw.tour.done";
20
+ export const TOUR_ID = "zgw-tour";
21
+ export const HOLE_PAD = 6;
22
+ export const BUBBLE_GAP = 12;
23
+ export const BUBBLE_MARGIN = 8;
24
+
25
+ /**
26
+ * The steps. `target`: the selector the hole frames (comma-separated selectors frame their union); `advance`: how the
27
+ * step ends - "input" (the user types in the target), "click" (a click on `hit`, by default the target, after the page
28
+ * handled it) or "next" (the 다음 button). `drawer`: the sidebar step, which opens the sidebar as a drawer where it is
29
+ * hidden. `opens`: the dialog the step needs open. `within`: where app.js draws a target that index.html does not hold.
30
+ */
31
+ export const TOUR_STEPS = Object.freeze([
32
+ Object.freeze({ id: "input", target: "#input", advance: "input" }),
33
+ Object.freeze({ id: "sidebar", target: "#conv-list, #agent-group", hit: "#conv-list li, #agent-list li[data-conv]", advance: "click", drawer: true }),
34
+ Object.freeze({ id: "newSession", target: "#rotate-session", advance: "next" }),
35
+ Object.freeze({ id: "inbox", target: "#toggle-inbox", advance: "click" }),
36
+ Object.freeze({ id: "agents", target: "#toggle-agents", advance: "click" }),
37
+ Object.freeze({ id: "agentCreate", target: "#pe-new", within: "#agents-body", advance: "click", opens: "agents" }),
38
+ Object.freeze({ id: "settings", target: "#toggle-settings", advance: "click" }),
39
+ ]);
40
+
41
+ /** The dialogs the tour closes to uncover a target, outermost first, each with the control that closes it. */
42
+ export const COVERS = Object.freeze([
43
+ Object.freeze({ id: "settings", open: "#settings:not([hidden])", close: "#settings-close" }),
44
+ Object.freeze({ id: "manual", open: "#manual:not([hidden])", close: "#manual-close" }),
45
+ Object.freeze({ id: "agents", open: "#agents:not([hidden])", close: "#agents-close" }),
46
+ Object.freeze({ id: "inbox", open: '#toggle-inbox[aria-expanded="true"]', close: "#inbox-close" }),
47
+ Object.freeze({ id: "status", open: ".app.status-open", close: "#status-back" }),
48
+ ]);
49
+
50
+ export function readTourDone(storage) {
51
+ return storage.getItem(TOUR_DONE_KEY) === "1";
52
+ }
53
+
54
+ export function writeTourDone(storage) {
55
+ storage.setItem(TOUR_DONE_KEY, "1");
56
+ }
57
+
58
+ /** The tour starts by itself only while this browser has never finished or skipped it. */
59
+ export function autoStartWanted(storage) {
60
+ return !readTourDone(storage);
61
+ }
62
+
63
+ export function startState() {
64
+ return { at: 0, done: false, reason: "running" };
65
+ }
66
+
67
+ /** "advance" moves to the next step and finishes after the last one; "skip" ends the tour where it is. */
68
+ export function tourTransition(state, action, count = TOUR_STEPS.length) {
69
+ if (state.done) return state;
70
+ if (action === "skip") return { at: state.at, done: true, reason: "skipped" };
71
+ const next = state.at + 1;
72
+ if (next >= count) return { at: count, done: true, reason: "finished" };
73
+ return { at: next, done: false, reason: "running" };
74
+ }
75
+
76
+ export function stepLabel(at, count = TOUR_STEPS.length) {
77
+ return t("tour.count", { n: at + 1, total: count });
78
+ }
79
+
80
+ /** A click or input step whose element is missing or disabled (an empty list, an offline composer) falls back to 다음. */
81
+ export function advanceMode(step, hasHit) {
82
+ return step.advance !== "next" && !hasHit ? "next" : step.advance;
83
+ }
84
+
85
+ export function hintKey(mode) {
86
+ return mode === "input" ? "tour.hint.input" : mode === "next" ? "tour.hint.next" : "tour.hint.click";
87
+ }
88
+
89
+ /**
90
+ * What a key does while the tour runs. `place` is where the key landed: "shielded" (on the 다음 step's real element),
91
+ * "input" (in the composer step's textarea), "row" (on an element the tour made focusable for its click) or "other".
92
+ * "skip" (Esc), "tab" (move inside the focus cycle), "block" (an activation the step must not let through), "activate"
93
+ * (Enter or Space does the row's click), null (left to the page).
94
+ */
95
+ export function keyAction(mode, key, place, shift = false) {
96
+ if (key === "Escape") return "skip";
97
+ if (key === "Tab") return "tab";
98
+ const activation = key === "Enter" || key === " " || key === "Spacebar";
99
+ if (!activation) return null;
100
+ if (mode === "next" && place === "shielded") return "block";
101
+ if (mode === "input" && key === "Enter" && !shift && place === "input") return "block";
102
+ if (mode === "click" && place === "row") return "activate";
103
+ return null;
104
+ }
105
+
106
+ /** The next position in the focus cycle: Tab moves forward, Shift+Tab backward, a focus outside the cycle enters at an end. */
107
+ export function cycleIndex(length, at, shift) {
108
+ if (at < 0) return shift ? length - 1 : 0;
109
+ return (at + (shift ? length - 1 : 1)) % length;
110
+ }
111
+
112
+ /** The Korean subject form of the agent's name: 이 after a final consonant, 가 otherwise (and for any other script). */
113
+ export function subjectName(name, lang = getLanguage()) {
114
+ if (lang !== "ko") return name;
115
+ const last = name.codePointAt(name.length - 1);
116
+ const hangul = last >= 0xac00 && last <= 0xd7a3;
117
+ return `${name}${hangul && (last - 0xac00) % 28 !== 0 ? "이" : "가"}`;
118
+ }
119
+
120
+ /** The padded union of the target boxes, clamped to the view; null when no target has a size. */
121
+ export function holeBox(rects, view, pad = HOLE_PAD) {
122
+ const boxes = rects.filter((box) => box.right > box.left && box.bottom > box.top);
123
+ if (boxes.length === 0) return null;
124
+ return {
125
+ left: Math.max(0, Math.min(...boxes.map((box) => box.left)) - pad),
126
+ top: Math.max(0, Math.min(...boxes.map((box) => box.top)) - pad),
127
+ right: Math.min(view.width, Math.max(...boxes.map((box) => box.right)) + pad),
128
+ bottom: Math.min(view.height, Math.max(...boxes.map((box) => box.bottom)) + pad),
129
+ };
130
+ }
131
+
132
+ /** The four shade panels around the hole; clicks land only inside the hole. */
133
+ export function shadeRects(hole, view) {
134
+ return {
135
+ top: { left: 0, top: 0, right: view.width, bottom: hole.top },
136
+ bottom: { left: 0, top: hole.bottom, right: view.width, bottom: view.height },
137
+ left: { left: 0, top: hole.top, right: hole.left, bottom: hole.bottom },
138
+ right: { left: hole.right, top: hole.top, right: view.width, bottom: hole.bottom },
139
+ };
140
+ }
141
+
142
+ /** The bubble sits below the hole, centred and clamped to the view; above when below does not fit. */
143
+ export function placeBubble(hole, size, view) {
144
+ const left = Math.max(BUBBLE_MARGIN, Math.min((hole.left + hole.right) / 2 - size.width / 2, view.width - size.width - BUBBLE_MARGIN));
145
+ const below = hole.bottom + BUBBLE_GAP;
146
+ const above = hole.top - BUBBLE_GAP - size.height;
147
+ if (below + size.height <= view.height - BUBBLE_MARGIN) return { left, top: below, side: "below" };
148
+ if (above >= BUBBLE_MARGIN) return { left, top: above, side: "above" };
149
+ return { left, top: Math.max(BUBBLE_MARGIN, Math.min(below, view.height - size.height - BUBBLE_MARGIN)), side: "below" };
150
+ }
151
+
152
+ function setBox(el, box) {
153
+ el.style.left = `${Math.round(box.left)}px`;
154
+ el.style.top = `${Math.round(box.top)}px`;
155
+ el.style.width = `${Math.max(0, Math.round(box.right - box.left))}px`;
156
+ el.style.height = `${Math.max(0, Math.round(box.bottom - box.top))}px`;
157
+ }
158
+
159
+ function firstSelector(step) {
160
+ return step.target.split(",")[0].trim();
161
+ }
162
+
163
+ function visibleBoxes(doc, selector) {
164
+ return [...doc.querySelectorAll(selector)].map((el) => el.getBoundingClientRect()).filter((box) => box.width > 0 && box.height > 0);
165
+ }
166
+
167
+ const FOCUSABLE = "button, textarea, input, select, a[href], [tabindex]";
168
+
169
+ export function installTour(doc, win, storage) {
170
+ const root = doc.createElement("div");
171
+ root.id = TOUR_ID;
172
+ root.className = "tour";
173
+ root.setAttribute("role", "dialog");
174
+ root.setAttribute("aria-modal", "true");
175
+ root.hidden = true;
176
+ const shades = {};
177
+ for (const side of ["top", "right", "bottom", "left"]) {
178
+ const shade = doc.createElement("div");
179
+ shade.className = "tour-shade";
180
+ shade.dataset.side = side;
181
+ shades[side] = shade;
182
+ root.append(shade);
183
+ }
184
+ const shield = doc.createElement("div");
185
+ shield.className = "tour-shield";
186
+ const ring = doc.createElement("div");
187
+ ring.className = "tour-ring";
188
+ const bubble = doc.createElement("div");
189
+ bubble.className = "tour-bubble";
190
+ bubble.tabIndex = -1;
191
+ const text = doc.createElement("p");
192
+ text.className = "tour-text";
193
+ text.id = `${TOUR_ID}-text`;
194
+ const hint = doc.createElement("p");
195
+ hint.className = "tour-hint";
196
+ const foot = doc.createElement("div");
197
+ foot.className = "tour-foot";
198
+ const count = doc.createElement("span");
199
+ count.className = "tour-count";
200
+ const next = doc.createElement("button");
201
+ next.type = "button";
202
+ next.className = "text-btn tour-next";
203
+ const skip = doc.createElement("button");
204
+ skip.type = "button";
205
+ skip.className = "text-btn tour-skip";
206
+ foot.append(count, next, skip);
207
+ bubble.append(text, hint, foot);
208
+ root.setAttribute("aria-labelledby", text.id);
209
+ root.append(shield, ring, bubble);
210
+ doc.body.append(root);
211
+
212
+ let state = null;
213
+ let mode = "click";
214
+ let step = null;
215
+ let returnFocus = null;
216
+ let frame = 0;
217
+
218
+ const app = () => doc.querySelector(".app");
219
+ const view = () => ({ width: win.innerWidth, height: win.innerHeight });
220
+ const agentName = () => doc.querySelector(".agent-name")?.textContent?.trim() || "Zig";
221
+
222
+ const covered = (selector) => {
223
+ const el = doc.querySelector(selector);
224
+ if (!el) return true;
225
+ const box = el.getBoundingClientRect();
226
+ if (box.width === 0 || box.height === 0) return true;
227
+ const hit = doc.elementFromPoint(Math.min(win.innerWidth - 1, box.left + box.width / 2), Math.min(win.innerHeight - 1, box.top + box.height / 2));
228
+ return !(hit && (hit === el || el.contains(hit)));
229
+ };
230
+
231
+ const closeCovers = (keep, selector) => {
232
+ for (const cover of COVERS) {
233
+ if (cover.id === keep || !covered(selector) || !doc.querySelector(cover.open)) continue;
234
+ doc.querySelector(cover.close)?.click();
235
+ }
236
+ };
237
+
238
+ /** Brings the step's target on screen: opens the dialog it needs, closes the ones over it, opens the phone's sidebar drawer. */
239
+ const reveal = (current) => {
240
+ root.hidden = true;
241
+ if (current.opens === "agents") {
242
+ if (!doc.querySelector("#agents:not([hidden])")) doc.querySelector("#toggle-agents")?.click();
243
+ if (!doc.querySelector(current.target)) doc.querySelector("#agents-nav-list")?.click();
244
+ }
245
+ const first = firstSelector(current);
246
+ closeCovers(current.opens, first);
247
+ if (current.drawer && covered(first)) app()?.classList.add("tour-drawer");
248
+ doc.querySelector(first)?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
249
+ return !covered(first);
250
+ };
251
+
252
+ const hits = () => (step ? [...doc.querySelectorAll(step.hit ?? step.target)] : []);
253
+ const shown = (el) => {
254
+ const box = el.getBoundingClientRect();
255
+ return box.width > 0 && box.height > 0;
256
+ };
257
+
258
+ /** A click step's rows get a tab stop for the tour's keyboard path; a 다음 step's real element is made inert. */
259
+ const prepareHits = () => {
260
+ if (mode === "click") {
261
+ for (const el of hits()) {
262
+ if (el.disabled || el.matches(FOCUSABLE)) continue;
263
+ el.setAttribute("tabindex", "0");
264
+ el.setAttribute("data-tour-focus", "");
265
+ }
266
+ }
267
+ if (mode === "next") {
268
+ for (const el of doc.querySelectorAll(step.target)) {
269
+ if (el.hasAttribute("data-tour-inert")) continue;
270
+ el.setAttribute("inert", "");
271
+ el.setAttribute("data-tour-inert", "");
272
+ }
273
+ }
274
+ };
275
+ const releaseHits = () => {
276
+ for (const el of doc.querySelectorAll("[data-tour-focus]")) {
277
+ el.removeAttribute("tabindex");
278
+ el.removeAttribute("data-tour-focus");
279
+ }
280
+ for (const el of doc.querySelectorAll("[data-tour-inert]")) {
281
+ el.removeAttribute("inert");
282
+ el.removeAttribute("data-tour-inert");
283
+ }
284
+ };
285
+
286
+ const restore = () => {
287
+ app()?.classList.remove("tour-drawer");
288
+ releaseHits();
289
+ };
290
+
291
+ /** The step's real elements the keyboard can act on, then 다음 (when shown), then 건너뛰기; a shielded element is never in it. */
292
+ const focusCycle = () => {
293
+ const list = mode === "next" ? [] : hits().filter((el) => !el.disabled && el.matches(FOCUSABLE) && shown(el));
294
+ if (!next.hidden) list.push(next);
295
+ list.push(skip);
296
+ return list;
297
+ };
298
+ const placeOf = (el) => {
299
+ if (!el) return "other";
300
+ return mode === "next" && el.closest(step.target) ? "shielded"
301
+ : mode === "input" && el.matches(step.target) ? "input"
302
+ : mode === "click" && el.closest("[data-tour-focus]") ? "row"
303
+ : "other";
304
+ };
305
+
306
+ const hitMode = () => advanceMode(step, [...doc.querySelectorAll(step.hit ?? step.target)].some((el) => !el.disabled));
307
+ const sentence = () => t(`tour.step.${step.id}`, { name: subjectName(agentName()) });
308
+
309
+ /** A step whose element became usable or unusable changes mode: every mark of the old mode goes before the new one is drawn. */
310
+ const switchMode = (wanted) => {
311
+ releaseHits();
312
+ mode = wanted;
313
+ render();
314
+ if (!focusCycle().includes(doc.activeElement)) focusCycle()[0].focus({ preventScroll: true });
315
+ };
316
+
317
+ const paint = () => {
318
+ frame = 0;
319
+ const wanted = hitMode();
320
+ if (wanted !== mode) return switchMode(wanted);
321
+ if (text.textContent !== sentence()) text.textContent = sentence();
322
+ prepareHits();
323
+ const hole = holeBox(visibleBoxes(doc, step.target), view());
324
+ if (!hole) return;
325
+ const panels = shadeRects(hole, view());
326
+ for (const side of Object.keys(panels)) setBox(shades[side], panels[side]);
327
+ setBox(ring, hole);
328
+ setBox(shield, hole);
329
+ shield.hidden = mode !== "next";
330
+ const place = placeBubble(hole, bubble.getBoundingClientRect(), view());
331
+ bubble.style.left = `${Math.round(place.left)}px`;
332
+ bubble.style.top = `${Math.round(place.top)}px`;
333
+ bubble.dataset.side = place.side;
334
+ };
335
+ const schedulePaint = () => {
336
+ if (!step || frame) return;
337
+ frame = win.requestAnimationFrame(paint);
338
+ };
339
+
340
+ const render = () => {
341
+ if (!step) return;
342
+ text.textContent = sentence();
343
+ hint.textContent = t(hintKey(mode));
344
+ count.textContent = stepLabel(state.at);
345
+ next.textContent = t("tour.next");
346
+ skip.textContent = t("tour.skip");
347
+ root.setAttribute("aria-label", t("tour.label"));
348
+ next.hidden = mode !== "next";
349
+ paint();
350
+ };
351
+
352
+ const finish = () => {
353
+ if (!state) return;
354
+ writeTourDone(storage);
355
+ root.hidden = true;
356
+ doc.body.removeAttribute(TIP_QUIET_ATTR);
357
+ step = null;
358
+ state = null;
359
+ if (frame) win.cancelAnimationFrame(frame);
360
+ frame = 0;
361
+ const back = returnFocus;
362
+ returnFocus = null;
363
+ if (back && back !== doc.body && doc.contains(back) && !doc.activeElement?.closest(".settings")) back.focus({ preventScroll: true });
364
+ };
365
+
366
+ const show = () => {
367
+ for (;;) {
368
+ if (state.done) return finish();
369
+ step = TOUR_STEPS[state.at];
370
+ if (reveal(step)) break;
371
+ restore();
372
+ state = tourTransition(state, "advance");
373
+ }
374
+ mode = hitMode();
375
+ root.hidden = false;
376
+ render();
377
+ focusCycle()[0].focus({ preventScroll: true });
378
+ };
379
+
380
+ const advance = () => {
381
+ if (!state || state.done) return;
382
+ restore();
383
+ state = tourTransition(state, "advance");
384
+ show();
385
+ };
386
+
387
+ const start = () => {
388
+ if (!state) returnFocus = doc.activeElement;
389
+ restore();
390
+ state = startState();
391
+ doc.body.setAttribute(TIP_QUIET_ATTR, "");
392
+ show();
393
+ };
394
+
395
+ const skipTour = () => {
396
+ if (!state) return;
397
+ state = tourTransition(state, "skip");
398
+ restore();
399
+ finish();
400
+ };
401
+
402
+ next.addEventListener("click", () => {
403
+ if (mode === "next") advance();
404
+ });
405
+ skip.addEventListener("click", skipTour);
406
+
407
+ doc.addEventListener("click", (event) => {
408
+ if (!state || mode !== "click" || !(event.target instanceof win.Element)) return;
409
+ if (!event.target.closest(step.hit ?? step.target)) return;
410
+ win.setTimeout(advance, 0);
411
+ }, true);
412
+ doc.addEventListener("input", (event) => {
413
+ if (!state || mode !== "input" || !(event.target instanceof win.Element) || !event.target.matches(step.target)) return;
414
+ if (String(event.target.value ?? "").length === 0) return;
415
+ win.setTimeout(advance, 0);
416
+ }, true);
417
+ doc.addEventListener("keydown", (event) => {
418
+ if (!state) return;
419
+ const el = event.target instanceof win.Element ? event.target : null;
420
+ const action = keyAction(mode, event.key, placeOf(el), event.shiftKey);
421
+ if (action === null) return;
422
+ event.preventDefault();
423
+ event.stopPropagation();
424
+ if (action === "skip") skipTour();
425
+ else if (action === "activate") el.closest("[data-tour-focus]").click();
426
+ else if (action === "tab") {
427
+ const cycle = focusCycle();
428
+ cycle[cycleIndex(cycle.length, cycle.indexOf(doc.activeElement), event.shiftKey)].focus({ preventScroll: true });
429
+ }
430
+ }, true);
431
+ doc.addEventListener("keyup", (event) => {
432
+ if (!state || mode !== "next" || !(event.target instanceof win.Element) || !event.target.closest(step.target)) return;
433
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
434
+ event.preventDefault();
435
+ event.stopPropagation();
436
+ }
437
+ }, true);
438
+ doc.addEventListener("focusin", (event) => {
439
+ if (!state || mode !== "next" || !(event.target instanceof win.Element) || !event.target.closest(step.target)) return;
440
+ next.focus({ preventScroll: true });
441
+ }, true);
442
+ win.addEventListener("resize", schedulePaint);
443
+ win.addEventListener("scroll", schedulePaint, true);
444
+ new win.MutationObserver((records) => {
445
+ if (records.some((record) => !root.contains(record.target))) schedulePaint();
446
+ }).observe(doc.body, { subtree: true, childList: true, attributes: true });
447
+ onLanguageChange(render);
448
+
449
+ return { start, skip: skipTour, active: () => state !== null, step: () => (state ? state.at : -1) };
450
+ }