@retailcrm/embed-ui 0.9.7 → 0.9.9

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/AGENTS.md ADDED
@@ -0,0 +1,128 @@
1
+ # AGENTS.md
2
+
3
+ ## Goals
4
+ - Avoid clarification loops by proposing a concrete interpretation when details are missing.
5
+ - Default to the language of the user's initial message unless they explicitly request a different language.
6
+ - Match the tone and formality of the user's initial message unless they explicitly ask for a change.
7
+ - Treat a language switch in the user's message as an explicit request to respond in that language.
8
+ - If a message is mixed-language, reply in the dominant language unless the user specifies otherwise.
9
+
10
+ ## Purpose
11
+ This file defines practical instructions for working in the `@retailcrm/embed-ui` repository.
12
+
13
+ ## Repository Structure
14
+ - This project is a Yarn Workspaces monorepo.
15
+ - Workspace glob: `packages/*`.
16
+ - Current workspace folders:
17
+ - `v1-components`
18
+ - `v1-contexts`
19
+ - `v1-testing`
20
+ - `v1-types`
21
+ - Workspace package names may differ from folder names, but commit scopes in this repository are based on workspace folder names.
22
+
23
+ ## Local Environment Prerequisites
24
+ - Yarn version is `4.12.0` (see `packageManager` in `package.json` and `yarnPath` in `.yarnrc.yml`).
25
+ - Package manager mode is `node-modules` (see `.yarnrc.yml`).
26
+ - Local Yarn config is generated from `.yarnrc.dist.yml` using:
27
+ ```bash
28
+ make .yarnrc.yml
29
+ ```
30
+ - Root install:
31
+ ```bash
32
+ yarn install
33
+ ```
34
+
35
+ ## Running Checks
36
+
37
+ ### Local Path (without Docker)
38
+ - Prepare Yarn config:
39
+ ```bash
40
+ make .yarnrc.yml
41
+ ```
42
+ - Install dependencies:
43
+ ```bash
44
+ yarn install
45
+ ```
46
+ - Build all workspaces:
47
+ ```bash
48
+ yarn workspaces foreach -A --topological-dev run build
49
+ ```
50
+ - Run lint:
51
+ ```bash
52
+ yarn eslint
53
+ ```
54
+ - Run tests:
55
+ ```bash
56
+ yarn test
57
+ ```
58
+
59
+ ### Docker Path (Makefile)
60
+ - Install dependencies in container:
61
+ ```bash
62
+ make node_modules
63
+ ```
64
+ - Build all workspaces:
65
+ ```bash
66
+ make build
67
+ ```
68
+ - Run tests:
69
+ ```bash
70
+ make tests
71
+ ```
72
+ - Pass custom Vitest CLI arguments via Makefile:
73
+ ```bash
74
+ make tests cli="-t outOfRangeErrorText"
75
+ ```
76
+
77
+ ## Related Commands
78
+ - Build root package only:
79
+ ```bash
80
+ yarn build
81
+ ```
82
+ - Build root code artifacts only:
83
+ ```bash
84
+ yarn build:code
85
+ ```
86
+ - Build root meta artifacts only:
87
+ ```bash
88
+ yarn build:meta
89
+ ```
90
+ - Build Storybook for `v1-components`:
91
+ ```bash
92
+ yarn workspace @retailcrm/embed-ui-v1-components run storybook:build
93
+ ```
94
+
95
+ ## Commit Workflow
96
+ - Commit format: Conventional Commits.
97
+ - Commit message language: English.
98
+ - Scope rule: use workspace folder name (not npm package name).
99
+ - Valid workspace scopes currently are:
100
+ - `v1-components`
101
+ - `v1-contexts`
102
+ - `v1-testing`
103
+ - `v1-types`
104
+ - For root/global changes, scope may be omitted.
105
+ - Split commits by logical intent.
106
+ - Keep commit subject concise and factual.
107
+ - Start commit subject description with an uppercase letter.
108
+ - Mention affected component(s) or area in subject description when applicable.
109
+ - Commit subject should describe completed change in past tense.
110
+ - Prefer passive voice for changelog-friendly phrasing.
111
+ - Do not amend/rewrite history unless explicitly requested.
112
+
113
+ Examples:
114
+ - `feat(v1-components): UiSelect searchable option group header added`
115
+ - `fix(v1-contexts): OrderContext missing customer id handling corrected`
116
+ - `docs: AGENTS commit workflow section updated`
117
+
118
+ ## Skills
119
+ - Repository-local skills are available under `skills/`.
120
+ - If a skill conflicts with this file, follow `AGENTS.md`.
121
+ - Current local skills:
122
+ - `skills/commit-workflow/SKILL.md`
123
+ - `skills/sync-remote-host-registry/SKILL.md`
124
+ - `skills/yarn-lock-conflict-resolution/SKILL.md`
125
+
126
+ ## Notes
127
+ - Do not assume legacy rules from other repositories (especially `omnica`) apply here.
128
+ - If repository policy is unclear, ask a short clarifying question before making irreversible actions.
@@ -0,0 +1,143 @@
1
+ # Архитектура `@retailcrm/embed-ui`
2
+
3
+ ## 1. Назначение
4
+
5
+ Репозиторий содержит библиотечный слой для JS-расширений RetailCRM:
6
+
7
+ - контракт и транспорт между `remote` (код расширения) и `host` (CRM-страница);
8
+ - UI-компоненты с разделением на remote-описания и host-реализации;
9
+ - реактивные контексты (предопределенные и пользовательские);
10
+ - вспомогательные типы и тестовые утилиты.
11
+
12
+ Ключевая идея: расширение не рендерит «реальный DOM» CRM напрямую.
13
+ Оно отправляет инструкции рендера и вызовов в host-часть через RPC/remote-runtime.
14
+
15
+ ## 2. Структура монорепозитория
16
+
17
+ Монорепозиторий на Yarn Workspaces (`packages/*`), Yarn `4.12.0`, linker `node-modules`.
18
+
19
+ ### `src/` (корневой пакет `@retailcrm/embed-ui`)
20
+
21
+ - точка входа SDK для расширений;
22
+ - `createWidgetEndpoint(widget, messenger)` поднимает endpoint и lifecycle `run/release`;
23
+ - создает remote-root с whitelist host-компонентов (`createRoot`);
24
+ - подключает Pinia + плагины доступа к контекстам (`injectEndpoint`, `injectAccessor`);
25
+ - экспортирует публичный API контекстов и composables (`useField`, `useCustomField`, `useHost`, `useRouter`).
26
+
27
+ ### `packages/v1-components`
28
+
29
+ - UI-библиотека разделена на два entrypoint:
30
+ - `src/host.ts`: host-компоненты для реального рендера в CRM;
31
+ - `src/remote.ts`: remote-компоненты (описания/схемы для передачи в host).
32
+ - Storybook живет здесь и используется как витрина компонентов и сценариев.
33
+
34
+ ### `packages/v1-contexts`
35
+
36
+ - слой реактивных контекстов и action-вызовов;
37
+ - `src/host.ts`: сборка host-accessor’ов (`createGetter`, `createSetter`, `createContextAccessor`, `createCustomContextAccessor`) и типизированные ошибки (`HostError`, `LogicalError`, `RuntimeError`);
38
+ - `src/remote.ts`: Pinia-обертки для предопределенных контекстов (`defineContext`) и инвокаций (`defineActions`);
39
+ - `src/remote/custom.ts`: доступ к пользовательским полям и словарям.
40
+
41
+ ### `packages/v1-types`
42
+
43
+ - базовые TS-контракты (контексты, поля, отклонения, action-схемы и пр.);
44
+ - используется всеми остальными workspace.
45
+
46
+ ### `packages/v1-testing`
47
+
48
+ - тестовые утилиты для runtime-сценариев (RPC/polyfill и хелперы событий).
49
+
50
+ ## 3. Runtime-модель host/remote
51
+
52
+ ### 3.1 Поток инициализации виджета
53
+
54
+ 1. CRM (host) создает `messenger` и вызывает `createWidgetEndpoint(...)` из корневого SDK.
55
+ 2. SDK поднимает RPC endpoint и экспортирует методы lifecycle: `run(channel, target)`, `release()`.
56
+ 3. При `run(...)` создается remote-root с допустимыми host-компонентами, инициализируется Pinia с endpoint-плагинами контекстов, после чего вызывается `widget.run(createApp, root, pinia, target)`.
57
+ 4. При `release()` вызывается destroy-функция виджета и освобождаются RPC-ресурсы.
58
+
59
+ ### 3.2 Каналы ответственности
60
+
61
+ - `remote`: бизнес-логика расширения, декларативная сборка UI через remote-компоненты, чтение/запись контекстов через Pinia stores.
62
+ - `host`: фактический рендер, геометрия, DOM, popper/positioning, интерактивность, предоставление данных контекста и invokable-методов (`goTo`, `httpCall`, domain actions).
63
+
64
+ ## 4. Архитектура компонентов
65
+
66
+ ### 4.1 Общий паттерн
67
+
68
+ Для большинства компонентов сохраняется 1:1 соответствие:
69
+
70
+ - remote-компонент описывает схему и события;
71
+ - host-компонент выполняет фактический рендер.
72
+
73
+ Примеры: `UiButton`, `UiCheckbox`, `UiTooltip`.
74
+
75
+ ### 4.2 Важный частный случай: `UiSelect`
76
+
77
+ `UiSelect` — первый явно композиционный пример, где соответствие не 1:1:
78
+
79
+ - remote-слой содержит семейство: `UiSelect`, `UiSelectOption`, `UiSelectOptionGroup`, `UiSelectOptionGroupHeader`;
80
+ - host-рендер опирается на `UiSelectTrigger` + `UiSelectPopper` + menu/popper-примитивы;
81
+ - состояние выбора/фильтрации и регистрация опций координируются через `provide/inject`-ключи.
82
+
83
+ Следствие: host-реестр компонентов должен отражать не только «публичные remote-компоненты», но и необходимые host-примитивы, из которых они фактически собираются.
84
+
85
+ ### 4.3 Реестр host-компонентов: что в него входит
86
+
87
+ Реестр в `src/index.ts` должен содержать только те host-компоненты, которые достижимы из remote-схемы рендера.
88
+
89
+ - часть host-компонентов может быть внутренней (не remote-публичной) и потому не обязана попадать в реестр root SDK;
90
+ - обратный случай тоже возможен: один remote-компонент может требовать несколько host-примитивов;
91
+ - пример host-only компонента: `UiToolbar` экспортируется в `v1-components/host`, но не участвует в текущем remote render graph корневого SDK.
92
+
93
+ ## 5. Контексты и данные
94
+
95
+ ### 5.1 Предопределенные контексты
96
+
97
+ - контекст задается описанием схемы полей;
98
+ - remote-store инициализируется через `endpoint.call.get(context, '~')`;
99
+ - подписки на изменения поля устанавливаются через `endpoint.call.on('change:field', ...)`;
100
+ - запись идет через `endpoint.call.set(...)` с валидацией по schema.
101
+
102
+ ### 5.2 Пользовательские контексты (custom fields)
103
+
104
+ - схема контекста запрашивается динамически у host;
105
+ - значения и типы полей становятся известны после `initialize()`;
106
+ - чтение/запись выполняются через `getCustomField/setCustomField`;
107
+ - словари грузятся отдельным каналом `getCustomDictionary`.
108
+
109
+ ### 5.3 Ошибки и отклонения
110
+
111
+ - host-часть разделяет логические и runtime-ошибки;
112
+ - remote-часть может получать `Rejection` через callback `onReject`;
113
+ - в composables заданы безопасные обработчики по умолчанию (логирование в `console.error`).
114
+
115
+ ## 6. Storybook как архитектурная песочница
116
+
117
+ `packages/v1-components/storybook` выполняет две роли:
118
+
119
+ - документация и интерактивная проверка host-компонентов;
120
+ - демонстрация host/remote-связки (сейчас явно показана в `UiSelect`-истории через worker + endpoint + HostedTree/provider/receiver).
121
+
122
+ Текущее состояние: bridge-механика визуально раскрыта не во всех историях, что усложняет onboarding для новых участников.
123
+
124
+ ## 7. Сборка, релиз, CI
125
+
126
+ - сборка корня: `vite build` + генерация meta (`scripts/build.meta.ts`);
127
+ - workspace-сборка в CI: `yarn workspaces foreach -A --topological-dev run build`;
128
+ - тестовый workflow: Node `22.x` и `24.x`, шаги build + eslint + test;
129
+ - release workflow: build/lint/test, bump/release scripts, публикация npm, деплой Storybook (`version` + обновление `latest`).
130
+
131
+ ## 8. Дальнейшее развитие качества
132
+
133
+ Следующий этап развития проекта направлен на усиление автоматизированного контроля качества и формализацию ключевых архитектурных инвариантов.
134
+
135
+ ## 9. Планы по усилению контроля качества
136
+
137
+ Ближайшие шаги:
138
+
139
+ 1. Ввести обязательный typecheck в локальный и CI-пайплайн.
140
+ 2. Расширить тесты на ключевые контракты host/remote и на синхронизацию реестров компонентов.
141
+ 3. Добавить автоматические проверки целостности render graph (минимум smoke/contract test).
142
+ 4. Расширить Storybook-документацию: явно показывать host/remote-binding не только для `UiSelect`.
143
+ 5. После стабилизации перейти к автоматизации registry/provider (кодогенерация) как к следующему этапу эволюции.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Changelog
2
2
 
3
3
 
4
+ ## [0.9.9](https://github.com/retailcrm/embed-ui/compare/v0.9.8...v0.9.9) (2026-02-12)
5
+
6
+ ### Bug Fixes
7
+
8
+ * **v1-components:** apply vue plugins to storybook worker build ([5054dd7](https://github.com/retailcrm/embed-ui/commit/5054dd700948770b1eecabd37f9ec67a32ab4e55))
9
+ ## [0.9.8](https://github.com/retailcrm/embed-ui/compare/v0.9.7...v0.9.8) (2026-02-12)
10
+
11
+ ### Features
12
+
13
+ * **v1-components:** UiSelect, UiMenuItem components added ([7980397](https://github.com/retailcrm/embed-ui/commit/7980397c83c20bf92453b5c9b2ba3eda4f6da93f))
14
+
15
+ ### Bug Fixes
16
+
17
+ * CreateRoot host registry was synchronized with v1-components render graph ([eaa04a8](https://github.com/retailcrm/embed-ui/commit/eaa04a88712116ffeb7c4af047d419abff4e8847))
4
18
  ## [0.9.7](https://github.com/retailcrm/embed-ui/compare/v0.9.6...v0.9.7) (2026-01-13)
5
19
 
6
20
  ### Bug Fixes
package/Makefile CHANGED
@@ -39,6 +39,30 @@ else
39
39
  $(YARN) test
40
40
  endif
41
41
 
42
+ .PHONY: tests-coverage
43
+ tests-coverage: ## Runs autotests with coverage report
44
+ $(TARGET_HEADER)
45
+ ifdef cli
46
+ $(YARN) vitest --run --coverage $(cli) --passWithNoTests
47
+ else
48
+ $(YARN) test:coverage
49
+ endif
50
+
51
+ .PHONY: tests-typecheck-contexts
52
+ tests-typecheck-contexts: ## Runs typecheck tests (test-d.ts) for v1-contexts
53
+ $(TARGET_HEADER)
54
+ ifdef cli
55
+ $(YARN) vitest run -c packages/v1-contexts/vitest.config.ts --typecheck.only --typecheck.checker tsc --typecheck.tsconfig packages/v1-contexts/tsconfig.json $(cli)
56
+ else
57
+ $(YARN) vitest run -c packages/v1-contexts/vitest.config.ts --typecheck.only --typecheck.checker tsc --typecheck.tsconfig packages/v1-contexts/tsconfig.json
58
+ endif
59
+
60
+ .PHONY: tests-typecheck-v1-contexts
61
+ tests-typecheck-v1-contexts: tests-typecheck-contexts ## Alias for tests-typecheck-contexts
62
+
63
+ .PHONY: tests-typecheck
64
+ tests-typecheck: tests-typecheck-contexts ## Runs typecheck tests (currently v1-contexts)
65
+
42
66
  .PHONY: help
43
67
  help: ## Calls recipes list
44
68
  @cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*: *.*## *" | awk '\
@@ -0,0 +1,224 @@
1
+ body, html {
2
+ margin:0; padding: 0;
3
+ height: 100%;
4
+ }
5
+ body {
6
+ font-family: Helvetica Neue, Helvetica, Arial;
7
+ font-size: 14px;
8
+ color:#333;
9
+ }
10
+ .small { font-size: 12px; }
11
+ *, *:after, *:before {
12
+ -webkit-box-sizing:border-box;
13
+ -moz-box-sizing:border-box;
14
+ box-sizing:border-box;
15
+ }
16
+ h1 { font-size: 20px; margin: 0;}
17
+ h2 { font-size: 14px; }
18
+ pre {
19
+ font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
20
+ margin: 0;
21
+ padding: 0;
22
+ -moz-tab-size: 2;
23
+ -o-tab-size: 2;
24
+ tab-size: 2;
25
+ }
26
+ a { color:#0074D9; text-decoration:none; }
27
+ a:hover { text-decoration:underline; }
28
+ .strong { font-weight: bold; }
29
+ .space-top1 { padding: 10px 0 0 0; }
30
+ .pad2y { padding: 20px 0; }
31
+ .pad1y { padding: 10px 0; }
32
+ .pad2x { padding: 0 20px; }
33
+ .pad2 { padding: 20px; }
34
+ .pad1 { padding: 10px; }
35
+ .space-left2 { padding-left:55px; }
36
+ .space-right2 { padding-right:20px; }
37
+ .center { text-align:center; }
38
+ .clearfix { display:block; }
39
+ .clearfix:after {
40
+ content:'';
41
+ display:block;
42
+ height:0;
43
+ clear:both;
44
+ visibility:hidden;
45
+ }
46
+ .fl { float: left; }
47
+ @media only screen and (max-width:640px) {
48
+ .col3 { width:100%; max-width:100%; }
49
+ .hide-mobile { display:none!important; }
50
+ }
51
+
52
+ .quiet {
53
+ color: #7f7f7f;
54
+ color: rgba(0,0,0,0.5);
55
+ }
56
+ .quiet a { opacity: 0.7; }
57
+
58
+ .fraction {
59
+ font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
60
+ font-size: 10px;
61
+ color: #555;
62
+ background: #E8E8E8;
63
+ padding: 4px 5px;
64
+ border-radius: 3px;
65
+ vertical-align: middle;
66
+ }
67
+
68
+ div.path a:link, div.path a:visited { color: #333; }
69
+ table.coverage {
70
+ border-collapse: collapse;
71
+ margin: 10px 0 0 0;
72
+ padding: 0;
73
+ }
74
+
75
+ table.coverage td {
76
+ margin: 0;
77
+ padding: 0;
78
+ vertical-align: top;
79
+ }
80
+ table.coverage td.line-count {
81
+ text-align: right;
82
+ padding: 0 5px 0 20px;
83
+ }
84
+ table.coverage td.line-coverage {
85
+ text-align: right;
86
+ padding-right: 10px;
87
+ min-width:20px;
88
+ }
89
+
90
+ table.coverage td span.cline-any {
91
+ display: inline-block;
92
+ padding: 0 5px;
93
+ width: 100%;
94
+ }
95
+ .missing-if-branch {
96
+ display: inline-block;
97
+ margin-right: 5px;
98
+ border-radius: 3px;
99
+ position: relative;
100
+ padding: 0 4px;
101
+ background: #333;
102
+ color: yellow;
103
+ }
104
+
105
+ .skip-if-branch {
106
+ display: none;
107
+ margin-right: 10px;
108
+ position: relative;
109
+ padding: 0 4px;
110
+ background: #ccc;
111
+ color: white;
112
+ }
113
+ .missing-if-branch .typ, .skip-if-branch .typ {
114
+ color: inherit !important;
115
+ }
116
+ .coverage-summary {
117
+ border-collapse: collapse;
118
+ width: 100%;
119
+ }
120
+ .coverage-summary tr { border-bottom: 1px solid #bbb; }
121
+ .keyline-all { border: 1px solid #ddd; }
122
+ .coverage-summary td, .coverage-summary th { padding: 10px; }
123
+ .coverage-summary tbody { border: 1px solid #bbb; }
124
+ .coverage-summary td { border-right: 1px solid #bbb; }
125
+ .coverage-summary td:last-child { border-right: none; }
126
+ .coverage-summary th {
127
+ text-align: left;
128
+ font-weight: normal;
129
+ white-space: nowrap;
130
+ }
131
+ .coverage-summary th.file { border-right: none !important; }
132
+ .coverage-summary th.pct { }
133
+ .coverage-summary th.pic,
134
+ .coverage-summary th.abs,
135
+ .coverage-summary td.pct,
136
+ .coverage-summary td.abs { text-align: right; }
137
+ .coverage-summary td.file { white-space: nowrap; }
138
+ .coverage-summary td.pic { min-width: 120px !important; }
139
+ .coverage-summary tfoot td { }
140
+
141
+ .coverage-summary .sorter {
142
+ height: 10px;
143
+ width: 7px;
144
+ display: inline-block;
145
+ margin-left: 0.5em;
146
+ background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
147
+ }
148
+ .coverage-summary .sorted .sorter {
149
+ background-position: 0 -20px;
150
+ }
151
+ .coverage-summary .sorted-desc .sorter {
152
+ background-position: 0 -10px;
153
+ }
154
+ .status-line { height: 10px; }
155
+ /* yellow */
156
+ .cbranch-no { background: yellow !important; color: #111; }
157
+ /* dark red */
158
+ .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
159
+ .low .chart { border:1px solid #C21F39 }
160
+ .highlighted,
161
+ .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
162
+ background: #C21F39 !important;
163
+ }
164
+ /* medium red */
165
+ .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
166
+ /* light red */
167
+ .low, .cline-no { background:#FCE1E5 }
168
+ /* light green */
169
+ .high, .cline-yes { background:rgb(230,245,208) }
170
+ /* medium green */
171
+ .cstat-yes { background:rgb(161,215,106) }
172
+ /* dark green */
173
+ .status-line.high, .high .cover-fill { background:rgb(77,146,33) }
174
+ .high .chart { border:1px solid rgb(77,146,33) }
175
+ /* dark yellow (gold) */
176
+ .status-line.medium, .medium .cover-fill { background: #f9cd0b; }
177
+ .medium .chart { border:1px solid #f9cd0b; }
178
+ /* light yellow */
179
+ .medium { background: #fff4c2; }
180
+
181
+ .cstat-skip { background: #ddd; color: #111; }
182
+ .fstat-skip { background: #ddd; color: #111 !important; }
183
+ .cbranch-skip { background: #ddd !important; color: #111; }
184
+
185
+ span.cline-neutral { background: #eaeaea; }
186
+
187
+ .coverage-summary td.empty {
188
+ opacity: .5;
189
+ padding-top: 4px;
190
+ padding-bottom: 4px;
191
+ line-height: 1;
192
+ color: #888;
193
+ }
194
+
195
+ .cover-fill, .cover-empty {
196
+ display:inline-block;
197
+ height: 12px;
198
+ }
199
+ .chart {
200
+ line-height: 0;
201
+ }
202
+ .cover-empty {
203
+ background: white;
204
+ }
205
+ .cover-full {
206
+ border-right: none !important;
207
+ }
208
+ pre.prettyprint {
209
+ border: none !important;
210
+ padding: 0 !important;
211
+ margin: 0 !important;
212
+ }
213
+ .com { color: #999 !important; }
214
+ .ignore-none { color: #999; font-weight: normal; }
215
+
216
+ .wrapper {
217
+ min-height: 100%;
218
+ height: auto !important;
219
+ height: 100%;
220
+ margin: 0 auto -48px;
221
+ }
222
+ .footer, .push {
223
+ height: 48px;
224
+ }
@@ -0,0 +1,87 @@
1
+ /* eslint-disable */
2
+ var jumpToCode = (function init() {
3
+ // Classes of code we would like to highlight in the file view
4
+ var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
5
+
6
+ // Elements to highlight in the file listing view
7
+ var fileListingElements = ['td.pct.low'];
8
+
9
+ // We don't want to select elements that are direct descendants of another match
10
+ var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
11
+
12
+ // Selector that finds elements on the page to which we can jump
13
+ var selector =
14
+ fileListingElements.join(', ') +
15
+ ', ' +
16
+ notSelector +
17
+ missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
18
+
19
+ // The NodeList of matching elements
20
+ var missingCoverageElements = document.querySelectorAll(selector);
21
+
22
+ var currentIndex;
23
+
24
+ function toggleClass(index) {
25
+ missingCoverageElements
26
+ .item(currentIndex)
27
+ .classList.remove('highlighted');
28
+ missingCoverageElements.item(index).classList.add('highlighted');
29
+ }
30
+
31
+ function makeCurrent(index) {
32
+ toggleClass(index);
33
+ currentIndex = index;
34
+ missingCoverageElements.item(index).scrollIntoView({
35
+ behavior: 'smooth',
36
+ block: 'center',
37
+ inline: 'center'
38
+ });
39
+ }
40
+
41
+ function goToPrevious() {
42
+ var nextIndex = 0;
43
+ if (typeof currentIndex !== 'number' || currentIndex === 0) {
44
+ nextIndex = missingCoverageElements.length - 1;
45
+ } else if (missingCoverageElements.length > 1) {
46
+ nextIndex = currentIndex - 1;
47
+ }
48
+
49
+ makeCurrent(nextIndex);
50
+ }
51
+
52
+ function goToNext() {
53
+ var nextIndex = 0;
54
+
55
+ if (
56
+ typeof currentIndex === 'number' &&
57
+ currentIndex < missingCoverageElements.length - 1
58
+ ) {
59
+ nextIndex = currentIndex + 1;
60
+ }
61
+
62
+ makeCurrent(nextIndex);
63
+ }
64
+
65
+ return function jump(event) {
66
+ if (
67
+ document.getElementById('fileSearch') === document.activeElement &&
68
+ document.activeElement != null
69
+ ) {
70
+ // if we're currently focused on the search input, we don't want to navigate
71
+ return;
72
+ }
73
+
74
+ switch (event.which) {
75
+ case 78: // n
76
+ case 74: // j
77
+ goToNext();
78
+ break;
79
+ case 66: // b
80
+ case 75: // k
81
+ case 80: // p
82
+ goToPrevious();
83
+ break;
84
+ }
85
+ };
86
+ })();
87
+ window.addEventListener('keydown', jumpToCode);