@eifi1/ui-kit 0.20.1 → 0.22.0

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 (286) hide show
  1. package/dist/chart.d.ts +10 -8
  2. package/dist/components/account-chips.d.ts +10 -3
  3. package/dist/components/account-chips.js +5 -2
  4. package/dist/components/account-chips.js.map +1 -1
  5. package/dist/components/account-settings.d.ts +33 -2
  6. package/dist/components/account-settings.js +43 -21
  7. package/dist/components/account-settings.js.map +1 -1
  8. package/dist/components/amount-input.d.ts +27 -8
  9. package/dist/components/amount-input.js +15 -28
  10. package/dist/components/amount-input.js.map +1 -1
  11. package/dist/components/autocomplete.d.ts +8 -0
  12. package/dist/components/autocomplete.js +9 -4
  13. package/dist/components/autocomplete.js.map +1 -1
  14. package/dist/components/button-group.d.ts +10 -8
  15. package/dist/components/calculator.d.ts +10 -8
  16. package/dist/components/checkbox-group.d.ts +104 -0
  17. package/dist/components/checkbox-group.js +111 -0
  18. package/dist/components/checkbox-group.js.map +1 -0
  19. package/dist/components/checkbox.d.ts +19 -0
  20. package/dist/components/checkbox.js +30 -7
  21. package/dist/components/checkbox.js.map +1 -1
  22. package/dist/components/chip.d.ts +18 -0
  23. package/dist/components/chip.js +47 -10
  24. package/dist/components/chip.js.map +1 -1
  25. package/dist/components/combobox.d.ts +13 -2
  26. package/dist/components/combobox.js +17 -8
  27. package/dist/components/combobox.js.map +1 -1
  28. package/dist/components/confirm-dialog.d.ts +66 -161
  29. package/dist/components/copy-button.d.ts +10 -8
  30. package/dist/components/country-select.d.ts +124 -0
  31. package/dist/components/country-select.js +237 -0
  32. package/dist/components/country-select.js.map +1 -0
  33. package/dist/components/currency-select.d.ts +17 -1
  34. package/dist/components/currency-select.js +56 -23
  35. package/dist/components/currency-select.js.map +1 -1
  36. package/dist/components/danger-confirm.d.ts +66 -166
  37. package/dist/components/danger-confirm.js +133 -49
  38. package/dist/components/danger-confirm.js.map +1 -1
  39. package/dist/components/data-table-cells.d.ts +1 -2
  40. package/dist/components/data-table-filter-popover.d.ts +1 -2
  41. package/dist/components/data-table-filters.d.ts +1 -2
  42. package/dist/components/data-table-labels.d.ts +4 -97
  43. package/dist/components/data-table-labels.js +9 -1
  44. package/dist/components/data-table-labels.js.map +1 -1
  45. package/dist/components/data-table-pagination.d.ts +3 -1
  46. package/dist/components/data-table.d.ts +1 -2
  47. package/dist/components/data-table.js +98 -3
  48. package/dist/components/data-table.js.map +1 -1
  49. package/dist/components/date-picker.d.ts +25 -5
  50. package/dist/components/date-picker.js +110 -59
  51. package/dist/components/date-picker.js.map +1 -1
  52. package/dist/components/entity-combobox.d.ts +8 -1
  53. package/dist/components/entity-combobox.js +9 -4
  54. package/dist/components/entity-combobox.js.map +1 -1
  55. package/dist/components/facing-pair.d.ts +10 -8
  56. package/dist/components/field-anatomy.d.ts +95 -0
  57. package/dist/components/field-anatomy.js +84 -0
  58. package/dist/components/field-anatomy.js.map +1 -0
  59. package/dist/components/field-parts.d.ts +114 -0
  60. package/dist/components/field-parts.js +72 -0
  61. package/dist/components/field-parts.js.map +1 -0
  62. package/dist/components/file-button.d.ts +10 -8
  63. package/dist/components/file-button.js +13 -3
  64. package/dist/components/file-button.js.map +1 -1
  65. package/dist/components/file-dropzone.d.ts +32 -9
  66. package/dist/components/file-dropzone.js +57 -34
  67. package/dist/components/file-dropzone.js.map +1 -1
  68. package/dist/components/form-actions.d.ts +10 -8
  69. package/dist/components/form-actions.js +61 -3
  70. package/dist/components/form-actions.js.map +1 -1
  71. package/dist/components/hover-menu.d.ts +3 -0
  72. package/dist/components/hover-menu.js +2 -2
  73. package/dist/components/hover-menu.js.map +1 -1
  74. package/dist/components/iban-input.d.ts +66 -0
  75. package/dist/components/iban-input.js +125 -0
  76. package/dist/components/iban-input.js.map +1 -0
  77. package/dist/components/icon-picker.d.ts +12 -2
  78. package/dist/components/icon-picker.js +9 -2
  79. package/dist/components/icon-picker.js.map +1 -1
  80. package/dist/components/inline-edit-field.d.ts +167 -0
  81. package/dist/components/inline-edit-field.js +293 -0
  82. package/dist/components/inline-edit-field.js.map +1 -0
  83. package/dist/components/language-select.d.ts +127 -0
  84. package/dist/components/language-select.js +18 -0
  85. package/dist/components/language-select.js.map +1 -0
  86. package/dist/components/line-items.d.ts +25 -3
  87. package/dist/components/line-items.js +55 -46
  88. package/dist/components/line-items.js.map +1 -1
  89. package/dist/components/list.d.ts +25 -0
  90. package/dist/components/list.js +16 -5
  91. package/dist/components/list.js.map +1 -1
  92. package/dist/components/money-field.d.ts +20 -8
  93. package/dist/components/money-field.js.map +1 -1
  94. package/dist/components/month-picker.d.ts +52 -4
  95. package/dist/components/month-picker.js +290 -86
  96. package/dist/components/month-picker.js.map +1 -1
  97. package/dist/components/multi-select.d.ts +8 -1
  98. package/dist/components/multi-select.js +9 -4
  99. package/dist/components/multi-select.js.map +1 -1
  100. package/dist/components/number-field.d.ts +25 -13
  101. package/dist/components/number-field.js +6 -20
  102. package/dist/components/number-field.js.map +1 -1
  103. package/dist/components/number-input.d.ts +70 -13
  104. package/dist/components/number-input.js +90 -38
  105. package/dist/components/number-input.js.map +1 -1
  106. package/dist/components/numpad-sheet.d.ts +10 -8
  107. package/dist/components/one-time-code-input.d.ts +100 -0
  108. package/dist/components/one-time-code-input.js +137 -0
  109. package/dist/components/one-time-code-input.js.map +1 -0
  110. package/dist/components/phone-input.d.ts +66 -0
  111. package/dist/components/phone-input.js +171 -0
  112. package/dist/components/phone-input.js.map +1 -0
  113. package/dist/components/series-chart.d.ts +10 -8
  114. package/dist/components/settings-fields.d.ts +10 -8
  115. package/dist/components/share-card.d.ts +8 -6
  116. package/dist/components/sign-chip.d.ts +66 -0
  117. package/dist/components/sign-chip.js +39 -0
  118. package/dist/components/sign-chip.js.map +1 -0
  119. package/dist/components/swatch-picker.d.ts +27 -2
  120. package/dist/components/swatch-picker.js +73 -31
  121. package/dist/components/swatch-picker.js.map +1 -1
  122. package/dist/components/switch.d.ts +19 -0
  123. package/dist/components/switch.js +24 -6
  124. package/dist/components/switch.js.map +1 -1
  125. package/dist/components/text-link.d.ts +10 -8
  126. package/dist/components/tile-radio.d.ts +68 -13
  127. package/dist/components/tile-radio.js +3 -3
  128. package/dist/components/tile-radio.js.map +1 -1
  129. package/dist/components/time-input.d.ts +15 -9
  130. package/dist/components/time-input.js.map +1 -1
  131. package/dist/components/toggle-group.d.ts +47 -1
  132. package/dist/components/toggle-group.js +110 -69
  133. package/dist/components/toggle-group.js.map +1 -1
  134. package/dist/components/ui.d.ts +10 -8
  135. package/dist/components/ui.js +280 -78
  136. package/dist/components/ui.js.map +1 -1
  137. package/dist/components/use-table-state.d.ts +1 -2
  138. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  139. package/dist/data-table.d.ts +1 -2
  140. package/dist/data-table.js +2 -1
  141. package/dist/data-table.js.map +1 -1
  142. package/dist/feedback/feedback-attachment.d.ts +1 -1
  143. package/dist/feedback/feedback-attachment.js +210 -3
  144. package/dist/feedback/feedback-attachment.js.map +1 -1
  145. package/dist/feedback/feedback-dialog.d.ts +1 -1
  146. package/dist/feedback/feedback-dialog.js.map +1 -1
  147. package/dist/feedback/feedback-inbox.d.ts +1 -1
  148. package/dist/feedback/feedback-thread.d.ts +37 -3
  149. package/dist/feedback/feedback-thread.js +38 -9
  150. package/dist/feedback/feedback-thread.js.map +1 -1
  151. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-NhUm7Zga.d.ts} +70 -5
  152. package/dist/feedback.d.ts +2 -2
  153. package/dist/hooks/use-file-drop.d.ts +10 -8
  154. package/dist/i18n/defaults.d.ts +10 -8
  155. package/dist/i18n/defaults.js +17 -2
  156. package/dist/i18n/defaults.js.map +1 -1
  157. package/dist/i18n/german.d.ts +10 -8
  158. package/dist/i18n/german.js +50 -5
  159. package/dist/i18n/german.js.map +1 -1
  160. package/dist/i18n/kit-labels.d.ts +10 -8
  161. package/dist/i18n/kit-labels.js +8 -2
  162. package/dist/i18n/kit-labels.js.map +1 -1
  163. package/dist/i18n/languages.d.ts +10 -8
  164. package/dist/i18n/locales/de-CH.d.ts +10 -8
  165. package/dist/i18n/locales/en.d.ts +10 -8
  166. package/dist/i18n/locales/en.js +9 -0
  167. package/dist/i18n/locales/en.js.map +1 -1
  168. package/dist/i18n/locales/es.d.ts +10 -8
  169. package/dist/i18n/locales/es.js +48 -5
  170. package/dist/i18n/locales/es.js.map +1 -1
  171. package/dist/i18n/locales/fr.d.ts +10 -8
  172. package/dist/i18n/locales/fr.js +48 -5
  173. package/dist/i18n/locales/fr.js.map +1 -1
  174. package/dist/i18n/locales/hu.d.ts +10 -8
  175. package/dist/i18n/locales/hu.js +49 -5
  176. package/dist/i18n/locales/hu.js.map +1 -1
  177. package/dist/i18n/locales/it.d.ts +10 -8
  178. package/dist/i18n/locales/it.js +49 -5
  179. package/dist/i18n/locales/it.js.map +1 -1
  180. package/dist/i18n/locales/zh.d.ts +10 -8
  181. package/dist/i18n/locales/zh.js +48 -5
  182. package/dist/i18n/locales/zh.js.map +1 -1
  183. package/dist/i18n/review.d.ts +10 -8
  184. package/dist/i18n/review.js +8 -1
  185. package/dist/i18n/review.js.map +1 -1
  186. package/dist/index.d.ts +17 -10
  187. package/dist/index.js +57 -2
  188. package/dist/index.js.map +1 -1
  189. package/dist/{kit-labels-D1J2_jyD.d.ts → kit-labels-R8mIAc9N.d.ts} +811 -8
  190. package/dist/lib/countries.d.ts +43 -0
  191. package/dist/lib/countries.js +39 -0
  192. package/dist/lib/countries.js.map +1 -0
  193. package/dist/lib/decimal-marks.d.ts +46 -0
  194. package/dist/lib/decimal-marks.js +40 -0
  195. package/dist/lib/decimal-marks.js.map +1 -0
  196. package/dist/lib/iban.d.ts +98 -0
  197. package/dist/lib/iban.js +154 -0
  198. package/dist/lib/iban.js.map +1 -0
  199. package/dist/lib/isin.d.ts +30 -0
  200. package/dist/lib/isin.js +36 -0
  201. package/dist/lib/isin.js.map +1 -0
  202. package/dist/lib/phone.d.ts +69 -0
  203. package/dist/lib/phone.js +121 -0
  204. package/dist/lib/phone.js.map +1 -0
  205. package/dist/rhf/fields.d.ts +148 -11
  206. package/dist/rhf/fields.js +332 -3
  207. package/dist/rhf/fields.js.map +1 -1
  208. package/dist/rhf/form.d.ts +10 -8
  209. package/dist/rhf.d.ts +13 -9
  210. package/dist/rhf.js.map +1 -1
  211. package/dist/shell/app-shell.d.ts +10 -8
  212. package/dist/shell/top-bar-brand.d.ts +10 -8
  213. package/dist/shell/topbar-action-menu.d.ts +13 -1
  214. package/dist/shell/topbar-action-menu.js +31 -28
  215. package/dist/shell/topbar-action-menu.js.map +1 -1
  216. package/dist/shell.d.ts +10 -8
  217. package/dist/tour/tour.js +20 -24
  218. package/dist/tour/tour.js.map +1 -1
  219. package/dist/wizard/stepper-nav.d.ts +10 -8
  220. package/dist/wizard.d.ts +10 -8
  221. package/package.json +1 -1
  222. package/src/components/account-chips.tsx +17 -3
  223. package/src/components/account-settings.tsx +92 -26
  224. package/src/components/amount-input.tsx +49 -56
  225. package/src/components/autocomplete.tsx +133 -110
  226. package/src/components/checkbox-group.tsx +229 -0
  227. package/src/components/checkbox.tsx +72 -8
  228. package/src/components/chip.tsx +80 -10
  229. package/src/components/combobox.tsx +292 -253
  230. package/src/components/country-select.tsx +443 -0
  231. package/src/components/currency-select.tsx +109 -33
  232. package/src/components/danger-confirm.tsx +345 -77
  233. package/src/components/data-table-labels.ts +16 -1
  234. package/src/components/data-table.tsx +175 -6
  235. package/src/components/date-picker.tsx +173 -62
  236. package/src/components/entity-combobox.tsx +107 -87
  237. package/src/components/field-anatomy.tsx +190 -0
  238. package/src/components/field-parts.tsx +184 -0
  239. package/src/components/file-button.tsx +36 -3
  240. package/src/components/file-dropzone.tsx +106 -38
  241. package/src/components/form-actions.tsx +115 -2
  242. package/src/components/hover-menu.tsx +7 -4
  243. package/src/components/iban-input.tsx +231 -0
  244. package/src/components/icon-picker.tsx +26 -4
  245. package/src/components/inline-edit-field.tsx +533 -0
  246. package/src/components/language-select.tsx +78 -0
  247. package/src/components/line-items.tsx +35 -2
  248. package/src/components/list.tsx +55 -6
  249. package/src/components/money-field.tsx +10 -0
  250. package/src/components/month-picker.tsx +399 -63
  251. package/src/components/multi-select.tsx +80 -58
  252. package/src/components/number-field.tsx +28 -46
  253. package/src/components/number-input.tsx +210 -43
  254. package/src/components/one-time-code-input.tsx +288 -0
  255. package/src/components/phone-input.tsx +299 -0
  256. package/src/components/sign-chip.tsx +103 -0
  257. package/src/components/swatch-picker.tsx +118 -30
  258. package/src/components/switch.tsx +58 -7
  259. package/src/components/tile-radio.tsx +71 -16
  260. package/src/components/time-input.tsx +8 -1
  261. package/src/components/toggle-group.tsx +206 -73
  262. package/src/components/ui.tsx +472 -105
  263. package/src/data-table.ts +2 -1
  264. package/src/feedback/feedback-attachment.tsx +313 -9
  265. package/src/feedback/feedback-dialog.tsx +2 -0
  266. package/src/feedback/feedback-thread.tsx +66 -5
  267. package/src/i18n/defaults.ts +16 -1
  268. package/src/i18n/german.ts +47 -0
  269. package/src/i18n/kit-labels.tsx +78 -2
  270. package/src/i18n/locales/en.ts +10 -0
  271. package/src/i18n/locales/es.ts +46 -0
  272. package/src/i18n/locales/fr.ts +45 -0
  273. package/src/i18n/locales/hu.ts +46 -0
  274. package/src/i18n/locales/it.ts +47 -0
  275. package/src/i18n/locales/zh.ts +44 -0
  276. package/src/i18n/review.ts +7 -0
  277. package/src/index.ts +53 -3
  278. package/src/lib/countries.ts +91 -0
  279. package/src/lib/decimal-marks.ts +99 -0
  280. package/src/lib/iban.ts +171 -0
  281. package/src/lib/isin.ts +62 -0
  282. package/src/lib/phone.ts +211 -0
  283. package/src/rhf/fields.tsx +606 -9
  284. package/src/rhf.ts +2 -1
  285. package/src/shell/topbar-action-menu.tsx +53 -29
  286. package/src/tour/tour.tsx +66 -28
@@ -136,6 +136,7 @@ export function TopBarActionMenu({
136
136
  iconBadge,
137
137
  trigger,
138
138
  triggerClassName,
139
+ href,
139
140
  ariaLabel,
140
141
  heading,
141
142
  header,
@@ -169,6 +170,18 @@ export function TopBarActionMenu({
169
170
  trigger?: (state: { open: boolean }) => ReactNode;
170
171
  /** Extra classes for the trigger button — `rounded-full` round an avatar. */
171
172
  triggerClassName?: string;
173
+ /**
174
+ * Make the trigger a LINK to this route (keksdose dev #585, Marcel: "clicking on the
175
+ * guided tours icon shall lead to the tours page like the all tours element does").
176
+ * A click or Enter follows it and shuts the panel; hover still opens the panel after
177
+ * the usual delay, and ↓/↑ on the focused trigger still open it onto the first/last
178
+ * row, so the rows stay reachable by keyboard. A tap on a touch screen follows the
179
+ * link — so use it where the destination offers everything the menu does. Rendered as
180
+ * an `<a>` (the router's `Link`) with `aria-haspopup="menu"` and `aria-expanded`,
181
+ * named by `ariaLabel`: a link inside the kit's button would be interactive content
182
+ * inside interactive content.
183
+ */
184
+ href?: string;
172
185
  /** Names the trigger and the menu. */
173
186
  ariaLabel: string;
174
187
  heading?: string;
@@ -197,37 +210,48 @@ export function TopBarActionMenu({
197
210
  <HoverMenu
198
211
  ariaLabel={ariaLabel}
199
212
  align={align}
200
- trigger={({ open, toggle }) =>
201
- trigger ? (
202
- <button type="button" onClick={toggle} className={cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName)}>
203
- <span className="sr-only">{ariaLabel}</span>
204
- {/* The space keeps the name from running into the face's text ("Account
205
- menu3 unread"); as a flex item it takes no room. */}
206
- {" "}
207
- {trigger({ open })}
208
- </button>
209
- ) : iconBadge ? (
213
+ trigger={({ open, toggle, close }) => {
214
+ // One element for both kinds: the router's link when `href` is set, else the
215
+ // kit's button. Same classes and face either way.
216
+ const control = (className: string, children: ReactNode, label?: string) =>
217
+ href ? (
218
+ <Link to={href} onClick={close} aria-label={label} className={className}>
219
+ {children}
220
+ </Link>
221
+ ) : (
222
+ <button type="button" onClick={toggle} aria-label={label} className={className}>
223
+ {children}
224
+ </button>
225
+ );
226
+ if (trigger) {
227
+ return control(
228
+ cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName),
229
+ <>
230
+ <span className="sr-only">{ariaLabel}</span>
231
+ {/* The space keeps the name from running into the face's text ("Account
232
+ menu3 unread"); as a flex item it takes no room. */}
233
+ {" "}
234
+ {trigger({ open })}
235
+ </>,
236
+ );
237
+ }
238
+ if (iconBadge) {
210
239
  // Named by its content, like the custom-trigger branch, so the badge label is
211
240
  // part of the name; an `aria-label` would have hidden it.
212
- <button type="button" onClick={toggle} className={cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName)}>
213
- <span className="sr-only">{ariaLabel}</span>{" "}
214
- <span aria-hidden className="relative inline-flex">
215
- {icon}
216
- <StatusDot ring tone={iconBadge.tone ?? "danger"} className="absolute -end-0.5 -top-0.5" />
217
- </span>{" "}
218
- <span className="sr-only">{iconBadge.label}</span>
219
- </button>
220
- ) : (
221
- <button
222
- type="button"
223
- onClick={toggle}
224
- aria-label={ariaLabel}
225
- className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}
226
- >
227
- {icon}
228
- </button>
229
- )
230
- }
241
+ return control(
242
+ cn(TOPBAR_TRIGGER_CLASS, "relative", triggerClassName),
243
+ <>
244
+ <span className="sr-only">{ariaLabel}</span>{" "}
245
+ <span aria-hidden className="relative inline-flex">
246
+ {icon}
247
+ <StatusDot ring tone={iconBadge.tone ?? "danger"} className="absolute -end-0.5 -top-0.5" />
248
+ </span>{" "}
249
+ <span className="sr-only">{iconBadge.label}</span>
250
+ </>,
251
+ );
252
+ }
253
+ return control(cn(TOPBAR_TRIGGER_CLASS, triggerClassName), icon, ariaLabel);
254
+ }}
231
255
  >
232
256
  {(close) => {
233
257
  const footerBody = typeof footer === "function" ? footer(close) : footer;
package/src/tour/tour.tsx CHANGED
@@ -261,6 +261,43 @@ function sameRect(a: Rect | null, b: Rect | null): boolean {
261
261
  return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;
262
262
  }
263
263
 
264
+ function rectOf(el: Element): Rect {
265
+ const r = el.getBoundingClientRect();
266
+ return { top: r.top, left: r.left, width: r.width, height: r.height };
267
+ }
268
+
269
+ /**
270
+ * What the finder established for ONE step: where its target is (`null` = no target,
271
+ * or not on the page), and the reading direction the card takes from it.
272
+ *
273
+ * It carries the step it was found for, and the overlay only believes it for that
274
+ * step — which is the whole fix for keksdose's tours audit 2026-10-02, §1.0 A ("stale
275
+ * spotlight on the step after a spotlighted one", 15 steps in 16 tours, desktop and
276
+ * phone, mouse and keyboard). When step N spotlit a target and step N+1 had none (or
277
+ * its anchor was missing), N+1 kept N's hole and parked its card beside it, so a
278
+ * centred "You're all set" read as pointing at the Import & Export link; Back→Next kept
279
+ * it for as long as anyone watched.
280
+ *
281
+ * The overlay used to hold `rect` and `ready` as separate states and reset them
282
+ * DURING RENDER when the step changed. React 19 does not keep a render-phase update
283
+ * while an earlier, lower-priority update to the same hook is still queued — it only
284
+ * writes the result to the base state "unless the queue is empty" — and the follow
285
+ * interval below queues exactly such an update every 250 ms (eagerly bailed out because
286
+ * the rect had not moved, but left in the queue for rebasing). So the click's render
287
+ * showed the reset, and the very next render (the finder saying "ready") re-based on
288
+ * the old rect. A deep link was fine because nothing had queued anything yet.
289
+ *
290
+ * Keying the result to its step does not depend on any of that ordering: a rect found
291
+ * for another step is not this step's rect, whatever the queue does with it, and a
292
+ * missing anchor reads as "no target" instead of as the last thing that was lit.
293
+ */
294
+ interface Located {
295
+ index: number;
296
+ step: TourStep;
297
+ rect: Rect | null;
298
+ dir: Direction;
299
+ }
300
+
264
301
  function TourOverlay({
265
302
  steps,
266
303
  index,
@@ -279,33 +316,31 @@ function TourOverlay({
279
316
  const step = steps[index];
280
317
  const isFirst = index === 0;
281
318
  const isLast = index === steps.length - 1;
282
- const [rect, setRect] = useState<Rect | null>(null);
283
- const [ready, setReady] = useState(false);
284
- // A new step starts unlocated: reset during render when the step changes, so the
285
- // previous step's spotlight never paints over the new one. The effect below then
286
- // locates it.
287
- const [locating, setLocating] = useState({ index, step });
288
- if (locating.index !== index || locating.step !== step) {
289
- setLocating({ index, step });
290
- setReady(false);
291
- setRect(null);
292
- }
319
+ // A new step starts unlocated: whatever was found for another step (see `Located`)
320
+ // is ignored, so the overlay renders nothing until the effect below has located THIS
321
+ // step, and never the previous step's spotlight.
322
+ const [located, setLocated] = useState<Located | null>(null);
323
+ const current = located !== null && located.index === index && located.step === step ? located : null;
324
+ const ready = current !== null;
325
+ const rect = current?.rect ?? null;
293
326
  // The overlay is portalled to <body>, out of whatever `dir` subtree the app set, so it
294
327
  // takes the direction of the element it spotlights (the document's for a centered
295
328
  // step). Arrow keys and start/end placement read it.
296
- const [dir, setDir] = useState<Direction>(() => dirOf(null));
329
+ const dir: Direction = current?.dir ?? "ltr";
297
330
  const rootRef = useRef<HTMLDivElement>(null);
298
331
 
299
332
  // Run beforeStep, then locate the target (retrying across route/render lag).
300
333
  useEffect(() => {
301
334
  let cancelled = false;
302
335
  let raf = 0;
336
+ // `null` = untargeted, or the finder gave up → the card centres, with no hole.
337
+ const settle = (el: Element | null) =>
338
+ setLocated({ index, step, rect: el ? rectOf(el) : null, dir: dirOf(el) });
303
339
  void (async () => {
304
340
  await step.beforeStep?.();
305
341
  if (cancelled) return;
306
342
  if (!step.target) {
307
- setDir(dirOf(null));
308
- setReady(true);
343
+ settle(null);
309
344
  return;
310
345
  }
311
346
  let tries = 0;
@@ -314,17 +349,11 @@ function TourOverlay({
314
349
  const el = queryVisibleTarget(step.target!);
315
350
  if (el) {
316
351
  el.scrollIntoView({ block: "center", behavior: "smooth" });
317
- const r = el.getBoundingClientRect();
318
- setRect({ top: r.top, left: r.left, width: r.width, height: r.height });
319
- setDir(dirOf(el));
320
- setReady(true);
352
+ settle(el);
321
353
  return;
322
354
  }
323
355
  if (tries++ < 45) raf = requestAnimationFrame(find);
324
- else {
325
- setDir(dirOf(null));
326
- setReady(true); // give up → render centered
327
- }
356
+ else settle(null); // give up → render centered
328
357
  };
329
358
  find();
330
359
  })();
@@ -334,15 +363,19 @@ function TourOverlay({
334
363
  };
335
364
  }, [index, step]);
336
365
 
337
- // Follow the target as the page scrolls / reflows.
366
+ // Follow the target as the page scrolls / reflows — only once THIS step is located,
367
+ // and only into this step's result.
338
368
  useEffect(() => {
339
369
  if (!step.target) return;
340
370
  const update = () => {
341
371
  const el = queryVisibleTarget(step.target!);
342
372
  if (el) {
343
- const r = el.getBoundingClientRect();
344
- const next = { top: r.top, left: r.left, width: r.width, height: r.height };
345
- setRect((prev) => (sameRect(prev, next) ? prev : next));
373
+ const next = rectOf(el);
374
+ setLocated((prev) =>
375
+ prev && prev.index === index && prev.step === step && !sameRect(prev.rect, next)
376
+ ? { ...prev, rect: next }
377
+ : prev,
378
+ );
346
379
  }
347
380
  };
348
381
  window.addEventListener("scroll", update, true);
@@ -358,14 +391,19 @@ function TourOverlay({
358
391
  // awaitClick: advance when the *real* target is clicked. The spotlight hole is
359
392
  // click-through (see the shields in the render below) so the element's own
360
393
  // handler runs first; we step forward on the next macrotask.
394
+ //
395
+ // Re-armed when the target turns up after the finder gave up (the follow effect
396
+ // lights it late): the card swaps Next for the "click the highlighted element" hint
397
+ // as soon as there is a rect, so the click must work from that moment too.
398
+ const spotted = rect !== null;
361
399
  useEffect(() => {
362
- if (!ready || !step.awaitClick || !step.target) return;
400
+ if (!ready || !spotted || !step.awaitClick || !step.target) return;
363
401
  const el = queryVisibleTarget(step.target);
364
402
  if (!el) return;
365
403
  const onClick = () => window.setTimeout(() => onNext(), 0);
366
404
  el.addEventListener("click", onClick, { once: true });
367
405
  return () => el.removeEventListener("click", onClick);
368
- }, [ready, index, step, onNext]);
406
+ }, [ready, spotted, index, step, onNext]);
369
407
 
370
408
  // Keyboard: Esc skips, Enter and the reading-direction arrow advance (→ in LTR,
371
409
  // ← in RTL), the other arrow goes back. On awaitClick steps Enter is ignored (it