help-layer 1.2.0 → 1.3.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.
package/README.ja.md CHANGED
@@ -15,7 +15,7 @@
15
15
  ユーザーが「解説モード」を ON にしている間だけ、知りたい要素の「?」マーカーをクリックして説明を読めます。通常時の見た目は一切変わりません。
16
16
  仕組みは透明な遮断レイヤー — 対象要素の近くにマーカーを出しつつ、元アプリのイベントには一切触れずに操作を吸収します。
17
17
 
18
- - 依存は [`@floating-ui/dom`](https://floating-ui.com/) のみ・軽量(プリビルドの IIFE で約 33KB minified、`@floating-ui/dom` 同梱)
18
+ - 実行時依存ゼロ・軽量(プリビルドの IIFE で約 20KB minified
19
19
  - Shadow DOM 貫通・SPA の動的要素・マーカー同士の重なり回避・画面端でのポップアップ自動調整に対応
20
20
  - キーボード操作・スクリーンリーダーに配慮(ポップアップは `role="dialog"`、開くとフォーカスを移し閉じるとマーカーへ復帰。モード中はフォーカスを UI 内に封じ込め、`Esc` で説明を閉じる(開いている説明が無ければ解説モード自体を終了))
21
21
  - ON→OFF で追加した DOM・イベント・スタイルを**完全後始末**
@@ -29,9 +29,11 @@
29
29
  - [クイックスタート](#クイックスタート)
30
30
  - [自由配置(要素に紐づけない説明)](#自由配置要素に紐づけない説明)
31
31
  - [API](#api)
32
+ - [API の上に作るレシピ(分析・ディープリンク・検索・フレームワーク連携)](./RECIPES.ja.md)
32
33
  - [テーマ(CSS カスタムプロパティ)](#テーマcss-カスタムプロパティ)
33
34
  - [対応環境(ブラウザ/ランタイム)](#対応環境ブラウザランタイム)
34
35
  - [既知の制約](#既知の制約)
36
+ - [パフォーマンス(マーカー数の目安)](#パフォーマンスマーカー数の目安)
35
37
  - [アクセシビリティ](#アクセシビリティ)
36
38
  - [セキュリティ](#セキュリティ)
37
39
  - [開発](#開発)
@@ -47,7 +49,7 @@
47
49
  - **常設ツールチップとの違い** … 説明を常時表示してUIを煩雑にすることがありません。マーカーは
48
50
  **モードON中だけ**出るので、**通常時のデザインは一切変わりません**。
49
51
  - **DAP系SaaS(Digital Adoption Platform=定着化支援 SaaS)との違い**
50
- … 外部基盤・契約・トラッキングを必要とせず、**ランニングコスト0・依存1つ・約33KB の完全ローカル動作**。
52
+ … 外部基盤・契約・トラッキングを必要とせず、**ランニングコスト0・依存ゼロ・約20KB の完全ローカル動作**。
51
53
  CSP / Trusted Types にも対応するため、持ち込み制約の厳しい環境にも入ります。
52
54
 
53
55
  そのうえで共通の核として、**既存コードを書き換えずに後付け**でき、**フレームワーク非依存**で、
@@ -58,16 +60,19 @@
58
60
  | 提示形式 | 線形ステップになりがち | 常時表示になりがち | サービス依存 | **モードON中だけ・任意箇所を探索** |
59
61
  | 通常時のUI | 実装次第 | 煩雑になりがち | 実装次第 | **一切変えない** |
60
62
  | 導入方法 | 多くは要組み込み | CSS/JS を追記 | スニペット+外部基盤+契約 | **後付け・既存コード非改変** |
61
- | コスト/運用 | 実装次第 | ローカル | 月額+トラッキング運用 | **ランニング0・依存1つ** |
63
+ | コスト/運用 | 実装次第 | ローカル | 月額+トラッキング運用 | **ランニング0・依存ゼロ** |
62
64
 
63
65
  > ※ HelpLayer は DAP の **フル代替ではありません**。アナリティクスやセグメント別配信、複雑なフロー誘導・
64
66
  > オンボーディング自動化といった高機能は対象外で、**「画面内に説明を出す」というコア機能だけを最小コストで満たす**ことに
65
67
  > 振り切っています。逆に、強い導線を引きたい・利用状況を計測したいといった目的が主なら、DAP やツアーの方が向きます。
68
+ >
69
+ > とはいえ、これらのいくつか(分析・ディープリンク・検索パネル・フレームワーク連携)は、公開 API の
70
+ > *上に* 数行で作れます。レシピは **[RECIPES.ja.md](./RECIPES.ja.md)** を参照してください。
66
71
 
67
72
  ## こんなときに(導入が刺さるケース)
68
73
 
69
74
  - **DAP/ガイド系 SaaS のコストが見合わず、解約を検討している。でも解約すると画面内ヘルプがゼロに戻る。**
70
- 「画面内に説明を出す」というコア機能だけを、依存1つ・ランニングコスト0 で自前に残せます。乗り換え後の受け皿に。
75
+ 「画面内に説明を出す」というコア機能だけを、依存ゼロ・ランニングコスト0 で自前に残せます。乗り換え後の受け皿に。
71
76
  - **SaaS を契約する予算感はないが、ヘルプは拡張したい。**
72
77
  → npm か `<script>` 1本で後付け。月額もアカウントも要りません。
73
78
  - **オフィスソフトで別途マニュアルを作る・更新するのが重い。しかも作っても読まれない。**
@@ -140,7 +145,7 @@ CDN から読む場合は、改ざん検知のため **バージョンを固定*
140
145
 
141
146
  ```html
142
147
  <script
143
- src="https://unpkg.com/help-layer@1.2.0/dist/help-layer.iife.js"
148
+ src="https://unpkg.com/help-layer@1.3.0/dist/help-layer.iife.js"
144
149
  integrity="sha384-……(公開版のハッシュに差し替え)"
145
150
  crossorigin="anonymous"></script>
146
151
  <script>
@@ -152,7 +157,7 @@ CDN から読む場合は、改ざん検知のため **バージョンを固定*
152
157
  ```
153
158
 
154
159
  > `integrity` のハッシュは公開した実ファイルから生成します。例:
155
- > `curl -s https://unpkg.com/help-layer@1.2.0/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
160
+ > `curl -s https://unpkg.com/help-layer@1.3.0/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
156
161
  > (バージョンを固定しないと SRI と不整合になり読み込みが拒否されます。)
157
162
 
158
163
  ## 自由配置(要素に紐づけない説明)
@@ -236,7 +241,7 @@ initHelpLayer({
236
241
 
237
242
  | 変数 | 既定 | 用途 |
238
243
  |------|------|------|
239
- | `--help-layer-marker-size` | `22px` | マーカー直径 |
244
+ | `--help-layer-marker-size` | `24px` | マーカー直径(WCAG 2.5.8 の最小ターゲットサイズに合わせた既定) |
240
245
  | `--help-layer-marker-bg` | `#2563eb` | マーカー背景色 |
241
246
  | `--help-layer-marker-color` | `#fff` | マーカー文字色 |
242
247
  | `--help-layer-popup-bg` | `#fff` | ポップアップ背景色 |
@@ -251,7 +256,7 @@ initHelpLayer({
251
256
 
252
257
  HelpLayer は **現代的な evergreen ブラウザ**(Chrome / Edge・Firefox・Safari)と近年の Electron の
253
258
  Chromium を対象とします。**IE11 は非対応であり、構造上対応できません** — ES2020 構文・ES Modules・
254
- `ResizeObserver`・Shadow DOM・`clip-path` に依存しており、IE はいずれも備えていないためです。これは
259
+ Shadow DOM・`clip-path` に依存しており、IE はいずれも備えていないためです。これは
255
260
  パッケージ形式の調整では埋められません。本当に古いランタイムを対象とする必要がある場合、本ライブラリは
256
261
  適合しません。
257
262
 
@@ -260,7 +265,7 @@ Chromium を対象とします。**IE11 は非対応であり、構造上対応
260
265
  | 機能 | 用途 | 下限の目安 | フォールバック |
261
266
  |---|---|---|---|
262
267
  | ES2020+ES Modules | ライブラリ全体 | evergreen(〜2020) | なし(古い対象はトランスパイル/バンドルが必要) |
263
- | `ResizeObserver`(`@floating-ui/dom` 経由) | マーカー/ポップアップの自動配置 | evergreen(〜2020) | なし |
268
+ | `requestAnimationFrame` | マーカー/ポップアップの自動配置(毎フレーム追従) | evergreen | なし |
264
269
  | open Shadow DOM 貫通 | shadow root 内の対象探索 | evergreen | closed は設計上非対応 |
265
270
  | `clip-path: polygon()` | 遮断レイヤーのトグル「穴」 | evergreen(極端に古い Safari は `-webkit-` 必要) | なし |
266
271
  | `inert` | ホストを a11y ツリーから除外 | Chrome102 / FF112 / Safari15.5(2023) | 視覚+キーボード遮断のみに縮退 |
@@ -269,8 +274,8 @@ Chromium を対象とします。**IE11 は非対応であり、構造上対応
269
274
  ### モジュール形式
270
275
 
271
276
  - **ESM(既定)**: `import { initHelpLayer } from 'help-layer'` はビルド済み・テスト済みの
272
- `dist/help-layer.esm.js` を解決します(`@floating-ui/dom` は external のままで、バンドラ/npm が解決)。
273
- - **バンドラ無し/`<script>`/CDN/厳格環境**: `@floating-ui/dom` を同梱しグローバル `HelpLayer` を公開する
277
+ `dist/help-layer.esm.js` を解決します(自己完結・解決すべき実行時依存なし)。
278
+ - **バンドラ無し/`<script>`/CDN/厳格環境**: グローバル `HelpLayer` を公開する
274
279
  自己完結の IIFE ビルドを使用 — [`<script>` だけで使う](#script-だけで使うバンドラなし) 参照。
275
280
  - **CommonJS(`require`)**: `require` 入口は提供しません。ブラウザ DOM 専用のため Node の CJS 文脈では
276
281
  意味を持ちません。非 ESM のツールチェーンではバンドラ経由で ESM を取り込むか、上記 IIFE を読み込んでください。
@@ -278,7 +283,7 @@ Chromium を対象とします。**IE11 は非対応であり、構造上対応
278
283
  ## 既知の制約
279
284
 
280
285
  - closed な Shadow DOM は JS から到達できないため非対応(open のみ貫通)。
281
- - マーカーを隅へ重ねるオフセットは既定マーカーサイズ(22px)前提。`--help-layer-marker-size` を大きく変えると
286
+ - マーカーを隅へ重ねるオフセットは既定マーカーサイズ(24px)前提。`--help-layer-marker-size` を大きく変えると
282
287
  わずかにズレることがあります。
283
288
  - **対象要素の「状態」変化は監視しません**(監視するのは「レイアウト」と「DOM 上の有無」のみ)。ON 中、
284
289
  マーカーは対象の位置・サイズ変化に追従し、DOM への追加/削除に応じて mount/unmount され、対象自体が
@@ -290,6 +295,33 @@ Chromium を対象とします。**IE11 は非対応であり、構造上対応
290
295
  `update(config)` で作り直す、モードを OFF→ON する、または対象要素を一度 DOM から外して入れ直してください
291
296
  (入れ直しは `childList` 監視に乗ります)。
292
297
 
298
+ ## パフォーマンス(マーカー数の目安)
299
+
300
+ コストは **同時に表示しているマーカー数** に比例します(登録した `config` の総数ではありません)。
301
+ 表示中の全マーカーは **1 本の共有 `requestAnimationFrame` ループ** でまとめて配置します(マーカーごとの
302
+ 監視や外部の配置ライブラリは使いません)。そのためスクロール中・アニメーション中も、1 フレームにつき
303
+ 表示中マーカーをまとめた read → compute → write の 1 パスだけが走ります。
304
+ マーカーは **モード ON 中、かつ DOM 上に存在して表示されている対象にのみ** 出ます。`display:none`
305
+ などで隠れた対象(`checkVisibility` が「隠れている」と判定した対象)のマーカーは、再配置・レイアウト
306
+ 計測・重なり回避のいずれからも除外されます。つまり効いてくるのは「いま画面に出ている数」です。
307
+
308
+ マーカー同士の重なり回避は 1 パスあたり `O(n²)` ですが、反復回数に上限があり定数も小さく、かつ実際に
309
+ 動いたフレームだけ実行されます。1 パスで読む矩形は表示中の参照要素ぶん各 1 回のみ(read と write を
310
+ フェーズ分離するのでレイアウトスラッシングは起きません)。規模で最初に効いてくるのは、表示中マーカー数
311
+ に比例して増える毎フレームの追従コストのほうです。
312
+
313
+ 目安(**同時に表示する**マーカー数):
314
+
315
+ - **数百個まで** … 一般的な端末で快適。
316
+ - **〜1000 個** … 実測でも滑らか(同梱のストレスページで自動スクロール中も 60fps を維持)。
317
+ - **それを大きく超える** … スクロール/アニメ中の毎フレーム追従コストがいずれ目立ち始めます。
318
+
319
+ とはいえこのモードは **探索型**(利用者が見たい箇所を選ぶ)なので、一画面に数十個もあれば十分なことが
320
+ ほとんどです。大規模ページでは対象を絞る、ページ/タブ単位で出し分ける(`update(config)` でセットを
321
+ 差し替える、別の `attribute` を使う等)として、同時に出す数を抑えるとよいでしょう。これは
322
+ [既知の制約](#既知の制約) で文書全体に属性監視を張らない選択をしたのと同じ理由 — どちらも「割に合わない
323
+ 毎フレーム処理」を避けるためです。
324
+
293
325
  ## アクセシビリティ
294
326
 
295
327
  モード ON 中は、ホストアプリを視覚・ポインタ/キーボードだけでなく **支援技術(AT)に対しても意味的に
@@ -315,7 +347,7 @@ Chromium を対象とします。**IE11 は非対応であり、構造上対応
315
347
  - 外部通信(`fetch` 等)・`localStorage` / `cookie` などのストレージ利用も**ありません**(完全ローカル動作)。
316
348
  - 唯一、未信頼データを HTML/DOM ノードとして挿入しうる経路は `render` オプションです。戻り値はサニタイズされないため、
317
349
  ユーザー入力を含む場合は呼び出し側で無害化してください(上記「本文に改行を入れる / リンクを置く」参照)。
318
- - ランタイム依存は `@floating-ui/dom` のみ。CDN 利用時は前述のとおりバージョン固定+SRI を推奨します。
350
+ - 実行時依存はありません。CDN 利用時は前述のとおりバージョン固定+SRI を推奨します。
319
351
 
320
352
  ### Content Security Policy(CSP)
321
353
 
package/README.md CHANGED
@@ -15,7 +15,7 @@ A **framework-agnostic "help mode" library you can drop into any existing web ap
15
15
  While the mode is ON, it shows a "?" marker next to each target element; clicking it opens a description popup. The normal appearance is completely unchanged.
16
16
  It never touches the host app's own event listeners — a transparent blocking layer absorbs interaction instead — so you can adopt it without rewriting existing code.
17
17
 
18
- - Only one dependency, [`@floating-ui/dom`](https://floating-ui.com/); lightweight (the prebuilt IIFE is ~33KB minified, with `@floating-ui/dom` bundled in)
18
+ - Zero runtime dependencies; lightweight (the prebuilt IIFE is ~20KB minified)
19
19
  - Pierces Shadow DOM, keeps up with dynamically added/removed elements in SPAs, avoids marker-to-marker overlap, and auto-adjusts the popup at screen edges
20
20
  - Mindful of keyboard use and screen readers (the popup is `role="dialog"`; opening moves focus and closing returns it to the marker; while the mode is on, focus is trapped within the UI, and `Esc` closes the popup — or exits the mode when no popup is open)
21
21
  - Fully cleans up the DOM, listeners, and styles it added when you turn it OFF
@@ -29,9 +29,11 @@ It never touches the host app's own event listeners — a transparent blocking l
29
29
  - [Quick start](#quick-start)
30
30
  - [Free placement (descriptions not bound to an element)](#free-placement-descriptions-not-bound-to-an-element)
31
31
  - [API](#api)
32
+ - [Recipes on top of the API (analytics, deep-linking, search, frameworks)](./RECIPES.md)
32
33
  - [Theming (CSS custom properties)](#theming-css-custom-properties)
33
34
  - [Browser & runtime support](#browser--runtime-support)
34
35
  - [Known limitations](#known-limitations)
36
+ - [Performance (how many markers)](#performance-how-many-markers)
35
37
  - [Accessibility](#accessibility)
36
38
  - [Security](#security)
37
39
  - [Development](#development)
@@ -48,7 +50,7 @@ them on the spot,"** aiming to sacrifice neither the normal look nor your existi
48
50
  - **vs. always-on tooltips** … It never clutters the UI by showing explanations all the time. Markers
49
51
  appear **only while the mode is ON**, so **the normal design is completely unchanged**.
50
52
  - **vs. DAP SaaS (Digital Adoption Platform)** … No external platform, contract, or tracking required —
51
- **zero running cost, one dependency, ~33KB, fully local**. It also supports CSP / Trusted Types, so it
53
+ **zero running cost, zero dependencies, ~20KB, fully local**. It also supports CSP / Trusted Types, so it
52
54
  fits environments with strict constraints on what you can bring in.
53
55
 
54
56
  On top of that, they all share a common core: you can **drop it in without rewriting existing code**, it's
@@ -60,18 +62,21 @@ interaction), and it **fully cleans up on ON→OFF**.
60
62
  | Presentation | tends to be linear steps | tends to be always visible | service-dependent | **only while ON · explore any spot** |
61
63
  | Normal UI | depends on implementation | tends to get cluttered | depends on implementation | **left entirely unchanged** |
62
64
  | Adoption | usually needs integration | add CSS/JS | snippet + external platform + contract | **drop-in · no existing-code changes** |
63
- | Cost / ops | depends on implementation | local | monthly fee + tracking ops | **zero running cost · one dependency** |
65
+ | Cost / ops | depends on implementation | local | monthly fee + tracking ops | **zero running cost · zero dependencies** |
64
66
 
65
67
  > Note: HelpLayer is **not a full replacement for a DAP**. Advanced features like analytics, segmented
66
68
  > delivery, complex flow guidance, and onboarding automation are out of scope — it commits to
67
69
  > **satisfying just the core "show explanations in-screen" function at minimal cost**. Conversely, if your
68
70
  > main goal is to drive strong funnels or measure usage, a DAP or a tour is the better fit.
71
+ >
72
+ > That said, several of these (analytics, deep-linking, a search palette, framework glue) are easy to
73
+ > build *on top* of the public API in a few lines — see **[RECIPES.md](./RECIPES.md)** for recipes.
69
74
 
70
75
  ## When it fits (where adoption pays off)
71
76
 
72
77
  - **A DAP / guide SaaS isn't worth the cost and you're considering canceling — but canceling drops your
73
78
  in-screen help back to zero.**
74
- → Keep just the core "show explanations in-screen" function in-house, with one dependency and zero
79
+ → Keep just the core "show explanations in-screen" function in-house, with zero dependencies and zero
75
80
  running cost. It gives you a place to land after switching away.
76
81
  - **You don't have the budget to contract a SaaS, but you want to expand your help.**
77
82
  → Drop it in with npm or a single `<script>`. No monthly fee, no account.
@@ -146,7 +151,7 @@ When loading from a CDN, we recommend **pinning the version** and adding **SRI (
146
151
 
147
152
  ```html
148
153
  <script
149
- src="https://unpkg.com/help-layer@1.2.0/dist/help-layer.iife.js"
154
+ src="https://unpkg.com/help-layer@1.3.0/dist/help-layer.iife.js"
150
155
  integrity="sha384-……(replace with the published file's hash)"
151
156
  crossorigin="anonymous"></script>
152
157
  <script>
@@ -158,7 +163,7 @@ When loading from a CDN, we recommend **pinning the version** and adding **SRI (
158
163
  ```
159
164
 
160
165
  > Generate the `integrity` hash from the actually published file, e.g.:
161
- > `curl -s https://unpkg.com/help-layer@1.2.0/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
166
+ > `curl -s https://unpkg.com/help-layer@1.3.0/dist/help-layer.iife.js | openssl dgst -sha384 -binary | openssl base64 -A`
162
167
  > (If you don't pin the version, the SRI will mismatch and the browser will refuse to load it.)
163
168
 
164
169
  ## Free placement (descriptions not bound to an element)
@@ -242,7 +247,7 @@ You can change the look just by overriding the following variables in your host
242
247
 
243
248
  | Variable | Default | Purpose |
244
249
  |------|------|------|
245
- | `--help-layer-marker-size` | `22px` | marker diameter |
250
+ | `--help-layer-marker-size` | `24px` | marker diameter (default meets WCAG 2.5.8 minimum target size) |
246
251
  | `--help-layer-marker-bg` | `#2563eb` | marker background color |
247
252
  | `--help-layer-marker-color` | `#fff` | marker text color |
248
253
  | `--help-layer-popup-bg` | `#fff` | popup background color |
@@ -257,7 +262,7 @@ You can change the look just by overriding the following variables in your host
257
262
 
258
263
  HelpLayer targets **modern evergreen browsers** (Chrome / Edge, Firefox, Safari) and the Chromium in
259
264
  recent Electron. **Internet Explorer 11 is not supported and cannot be** — the library relies on ES2020
260
- syntax, ES modules, `ResizeObserver`, Shadow DOM, and `clip-path`, none of which IE provides. Packaging
265
+ syntax, ES modules, Shadow DOM, and `clip-path`, none of which IE provides. Packaging
261
266
  changes can't bridge this; if you must support genuinely old runtimes, this library is not the right fit.
262
267
 
263
268
  What sets the minimum (the two newest APIs degrade gracefully, so the practical hard floor is roughly
@@ -266,7 +271,7 @@ What sets the minimum (the two newest APIs degrade gracefully, so the practical
266
271
  | Feature | Used for | Minimum | Fallback |
267
272
  |---|---|---|---|
268
273
  | ES2020 + ES modules | the whole library | evergreen (~2020) | none — transpile/bundle for older targets |
269
- | `ResizeObserver` (via `@floating-ui/dom`) | marker/popup auto-positioning | evergreen (~2020) | none |
274
+ | `requestAnimationFrame` | marker/popup auto-positioning (per-frame tracking) | evergreen | none |
270
275
  | Open Shadow DOM piercing | finding targets inside shadow roots | evergreen | closed shadow roots are unsupported by design |
271
276
  | `clip-path: polygon()` | the blocking layer's toggle "hole" | evergreen (very old Safari needs `-webkit-`) | none |
272
277
  | `inert` | removing the host from the a11y tree | Chrome 102 / FF 112 / Safari 15.5 (2023) | degrades to visual + keyboard blocking only |
@@ -275,9 +280,9 @@ What sets the minimum (the two newest APIs degrade gracefully, so the practical
275
280
  ### Module formats
276
281
 
277
282
  - **ESM (default).** `import { initHelpLayer } from 'help-layer'` resolves to the prebuilt, tested
278
- `dist/help-layer.esm.js` (with `@floating-ui/dom` left external for your bundler/npm to resolve).
283
+ `dist/help-layer.esm.js` (self-contained no runtime dependencies to resolve).
279
284
  - **No bundler / `<script>` / CDN / strict environments.** Use the self-contained IIFE build, which
280
- bundles `@floating-ui/dom` and exposes a global `HelpLayer` — see [Use it with just a `<script>`](#use-it-with-just-a-script-no-bundler).
285
+ exposes a global `HelpLayer` — see [Use it with just a `<script>`](#use-it-with-just-a-script-no-bundler).
281
286
  - **CommonJS (`require`).** No `require` entry is provided: this is a browser-only DOM library, so a Node
282
287
  CJS context can't use it meaningfully. In non-ESM toolchains, consume the ESM build via your bundler,
283
288
  or load the IIFE build above.
@@ -285,7 +290,7 @@ What sets the minimum (the two newest APIs degrade gracefully, so the practical
285
290
  ## Known limitations
286
291
 
287
292
  - Closed Shadow DOM is unreachable from JS, so it is unsupported (only open shadow roots are pierced).
288
- - The offset that overlaps the marker onto a corner assumes the default marker size (22px). Changing
293
+ - The offset that overlaps the marker onto a corner assumes the default marker size (24px). Changing
289
294
  `--help-layer-marker-size` significantly may cause a slight drift.
290
295
  - **Target *state* changes are not watched** (only *layout* and *presence* are). While ON, a marker
291
296
  follows its target's position/size changes and is added/removed as the target enters/leaves the DOM,
@@ -298,6 +303,35 @@ What sets the minimum (the two newest APIs degrade gracefully, so the practical
298
303
  rebuild via `update(config)`, toggle the mode OFF→ON, or re-insert the target element into the DOM
299
304
  (re-insertion is picked up by the `childList` observation).
300
305
 
306
+ ## Performance (how many markers)
307
+
308
+ The cost scales with the number of **markers visible at once**, not with the size of your `config`.
309
+ All visible markers are positioned together in **one shared `requestAnimationFrame` loop** (no
310
+ per-marker watchers, no positioning library), so while the page scrolls or animates each frame does a
311
+ single batched read → compute → write pass over the visible markers.
312
+ Markers only exist **while the mode is ON and only for targets currently present and shown** — a target
313
+ hidden via `display:none` (or otherwise reported hidden by `checkVisibility`) has its marker excluded
314
+ from positioning, layout measurement, and overlap avoidance. So what matters is "how many are on the
315
+ page right now," not how many keys you registered.
316
+
317
+ Marker-to-marker overlap avoidance is `O(n²)` per pass, but it's capped at a few iterations with a small
318
+ constant and runs only when something actually moved; each pass reads every visible reference rect just
319
+ once (reads and writes are phase-separated, so there's no per-marker layout thrashing). What you'd feel
320
+ first at scale is the per-frame tracking, which grows linearly with the number of visible markers.
321
+
322
+ Rough guidance (markers shown **at the same time**):
323
+
324
+ - **Up to a few hundred** — comfortable on typical hardware.
325
+ - **~1000** — still smooth in our measurements (a locked 60fps under continuous auto-scroll on the
326
+ bundled stress page).
327
+ - **Well beyond that** — the per-frame tracking during scroll/animation eventually shows.
328
+
329
+ Even so, the mode is **exploratory** — users pick the spot they want, so a screen rarely needs more than
330
+ a few dozen markers. On large pages, scope your targets, or split them per page/tab (e.g. swap sets with
331
+ `update(config)`, or use a different `attribute`) to keep the number shown at once down. This mirrors the
332
+ deliberate choice in [Known limitations](#known-limitations) not to watch every attribute mutation across
333
+ the document — both avoid per-frame work that doesn't pay for itself.
334
+
301
335
  ## Accessibility
302
336
 
303
337
  While the mode is ON, the host app is blocked not only visually and for pointer/keyboard input, but
@@ -324,7 +358,7 @@ To report a vulnerability and for the support / security-release policy, see [SE
324
358
  - There is **no external communication** (`fetch`, etc.) and **no storage use** (`localStorage` / `cookie`) — it runs fully locally.
325
359
  - The only path through which untrusted data is inserted into the DOM as HTML / DOM nodes is the `render` option. Its return value is not sanitized, so
326
360
  neutralize it on the caller side if it contains user input (see "Line breaks & links in the body" above).
327
- - The only runtime dependency is `@floating-ui/dom`. When using a CDN, pin the version and add SRI as noted above.
361
+ - The library has no runtime dependencies. When using a CDN, pin the version and add SRI as noted above.
328
362
 
329
363
  ### Content Security Policy (CSP)
330
364
 
@@ -1,7 +1,7 @@
1
- function B(e,{toggleEl:t,isLibraryNode:o}){let r=new Set;function n(a){if(a.nodeType!==1)return;let c=a;o(c)||t&&(c===t||c.contains(t))||c.hasAttribute("inert")||(c.toggleAttribute("inert",!0),r.add(c))}for(let a of[...document.body.children])n(a);let i=new MutationObserver(a=>{for(let c of a)c.addedNodes.forEach(n)});i.observe(document.body,{childList:!0}),e.track(()=>{i.disconnect(),r.forEach(a=>a.removeAttribute("inert")),r.clear()})}var ve=0;function F(){let e=document.createElement("div");return e.className="help-layer-blocking-layer",e}function z(e,t="?"){let o=document.createElement("button");return o.type="button",o.className="help-layer-marker",o.textContent=t,o.setAttribute("aria-label",`Help: ${e}`),o}function H(){let e=`help-layer-popup-title-${ve++}`,t=document.createElement("div");t.className="help-layer-popup",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),t.setAttribute("aria-labelledby",e),t.tabIndex=-1;let o=document.createElement("div");o.className="help-layer-popup__title",o.id=e;let r=document.createElement("div");r.className="help-layer-popup__text";let n=document.createElement("button");return n.type="button",n.className="help-layer-popup__close",n.textContent="\xD7",n.setAttribute("aria-label","Close"),t.append(o,r,n),{root:t,titleEl:o,textEl:r,closeEl:n}}import{autoUpdate as R,computePosition as q,flip as ke,offset as V,shift as we}from"@floating-ui/dom";function K(e,t){let o=e.width||0,r=e.height||0,n=e.left-t.x,i=e.top-t.y;return{x:n,y:i,left:n,top:i,right:n+o,bottom:i+r,width:o,height:r}}function Z(e){return{contextElement:document.body,getBoundingClientRect(){return K(e(),{x:window.scrollX,y:window.scrollY})}}}function G(e,t,o){e.style.left=`${t}px`,e.style.top=`${o}px`}function W(e){if(!(e instanceof Element))return!1;let t=e;for(;t;){if(getComputedStyle(t).position==="fixed")return!0;let o=t.parentElement;if(o)t=o;else{let r=t.getRootNode();t=r instanceof ShadowRoot?r.host:null}}return!1}function Ee(e){if(!(e instanceof Element))return!1;if(typeof e.checkVisibility=="function")return!e.checkVisibility({visibilityProperty:!0,contentVisibilityAuto:!0});let t=e.getBoundingClientRect();return t.width===0&&t.height===0}var O=11;function Le(e){let t=e.endsWith("-start");return{mainAxis:-O,crossAxis:t?O:-O}}function Y(e,t,o,r="top-end",n){let i=W(e)?"fixed":"absolute";i==="fixed"&&t.style.setProperty("position","fixed","important");let a=!1;return R(e,t,()=>{if(Ee(e)){t.style.setProperty("display","none","important"),!a&&n&&n(),a=!0,o&&o();return}a=!1,t.style.removeProperty("display"),q(e,t,{placement:r,strategy:i,middleware:[V(Le(r))]}).then(({x:d,y})=>{G(t,d,y),o&&o()}).catch(()=>{})},{animationFrame:!0})}function U(e,t,o="bottom-start"){let r=W(e)?"fixed":"absolute";t.style.setProperty("position",r,"important");let n=()=>{q(e,t,{placement:o,strategy:r,middleware:[V(8),ke({padding:8}),we({padding:8})]}).then(({x:a,y:c})=>{G(t,a,c)}).catch(()=>{})},i=R(e,t,n,{animationFrame:!0});return{update:n,cleanup:i}}function X(e,t,o){return R(e,t,o)}function Ce(e){let t=e.left,o=e.top,r=e.right,n=e.bottom;return`polygon(
1
+ function W(e,{toggleEl:t,isLibraryNode:o}){let r=new Set;function n(i){if(i.nodeType!==1)return;let a=i;o(a)||t&&(a===t||a.contains(t))||a.hasAttribute("inert")||(a.toggleAttribute("inert",!0),r.add(a))}for(let i of[...document.body.children])n(i);let l=new MutationObserver(i=>{for(let a of i)a.addedNodes.forEach(n)});l.observe(document.body,{childList:!0}),e.track(()=>{l.disconnect(),r.forEach(i=>i.removeAttribute("inert")),r.clear()})}var we=0;function G(){let e=document.createElement("div");return e.className="help-layer-blocking-layer",e}function U(e,t="?"){let o=document.createElement("button");return o.type="button",o.className="help-layer-marker",o.textContent=t,o.setAttribute("aria-label",`Help: ${e}`),o}function Y(){let e=we++,t=`help-layer-popup-title-${e}`,o=`help-layer-popup-text-${e}`,r=document.createElement("div");r.className="help-layer-popup",r.setAttribute("role","dialog"),r.setAttribute("aria-modal","true"),r.setAttribute("aria-labelledby",t),r.setAttribute("aria-describedby",o),r.tabIndex=-1;let n=document.createElement("div");n.className="help-layer-popup__title",n.id=t;let l=document.createElement("div");l.className="help-layer-popup__text",l.id=o;let i=document.createElement("button");return i.type="button",i.className="help-layer-popup__close",i.textContent="\xD7",i.setAttribute("aria-label","Close"),r.append(n,l,i),{root:r,titleEl:n,textEl:l,closeEl:i}}function J(e,t){let o=e.width||0,r=e.height||0,n=e.left-t.x,l=e.top-t.y;return{x:n,y:l,left:n,top:l,right:n+o,bottom:l+r,width:o,height:r}}var _=12;function Q(e,t,o="top-end"){let[r,n]=o.split("-"),i=n==="start"?_:-_,a,c;return r==="top"||r==="bottom"?(c=r==="top"?e.top-t+_:e.top+e.height-_,n==="start"?a=e.left:n==="end"?a=e.left+e.width-t:a=e.left+e.width/2-t/2,a+=i):(a=r==="left"?e.left-t+_:e.left+e.width-_,n==="start"?c=e.top:n==="end"?c=e.top+e.height-t:c=e.top+e.height/2-t/2,c+=i),{left:a,top:c}}function D(e,t,o,r,n){return{left:e-o.left-r,top:t-o.top-n}}var X=(e,t,o)=>Math.min(Math.max(e,t),Math.max(t,o));function ee(e,t,o,r={}){let n=r.placement??"bottom-start",l=r.offset??8,i=r.padding??8,[a,c]=n.split("-"),m=a==="top"||a==="bottom",h=s=>s==="bottom"?e.top+e.height+l:s==="top"?e.top-t.height-l:s==="right"?e.left+e.width+l:e.left-t.width-l,u=s=>s==="bottom"?o.height-i-(e.top+e.height+l):s==="top"?e.top-l-i:s==="right"?o.width-i-(e.left+e.width+l):e.left-l-i,v={top:"bottom",bottom:"top",left:"right",right:"left"}[a],w=m?t.height:t.width,E=u(a)>=w||u(a)>=u(v)?a:v,b=(s,g,y)=>c==="start"?y:c==="end"?y+s-g:y+s/2-g/2,f,p;return m?(p=h(E),f=b(e.width,t.width,e.left),f=X(f,i,o.width-t.width-i)):(f=h(E),p=b(e.height,t.height,e.top),p=X(p,i,o.height-t.height-i)),{left:f,top:p,placement:c?`${E}-${c}`:E}}function H(e){return{contextElement:document.body,getBoundingClientRect(){return J(e(),{x:window.scrollX,y:window.scrollY})}}}function I(e){if(!(e instanceof Element))return!1;let t=e;for(;t;){if(getComputedStyle(t).position==="fixed")return!0;let o=t.parentElement;if(o)t=o;else{let r=t.getRootNode();t=r instanceof ShadowRoot?r.host:null}}return!1}function j(e){if(!(e instanceof Element))return!1;if(typeof e.checkVisibility=="function")return!e.checkVisibility({visibilityProperty:!0,contentVisibilityAuto:!0});let t=e.getBoundingClientRect();return t.width===0&&t.height===0}function te(e,t,o){e.style.left=`${t}px`,e.style.top=`${o}px`}function Ee(e,t){return e.top===t.top&&e.left===t.left&&e.width===t.width&&e.height===t.height}function oe(e,t){t();let o=e.getBoundingClientRect(),r=!1,n=requestAnimationFrame(function i(){if(r)return;let a=e.getBoundingClientRect();Ee(a,o)||t(),o=a,n=requestAnimationFrame(i)}),l=()=>t();return window.addEventListener("resize",l),()=>{r=!0,cancelAnimationFrame(n),window.removeEventListener("resize",l)}}function ne(e,t,o="bottom-start"){let r=I(e)?"fixed":"absolute";t.style.setProperty("position",r,"important");let n=()=>{let i=e.getBoundingClientRect(),a={width:t.offsetWidth,height:t.offsetHeight},c={width:window.innerWidth,height:window.innerHeight},{left:m,top:h}=ee(i,a,c,{placement:o});if(r==="fixed")te(t,m,h);else{let u=document.body,v=D(m,h,u.getBoundingClientRect(),u.clientLeft,u.clientTop);te(t,v.left,v.top)}},l=oe(e,n);return{update:n,cleanup:l}}function re(e,t,o){return oe(e,o)}function Le(e){let t=e.left,o=e.top,r=e.right,n=e.bottom;return`polygon(
2
2
  0px 0px, 100% 0px, 100% 100%, 0px 100%, 0px 0px,
3
3
  ${t}px ${o}px, ${t}px ${n}px, ${r}px ${n}px, ${r}px ${o}px, ${t}px ${o}px
4
- )`}function J(e,{toggleEl:t,onBackgroundClick:o,isLibraryElement:r,onEscape:n}){let i=F();if(document.body.appendChild(i),e.track(()=>i.remove()),t){let g=X(t,i,()=>{i.style.clipPath=Ce(t.getBoundingClientRect())});e.track(g)}o&&(i.addEventListener("click",o),e.track(()=>i.removeEventListener("click",o)));let a=document.activeElement;a instanceof HTMLElement&&a!==document.body&&a!==t&&a.blur();let c=f=>{r(f.target)||(f.stopPropagation(),t?t.focus({preventScroll:!0}):f.target instanceof HTMLElement&&f.target.blur())};document.addEventListener("focusin",c,!0),e.track(()=>document.removeEventListener("focusin",c,!0));let d=f=>{r(f.target)||(f.stopPropagation(),f.preventDefault())},y=f=>{if(f.key==="Escape"){f.stopPropagation(),f.preventDefault();return}d(f)},m=f=>{if(f.key==="Escape"){f.stopPropagation(),f.preventDefault(),n&&n();return}d(f)};return document.addEventListener("keydown",m,!0),document.addEventListener("keyup",y,!0),document.addEventListener("keypress",y,!0),e.track(()=>{document.removeEventListener("keydown",m,!0),document.removeEventListener("keyup",y,!0),document.removeEventListener("keypress",y,!0)}),i}function T(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Q(e){return T(e)&&Number.isFinite(e.top)&&Number.isFinite(e.left)}function M(e){if(!T(e))throw new Error("helpConfig must be a plain object");for(let[t,o]of Object.entries(e)){if(!T(o))throw new Error(`helpConfig["${t}"] must be an object`);if(typeof o.title!="string"||o.title==="")throw new Error(`helpConfig["${t}"].title must be a non-empty string`);if(typeof o.text!="string"||o.text==="")throw new Error(`helpConfig["${t}"].text must be a non-empty string`);if(o.position!==void 0&&!Q(o.position))throw new Error(`helpConfig["${t}"].position must be { top: finite number, left: finite number }`)}}function ee(e){return Object.entries(e).map(([t,o])=>Q(o.position)?{key:t,title:o.title,text:o.text,kind:"free",target:null,position:{top:o.position.top,left:o.position.left}}:{key:t,title:o.title,text:o.text,kind:"element",target:null,position:null})}function te(e,t={}){let o=t.minDistance??26,r=t.iterations??6,n=e.map(i=>({x:i.x,y:i.y}));for(let i=0;i<r;i++){let a=!1;for(let c=0;c<n.length;c++)for(let d=c+1;d<n.length;d++){let y=n[c],m=n[d],f=m.x-y.x,g=m.y-y.y,b=Math.hypot(f,g);if(b>=o)continue;b===0&&(f=1,g=0,b=1);let p=(o-b)/2,l=f/b,u=g/b;y.x-=l*p,y.y-=u*p,m.x+=l*p,m.y+=u*p,a=!0}if(!a)break}return n.map((i,a)=>({dx:i.x-e[a].x,dy:i.y-e[a].y}))}var oe="help-layer-target-highlight";function Ae(e){return e.kind==="free"?Z(()=>({top:e.position.top,left:e.position.left,width:0,height:0})):e.target}function ne(e,{onMarkerClick:t,onOverlapResolved:o,onMarkerHidden:r,markerLabel:n="?",markerPlacement:i="top-end"}){let a=new Map,c=null,d=!1;function y(){c=null;let p=[...a.values()].filter(s=>s.el.style.display!=="none");if(p.length<=1){let s=p.length===1?p[0].el:null;s&&s.style.transform&&(s.style.transform="",o&&o());return}p.forEach(s=>{s.el.style.transform=""});let l=p.map(s=>{let h=s.el.getBoundingClientRect();return{x:h.left+h.width/2,y:h.top+h.height/2}}),u=te(l);p.forEach((s,h)=>{let{dx:w,dy:v}=u[h];s.el.style.transform=w||v?`translate(${w}px, ${v}px)`:""}),o&&o()}function m(){c!==null||d||(c=requestAnimationFrame(y))}function f(p){if(a.has(p.id))return;let l=z(p.title,n);document.body.appendChild(l);let u=()=>t(p,l);l.addEventListener("click",u);let s=Y(Ae(p),l,m,i,()=>r&&r(p)),h=p.kind==="element"?p.target:null,w=()=>h&&h.classList.add(oe),v=()=>h&&h.classList.remove(oe);h&&(l.addEventListener("mouseenter",w),l.addEventListener("mouseleave",v),l.addEventListener("focus",w),l.addEventListener("blur",v));let C=!1,A=()=>{C||(C=!0,s(),l.removeEventListener("click",u),h&&(l.removeEventListener("mouseenter",w),l.removeEventListener("mouseleave",v),l.removeEventListener("focus",w),l.removeEventListener("blur",v),v()),l.remove(),a.delete(p.id),m())};a.set(p.id,{record:p,el:l,cleanup:A})}function g(p){let l=a.get(p);l&&l.cleanup()}function b(p){p.forEach(f)}return e.track(()=>{d=!0,c!==null&&(cancelAnimationFrame(c),c=null),[...a.values()].forEach(p=>p.cleanup())}),{mount:f,unmount:g,mountAll:b,has(p){return a.has(p)},findByKey(p){for(let l of a.values())if(l.record.key===p)return l;return null}}}function k(e,t,...o){if(t)try{return t(...o)}catch(r){console.error(`[help-layer] ${e} threw:`,r);return}}var Se=1;function P(e,t,o,r){typeof e.querySelectorAll=="function"&&e.querySelectorAll("*").forEach(n=>{o&&n.matches(t)&&o(n),n.shadowRoot&&(r&&r(n.shadowRoot),P(n.shadowRoot,t,o,r))})}function ie(e,t){let o=[];return P(e,t,r=>o.push(r)),o}function Te(e){let t=[];return P(e,"*",null,o=>t.push(o)),t}function re(e,t){let o=[],r=[];if(e.nodeType!==Se)return{matches:o,shadowRoots:r};let n=e;return typeof n.matches=="function"&&n.matches(t)&&o.push(n),n.shadowRoot&&(r.push(n.shadowRoot),P(n.shadowRoot,t,i=>o.push(i),i=>r.push(i))),P(n,t,i=>o.push(i),i=>r.push(i)),{matches:o,shadowRoots:r}}function ae({root:e=document,selector:t,onAdded:o,onRemoved:r}){let n=new Set,i=d=>{for(let y of d)y.addedNodes.forEach(m=>{let{matches:f,shadowRoots:g}=re(m,t);f.forEach(b=>k("observer onAdded",o,b)),g.forEach(c)}),y.removedNodes.forEach(m=>{re(m,t).matches.forEach(f=>k("observer onRemoved",r,f))})},a=new MutationObserver(i);function c(d){n.has(d)||(n.add(d),a.observe(d,{childList:!0,subtree:!0}))}return c(e),Te(e).forEach(c),{disconnect(){a.disconnect(),n.clear()}}}var $="data-help-title",N="data-help-text";function D(e="data-help-id"){return`[${e}], [${$}]`}function I(e){let t=new Map;for(let o of e)o.kind==="element"&&t.set(o.key,o);return t}function le(e){return e.filter(t=>t.kind==="free").map(t=>({id:t.key,kind:"free",key:t.key,title:t.title,text:t.text,position:t.position}))}function j(e,t,o="data-help-id"){let r=e.getAttribute(o),n=r!=null?t.get(r):void 0,i=n?n.title:e.getAttribute($),a=n?n.text:e.getAttribute(N);return!i||!a?null:{id:e,kind:"element",key:r,title:i,text:a,target:e}}function se(e,t=document,{silent:o=!1,attribute:r="data-help-id"}={}){let n=I(e),i=[];return ie(t,D(r)).forEach(a=>{let c=j(a,n,r);if(!c){if(!o){let d=a.getAttribute(r);console.warn(d!=null?`[help-layer] element with ${r}="${d}" has no matching helpConfig entry or inline ${$}/${N}`:`[help-layer] element needs both ${$} and ${N} (or a ${r} matching helpConfig)`)}return}i.push(c)}),i}function pe(e,{onClose:t,render:o,popupPlacement:r="bottom-start"}={}){let{root:n,titleEl:i,textEl:a,closeEl:c}=H();n.style.setProperty("display","none","important"),document.body.appendChild(n),c.addEventListener("click",()=>l());let d=null,y=null,m=null;function f(){m&&(m.cleanup(),m=null)}function g(u,s){i.textContent=u.title;let h=k("render",o,u);a.textContent="",h?a.appendChild(h):a.textContent=u.text,n.style.setProperty("display","block","important"),d=u.id,y=s,f(),m=U(s,n,r),n.focus({preventScroll:!0})}function b(){m&&m.update()}function p(){let u=d!==null;f(),d=null,y=null,n.style.setProperty("display","none","important"),u&&k("onClose",t)}function l(u){let s=u??y;p(),s&&s.isConnected&&typeof s.focus=="function"&&s.focus({preventScroll:!0})}return e.track(()=>{p(),n.remove()}),{root:n,isOpen(u){return d===u},getOpenId(){return d},open:g,close:l,reposition:b}}function ce(){let e=[];return{track(t){e.push(t)},teardownAll(){for(;e.length>0;){let t=e.pop();try{t()}catch(o){console.error("[help-layer] teardown step threw:",o)}}}}}var Pe="data-help-layer-style",$e=`
4
+ )`}function ie(e,{toggleEl:t,onBackgroundClick:o,isLibraryElement:r,onEscape:n}){let l=G();if(document.body.appendChild(l),e.track(()=>l.remove()),t){let v=re(t,l,()=>{l.style.clipPath=Le(t.getBoundingClientRect())});e.track(v)}o&&(l.addEventListener("click",o),e.track(()=>l.removeEventListener("click",o)));let i=document.activeElement;i instanceof HTMLElement&&i!==document.body&&i!==t&&i.blur();let a=u=>{r(u.target)||(u.stopPropagation(),t?t.focus({preventScroll:!0}):u.target instanceof HTMLElement&&u.target.blur())};document.addEventListener("focusin",a,!0),e.track(()=>document.removeEventListener("focusin",a,!0));let c=u=>{r(u.target)||(u.stopPropagation(),u.preventDefault())},m=u=>{if(u.key==="Escape"){u.stopPropagation(),u.preventDefault();return}c(u)},h=u=>{if(u.key==="Escape"){u.stopPropagation(),u.preventDefault(),n&&n();return}c(u)};return document.addEventListener("keydown",h,!0),document.addEventListener("keyup",m,!0),document.addEventListener("keypress",m,!0),e.track(()=>{document.removeEventListener("keydown",h,!0),document.removeEventListener("keyup",m,!0),document.removeEventListener("keypress",m,!0)}),l}function N(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function le(e){return N(e)&&Number.isFinite(e.top)&&Number.isFinite(e.left)}function q(e){if(!N(e))throw new Error("helpConfig must be a plain object");for(let[t,o]of Object.entries(e)){if(!N(o))throw new Error(`helpConfig["${t}"] must be an object`);if(typeof o.title!="string"||o.title==="")throw new Error(`helpConfig["${t}"].title must be a non-empty string`);if(typeof o.text!="string"||o.text==="")throw new Error(`helpConfig["${t}"].text must be a non-empty string`);if(o.position!==void 0&&!le(o.position))throw new Error(`helpConfig["${t}"].position must be { top: finite number, left: finite number }`)}}function se(e){return Object.entries(e).map(([t,o])=>le(o.position)?{key:t,title:o.title,text:o.text,kind:"free",target:null,position:{top:o.position.top,left:o.position.left}}:{key:t,title:o.title,text:o.text,kind:"element",target:null,position:null})}function ae(e,t={}){let o=t.minDistance??26,r=t.iterations??6,n=e.map(l=>({x:l.x,y:l.y}));for(let l=0;l<r;l++){let i=!1;for(let a=0;a<n.length;a++)for(let c=a+1;c<n.length;c++){let m=n[a],h=n[c],u=h.x-m.x,v=h.y-m.y,w=Math.hypot(u,v);if(w>=o)continue;w===0&&(u=1,v=0,w=1);let E=(o-w)/2,b=u/w,f=v/w;m.x-=b*E,m.y-=f*E,h.x+=b*E,h.y+=f*E,i=!0}if(!i)break}return n.map((l,i)=>({dx:l.x-e[i].x,dy:l.y-e[i].y}))}var pe="help-layer-target-highlight",Ce=24;function Ae(e){return e.kind==="free"?H(()=>({top:e.position.top,left:e.position.left,width:0,height:0})):e.target}function ce(e,{onMarkerClick:t,onOverlapResolved:o,onMarkerHidden:r,markerLabel:n="?",markerPlacement:l="top-end"}){let i=new Map,a=null,c=!1,m=0,h=-1;function u(){if(c||i.size===0)return;let p=document.body.getBoundingClientRect(),s=document.body.clientLeft,g=document.body.clientTop,y=[];for(let d of i.values()){if(j(d.reference)){d.hidden||(d.hidden=!0,d.lastBaseEl=null,d.el.style.setProperty("display","none","important"),r&&r(d.record));continue}d.hidden&&(d.hidden=!1,d.el.style.removeProperty("display")),d.refRect=d.reference.getBoundingClientRect(),y.push(d)}if(!m&&y.length){let d=y[0].el.offsetWidth;d>0&&(m=d)}let T=m||Ce,L=y.length!==h;h=y.length;let A=[],$=[];for(let d of y){let P=Q(d.refRect,T,d.placement);$.push({x:P.left+T/2,y:P.top+T/2});let k=d.strategy==="fixed"?{left:P.left,top:P.top}:D(P.left,P.top,p,s,g);A.push(k),(!d.lastBaseEl||d.lastBaseEl.left!==k.left||d.lastBaseEl.top!==k.top)&&(L=!0),d.lastBaseEl=k}if(L&&y.length){let d=ae($),P=!1;for(let k=0;k<y.length;k++){let C=y[k],O=A[k].left+d[k].dx,M=A[k].top+d[k].dy;(C.lastLeft!==O||C.lastTop!==M)&&(C.el.style.left=`${O}px`,C.el.style.top=`${M}px`,C.lastLeft=O,C.lastTop=M,P=!0)}P&&o&&o()}}function v(){a=null,!(c||i.size===0)&&(u(),a=requestAnimationFrame(v))}function w(){a!==null||c||i.size===0||(a=requestAnimationFrame(v))}function E(p){if(i.has(p.id))return;let s=U(p.title,n);document.body.appendChild(s);let g=()=>t(p,s);s.addEventListener("click",g);let y=Ae(p),T=I(y)?"fixed":"absolute";T==="fixed"&&s.style.setProperty("position","fixed","important");let L=p.kind==="element"?p.target:null,A=()=>L&&L.classList.add(pe),$=()=>L&&L.classList.remove(pe);L&&(s.addEventListener("mouseenter",A),s.addEventListener("mouseleave",$),s.addEventListener("focus",A),s.addEventListener("blur",$));let d=!1,P=()=>{d||(d=!0,s.removeEventListener("click",g),L&&(s.removeEventListener("mouseenter",A),s.removeEventListener("mouseleave",$),s.removeEventListener("focus",A),s.removeEventListener("blur",$),$()),s.remove(),i.delete(p.id),w())};i.set(p.id,{record:p,el:s,reference:y,strategy:T,placement:l,cleanup:P,hidden:!1,lastBaseEl:null,lastLeft:void 0,lastTop:void 0}),w()}function b(p){let s=i.get(p);s&&s.cleanup()}function f(p){p.forEach(E),u()}return e.track(()=>{c=!0,a!==null&&(cancelAnimationFrame(a),a=null),[...i.values()].forEach(p=>p.cleanup())}),{mount:E,unmount:b,mountAll:f,has(p){return i.has(p)},findByKey(p){for(let s of i.values())if(s.record.key===p)return s;return null}}}function S(e,t,...o){if(t)try{return t(...o)}catch(r){console.error(`[help-layer] ${e} threw:`,r);return}}var Te=1;function F(e,t,o,r){typeof e.querySelectorAll=="function"&&e.querySelectorAll("*").forEach(n=>{o&&n.matches(t)&&o(n),n.shadowRoot&&(r&&r(n.shadowRoot),F(n.shadowRoot,t,o,r))})}function fe(e,t){let o=[];return F(e,t,r=>o.push(r)),o}function Pe(e){let t=[];return F(e,"*",null,o=>t.push(o)),t}function ue(e,t){let o=[],r=[];if(e.nodeType!==Te)return{matches:o,shadowRoots:r};let n=e;return typeof n.matches=="function"&&n.matches(t)&&o.push(n),n.shadowRoot&&(r.push(n.shadowRoot),F(n.shadowRoot,t,l=>o.push(l),l=>r.push(l))),F(n,t,l=>o.push(l),l=>r.push(l)),{matches:o,shadowRoots:r}}function de({root:e=document,selector:t,onAdded:o,onRemoved:r}){let n=new Set,l=c=>{for(let m of c)m.addedNodes.forEach(h=>{let{matches:u,shadowRoots:v}=ue(h,t);u.forEach(w=>S("observer onAdded",o,w)),v.forEach(a)}),m.removedNodes.forEach(h=>{ue(h,t).matches.forEach(u=>S("observer onRemoved",r,u))})},i=new MutationObserver(l);function a(c){n.has(c)||(n.add(c),i.observe(c,{childList:!0,subtree:!0}))}return a(e),Pe(e).forEach(a),{disconnect(){i.disconnect(),n.clear()}}}var R="data-help-title",K="data-help-text";function V(e="data-help-id"){return`[${e}], [${R}]`}function z(e){let t=new Map;for(let o of e)o.kind==="element"&&t.set(o.key,o);return t}function he(e){return e.filter(t=>t.kind==="free").map(t=>({id:t.key,kind:"free",key:t.key,title:t.title,text:t.text,position:t.position}))}function Z(e,t,o="data-help-id"){let r=e.getAttribute(o),n=r!=null?t.get(r):void 0,l=n?n.title:e.getAttribute(R),i=n?n.text:e.getAttribute(K);return!l||!i?null:{id:e,kind:"element",key:r,title:l,text:i,target:e}}function me(e,t=document,{silent:o=!1,attribute:r="data-help-id"}={}){let n=z(e),l=[];return fe(t,V(r)).forEach(i=>{let a=Z(i,n,r);if(!a){if(!o){let c=i.getAttribute(r);console.warn(c!=null?`[help-layer] element with ${r}="${c}" has no matching helpConfig entry or inline ${R}/${K}`:`[help-layer] element needs both ${R} and ${K} (or a ${r} matching helpConfig)`)}return}l.push(a)}),l}function ye(e,{onClose:t,render:o,popupPlacement:r="bottom-start"}={}){let{root:n,titleEl:l,textEl:i,closeEl:a}=Y();n.style.setProperty("display","none","important"),document.body.appendChild(n),a.addEventListener("click",()=>p());let c=null,m=null,h=null;function u(){h&&(h.cleanup(),h=null)}let v='a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])';function w(s){if(s.key!=="Tab")return;let g=[...n.querySelectorAll(v)].filter(A=>A instanceof HTMLElement);if(s.preventDefault(),g.length===0){n.focus({preventScroll:!0});return}let y=g.length,T=g.indexOf(document.activeElement instanceof HTMLElement?document.activeElement:null);(T===-1?s.shiftKey?g[y-1]:g[0]:g[(T+(s.shiftKey?-1:1)+y)%y]).focus({preventScroll:!0})}function E(s,g){l.textContent=s.title;let y=S("render",o,s);i.textContent="",y?i.appendChild(y):i.textContent=s.text,n.style.setProperty("display","block","important"),c=s.id,m=g,u(),h=ne(g,n,r),n.addEventListener("keydown",w,!0),n.focus({preventScroll:!0})}function b(){h&&h.update()}function f(){let s=c!==null;u(),n.removeEventListener("keydown",w,!0),c=null,m=null,n.style.setProperty("display","none","important"),s&&S("onClose",t)}function p(s){let g=s??m;f(),g&&g.isConnected&&typeof g.focus=="function"&&g.focus({preventScroll:!0})}return e.track(()=>{f(),n.remove()}),{root:n,isOpen(s){return c===s},getOpenId(){return c},open:E,close:p,reposition:b}}function ge(){let e=[];return{track(t){e.push(t)},teardownAll(){for(;e.length>0;){let t=e.pop();try{t()}catch(o){console.error("[help-layer] teardown step threw:",o)}}}}}var Se="data-help-layer-style",$e=`
5
5
  .help-layer-blocking-layer {
6
6
  /* Structural properties !important so a host can't accidentally un-fix or restack the layer and
7
7
  defeat the blocking guarantee. */
@@ -36,15 +36,15 @@ function B(e,{toggleEl:t,isLibraryNode:o}){let r=new Set;function n(a){if(a.node
36
36
  pointer-events: auto !important;
37
37
  top: 0;
38
38
  left: 0;
39
- width: var(--help-layer-marker-size, 22px) !important;
40
- height: var(--help-layer-marker-size, 22px) !important;
39
+ width: var(--help-layer-marker-size, 24px) !important;
40
+ height: var(--help-layer-marker-size, 24px) !important;
41
41
  border-radius: 50%;
42
42
  background: var(--help-layer-marker-bg, #2563eb);
43
43
  color: var(--help-layer-marker-color, #fff);
44
44
  font-family: sans-serif;
45
45
  font-size: 13px;
46
46
  font-weight: bold;
47
- line-height: var(--help-layer-marker-size, 22px);
47
+ line-height: var(--help-layer-marker-size, 24px);
48
48
  text-align: center;
49
49
  cursor: pointer;
50
50
  user-select: none;
@@ -115,8 +115,8 @@ function B(e,{toggleEl:t,isLibraryNode:o}){let r=new Set;function n(a){if(a.node
115
115
  pointer-events: auto !important;
116
116
  top: 6px;
117
117
  right: 6px;
118
- width: 22px;
119
- height: 22px;
118
+ width: 24px;
119
+ height: 24px;
120
120
  padding: 0;
121
121
  border: none;
122
122
  border-radius: 4px;
@@ -156,5 +156,5 @@ function B(e,{toggleEl:t,isLibraryNode:o}){let r=new Set;function n(a){if(a.node
156
156
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.55);
157
157
  }
158
158
  }
159
- `;function ue(e){let t=document.createElement("style");return t.setAttribute(Pe,""),e&&t.setAttribute("nonce",e),t.textContent=$e,document.head.appendChild(t),t}function fe(e){e.remove()}function _e(e){if(typeof e=="string"){let t=document.querySelector(e);if(!t)throw new Error(`help-layer: toggle element not found for selector "${e}"`);return t}if(e instanceof HTMLElement)return e;throw new Error("help-layer: toggle must be a CSS selector string or a DOM element")}function de(e){if(!T(e))throw new Error("help-layer: initHelpLayer requires an options object");let{config:t,toggle:o,onEnable:r,onDisable:n,onOpen:i,onClose:a,silent:c=!1,attribute:d="data-help-id",render:y,markerLabel:m="?",markerPlacement:f="top-end",popupPlacement:g="bottom-start",nonce:b}=e,p=t;M(p);let l=o!=null?_e(o):null,u=null,s=null,h=null;function w(){if(u)return;u=ce(),l&&u.track(()=>{l.isConnected&&typeof l.focus=="function"&&l.focus({preventScroll:!0})});let L=ue(b);u.track(()=>fe(L));let E=ee(p),xe=I(E);s=pe(u,{onClose:a,render:y,popupPlacement:g}),h=ne(u,{markerLabel:m,markerPlacement:f,onMarkerClick:(x,S)=>{if(s.isOpen(x.id)){s.close();return}s.open(x,S),k("onOpen",i,x)},onOverlapResolved:()=>s.reposition(),onMarkerHidden:x=>{s.isOpen(x.id)&&s.close(l??void 0)}}),h.mountAll(le(E)),h.mountAll(se(E,document,{silent:c,attribute:d}));let be=ae({selector:D(d),onAdded:x=>{let S=j(x,xe,d);S&&!h.has(S.id)&&h.mount(S)},onRemoved:x=>{s.isOpen(x)&&s.close(l??void 0),h.unmount(x)}});u.track(()=>be.disconnect());let ge=J(u,{toggleEl:l,onBackgroundClick:()=>s.close(),isLibraryElement:x=>!!x&&((l?l.contains(x):!1)||s.root.contains(x)||typeof x.closest=="function"&&!!x.closest(".help-layer-marker")),onEscape:()=>{s.getOpenId()!==null?s.close():A()}});B(u,{toggleEl:l,isLibraryNode:x=>x===ge||x===s.root||!!x.classList&&x.classList.contains("help-layer-marker")})}function v(){u&&(u.teardownAll(),u=null,s=null,h=null)}function C(){u||(w(),k("onEnable",r))}function A(){u&&(v(),k("onDisable",n))}function _(){u?A():C()}function he(L){if(u||C(),!h||!s)return;let E=h.findByKey(L);if(!E){c||console.warn(`[help-layer] open(): no help marker for key "${L}"`);return}s.open(E.record,E.el),k("onOpen",i,E.record)}function me(){s&&s.close()}function ye(L){M(L),p=L,u&&(v(),w())}return l&&l.addEventListener("click",_),{enable:C,disable:A,toggle:_,isActive(){return u!==null},open:he,close:me,update:ye,destroy(){A(),l&&l.removeEventListener("click",_)}}}function bt(e){return de(e)}export{bt as initHelpLayer};
159
+ `;function be(e){let t=document.createElement("style");return t.setAttribute(Se,""),e&&t.setAttribute("nonce",e),t.textContent=$e,document.head.appendChild(t),t}function xe(e){e.remove()}function _e(e){if(typeof e=="string"){let t=document.querySelector(e);if(!t)throw new Error(`help-layer: toggle element not found for selector "${e}"`);return t}if(e instanceof HTMLElement)return e;throw new Error("help-layer: toggle must be a CSS selector string or a DOM element")}function ve(e){if(!N(e))throw new Error("help-layer: initHelpLayer requires an options object");let{config:t,toggle:o,onEnable:r,onDisable:n,onOpen:l,onClose:i,silent:a=!1,attribute:c="data-help-id",render:m,markerLabel:h="?",markerPlacement:u="top-end",popupPlacement:v="bottom-start",nonce:w}=e,E=t;q(E);let b=o!=null?_e(o):null,f=null,p=null,s=null;function g(){if(f)return;f=ge(),b&&f.track(()=>{b.isConnected&&typeof b.focus=="function"&&b.focus({preventScroll:!0})});let k=be(w);f.track(()=>xe(k));let C=se(E),O=z(C);p=ye(f,{onClose:i,render:m,popupPlacement:v}),s=ce(f,{markerLabel:h,markerPlacement:u,onMarkerClick:(x,B)=>{if(p.isOpen(x.id)){p.close();return}p.open(x,B),S("onOpen",l,x)},onOverlapResolved:()=>p.reposition(),onMarkerHidden:x=>{p.isOpen(x.id)&&p.close(b??void 0)}}),s.mountAll(he(C)),s.mountAll(me(C,document,{silent:a,attribute:c}));let M=de({selector:V(c),onAdded:x=>{let B=Z(x,O,c);B&&!s.has(B.id)&&s.mount(B)},onRemoved:x=>{p.isOpen(x)&&p.close(b??void 0),s.unmount(x)}});f.track(()=>M.disconnect());let ke=ie(f,{toggleEl:b,onBackgroundClick:()=>p.close(),isLibraryElement:x=>!!x&&((b?b.contains(x):!1)||p.root.contains(x)||typeof x.closest=="function"&&!!x.closest(".help-layer-marker")),onEscape:()=>{p.getOpenId()!==null?p.close():L()}});W(f,{toggleEl:b,isLibraryNode:x=>x===ke||x===p.root||!!x.classList&&x.classList.contains("help-layer-marker")})}function y(){f&&(f.teardownAll(),f=null,p=null,s=null)}function T(){f||(g(),S("onEnable",r))}function L(){f&&(y(),S("onDisable",n))}function A(){f?L():T()}function $(k){if(f||T(),!s||!p)return;let C=s.findByKey(k);if(!C){a||console.warn(`[help-layer] open(): no help marker for key "${k}"`);return}p.open(C.record,C.el),S("onOpen",l,C.record)}function d(){p&&p.close()}function P(k){q(k),E=k,f&&(y(),g())}return b&&b.addEventListener("click",A),{enable:T,disable:L,toggle:A,isActive(){return f!==null},open:$,close:d,update:P,destroy(){L(),b&&b.removeEventListener("click",A)}}}function kt(e){return ve(e)}export{kt as initHelpLayer};
160
160
  //# sourceMappingURL=help-layer.esm.js.map