@work-graph/cli 0.2.7 → 0.2.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.
@@ -0,0 +1,471 @@
1
+ /** @typedef {'lead' | 'pipeline' | 'featureColumns' | 'iconLabelGrid' | 'screenshotGallery' | 'graphTrinity' | 'codeShowcase' | 'comparisonStrip' | 'siteSections' | 'boundaries'} PublicPageBlockType */
2
+
3
+ /**
4
+ * @param {'ru' | 'en'} locale
5
+ * @returns {Record<string, { title: string, description: string }>}
6
+ */
7
+ export function getPublicSitePageMeta(locale) {
8
+ if (locale === 'ru') {
9
+ return {
10
+ product: {
11
+ title: 'Продуктовый workflow',
12
+ description:
13
+ 'Локальный UI и intent-граф в git: от AN-аналитики и BVC-задач до доски, матрицы проверок и памяти проекта. Каждый экран закрывает свой слой цикла разработки.',
14
+ },
15
+ evidence: {
16
+ title: 'Журнал доказательств',
17
+ description:
18
+ 'Задача закрывается не словами агента, а вердиктом контракта: команды, трассы, tier-проверки и assert_task_ready_for_done. Всё аудируется в репозитории.',
19
+ },
20
+ compare: {
21
+ title: 'От чат-воркфлоу к контрактному',
22
+ description:
23
+ 'Локальный таск-трекер в git с BVC-контрактами: доска, статусы, evidence и память проекта. Слой намерения и доказательств поверх обычного PM.',
24
+ },
25
+ };
26
+ }
27
+ return {
28
+ product: {
29
+ title: 'Product workflow',
30
+ description:
31
+ 'Local UI and intent graph in git: from AN analytics and BVC work items to the board, verification matrix and project memory. Each surface covers one layer of the development loop.',
32
+ },
33
+ evidence: {
34
+ title: 'Evidence ledger',
35
+ description:
36
+ 'A task closes on a contract verdict — commands, traces, tier checks and assert_task_ready_for_done — not on agent prose. Everything is auditable in the repository.',
37
+ },
38
+ compare: {
39
+ title: 'From chat workflow to contract workflow',
40
+ description:
41
+ 'A local git-backed task tracker with BVC contracts: board, statuses, evidence and project memory. Intent and proof layer beyond classic PM tools.',
42
+ },
43
+ };
44
+ }
45
+
46
+ /**
47
+ * @param {'ru' | 'en'} locale
48
+ * @param {'product' | 'evidence' | 'compare'} kind
49
+ * @returns {Array<Record<string, unknown>>}
50
+ */
51
+ export function getPublicSitePageBlocks(locale, kind) {
52
+ const ru = locale === 'ru';
53
+ const blocks = ru ? PAGE_BLOCKS_RU : PAGE_BLOCKS_EN;
54
+ return blocks[kind] ?? [];
55
+ }
56
+
57
+ /** @type {Record<string, Array<Record<string, unknown>>>} */
58
+ const PAGE_BLOCKS_EN = {
59
+ product: [
60
+ {
61
+ type: 'lead',
62
+ text:
63
+ 'Work Graph is a local task tracker in git — board, backlog and statuses included. The difference is the unit of work: a BVC contract with evidence gates, not a chat ticket. Decisions become AN records, tasks become .bvc atoms, motion on the board, readiness via verification, outcomes in project memory.',
64
+ },
65
+ {
66
+ type: 'pipeline',
67
+ title: 'Five steps in one repository',
68
+ intro: 'Each step has a screen in the UI and a file contract in intent/.',
69
+ steps: [
70
+ { iconName: 'lightbulb', label: 'Decision (AN)', detail: 'Capture why before backlog intake' },
71
+ { iconName: 'clipboard-text', label: 'Work contract (.bvc)', detail: 'Basis, Vector, Goal and checks' },
72
+ { iconName: 'robot', label: 'Agent claim', detail: 'work.id, targetFiles and allowlist' },
73
+ { iconName: 'list-checks', label: 'Evidence', detail: 'Commands, traces and structured records' },
74
+ { iconName: 'brain', label: 'Verified memory', detail: 'Closed work linked to files in git' },
75
+ ],
76
+ },
77
+ {
78
+ type: 'siteSections',
79
+ title: 'Surfaces you work in every day',
80
+ intro: 'Open npm run workgraph:ui on port 4177 after init — the same backlog your MCP agent sees.',
81
+ sections: [
82
+ {
83
+ icon: 'chart-bar',
84
+ title: 'Analytics',
85
+ body: 'AN records hold options, boundaries and lineage before tasks exist. You see which epic or decision a work item came from — not a lost chat thread.',
86
+ },
87
+ {
88
+ icon: 'clipboard-text',
89
+ title: 'BVC work items',
90
+ body: 'Each task is a .bvc atom with Basis, Vector, Goal, checks and labels. The drawer is the human view of what get_work_contract returns to the agent.',
91
+ },
92
+ {
93
+ icon: 'kanban',
94
+ title: 'Kanban board',
95
+ body: 'Columns reflect contract state: backlog, ready, claimed, doing, verify. Moving a card follows evidence and gates — not a manual status edit.',
96
+ },
97
+ {
98
+ icon: 'shield-check',
99
+ title: 'Verification',
100
+ body: 'Tier A/B/C matrix lists deterministic commands and optional gates. assert_task_ready_for_done returns violations[] when proof is missing.',
101
+ },
102
+ {
103
+ icon: 'brain',
104
+ title: 'Project memory',
105
+ body: 'Done tasks with valid evidence become memory records. The next agent session reads git, not a recap of the previous conversation.',
106
+ },
107
+ ],
108
+ },
109
+ {
110
+ type: 'screenshotGallery',
111
+ title: 'What Work Graph looks like',
112
+ lead: 'Walk through the surfaces in order — analytics, contract, board, verification and memory — in one local UI tied to intent/.',
113
+ shotKeys: ['analytics', 'tasks', 'board', 'verification', 'memory', 'architecture'],
114
+ },
115
+ {
116
+ type: 'graphTrinity',
117
+ title: 'Three graphs behind the UI',
118
+ body: 'Screens map to Intent, Execution and Memory graphs. Together they form a loop humans can audit and agents can execute via MCP.',
119
+ },
120
+ {
121
+ type: 'iconLabelGrid',
122
+ title: 'What lands in your repo after init',
123
+ body: 'npx @work-graph/cli init . wires the filesystem contract — no WG cloud account.',
124
+ items: ['intent/ graph', 'work.bvc backlog', 'MCP for Cursor', 'UI on :4177', 'workgraph:doctor', 'llms.txt'],
125
+ },
126
+ ],
127
+ evidence: [
128
+ {
129
+ type: 'lead',
130
+ text:
131
+ 'The evidence ledger answers one question: why is this work.id allowed to be done? It stores command output, file traces, verification results and gate verdicts next to the BVC contract — reviewable in git like any other change.',
132
+ },
133
+ {
134
+ type: 'siteSections',
135
+ title: 'What counts as evidence',
136
+ intro: 'Evidence is machine-readable. Chat messages and hand-waved “done” are not sufficient for Tier A work.',
137
+ sections: [
138
+ {
139
+ icon: 'terminal',
140
+ title: 'Command output',
141
+ body: 'npm test, bvc lint, custom scripts — exit code, stdout/stderr and timestamps attached to work.id. CI results can be referenced the same way.',
142
+ },
143
+ {
144
+ icon: 'link',
145
+ title: 'Trace links',
146
+ body: 'work.id ↔ files ↔ tests ↔ AN decisions. Broken links surface in diagnostics before merge, not after production.',
147
+ },
148
+ {
149
+ icon: 'shield-check',
150
+ title: 'Tier checks',
151
+ body: 'Tier A demands deterministic proof. Tier B/C add optional or environment gates. The matrix is visible in the verification UI and in contract labels.',
152
+ },
153
+ ],
154
+ },
155
+ {
156
+ type: 'pipeline',
157
+ title: 'From claim to done',
158
+ intro: 'MCP tools enforce the sequence; skipping a step leaves the task open with a PolicyViolation or missing-evidence error.',
159
+ steps: [
160
+ { iconName: 'hand-grabbing', label: 'claim_work_item', detail: 'Agent takes work.id and reads contract' },
161
+ { iconName: 'file-code', label: 'edit target_files', detail: 'Changes stay inside allowlist' },
162
+ { iconName: 'terminal', label: 'run commands', detail: 'Only approved scripts' },
163
+ { iconName: 'upload-simple', label: 'submit evidence', detail: 'Structured JSON + logs' },
164
+ { iconName: 'seal-check', label: 'assert_task_ready_for_done', detail: 'Gate verdict → done' },
165
+ ],
166
+ },
167
+ {
168
+ type: 'codeShowcase',
169
+ title: 'Checks live in the contract',
170
+ body: 'BVC Checks and MCP gates share the same definition of ready — agents and humans see the same missing[] list.',
171
+ },
172
+ {
173
+ type: 'screenshotGallery',
174
+ title: 'Verification and memory in the UI',
175
+ lead: 'The matrix shows what is still missing. Memory lists what was proven and closed — the audit trail for the next session.',
176
+ shotKeys: ['verification', 'memory', 'tasks'],
177
+ },
178
+ {
179
+ type: 'siteSections',
180
+ sections: [
181
+ {
182
+ title: 'Ready-for-done',
183
+ body: 'Closing requires evidence, checks and a traceable work contract. The gate returns exactly which field or command failed — not a generic error.',
184
+ icon: 'check-circle',
185
+ badges: [{ label: 'GATE', tone: 'accent' }],
186
+ },
187
+ {
188
+ title: 'Evidence records',
189
+ body: 'Lines and structured entries bind commands, diffs, traces and verification outcomes to one work.id. Export and review follow git history.',
190
+ icon: 'list-checks',
191
+ badges: [{ label: 'TRACE', tone: 'warning' }],
192
+ },
193
+ {
194
+ title: 'Local by default',
195
+ body: 'The ledger is files in your repository. No separate SaaS database — PR review and blame apply to evidence the same as to code.',
196
+ icon: 'hard-drives',
197
+ badges: [{ label: 'GIT', tone: 'ok' }],
198
+ },
199
+ ],
200
+ },
201
+ ],
202
+ compare: [
203
+ {
204
+ type: 'lead',
205
+ text:
206
+ 'Teams often pair an IDE agent with Jira or Linear in the cloud. Work Graph is the tracker that lives in the repo: same board and statuses, plus intent in .bvc and proof per work.id. It complements the IDE; external PM sync is not the core product.',
207
+ },
208
+ { type: 'comparisonStrip' },
209
+ {
210
+ type: 'siteSections',
211
+ sections: [
212
+ {
213
+ title: 'Competitor matrix',
214
+ body: 'Work Graph sits at the intent-graph layer: goals, decisions, work items, evidence and verified memory in one local graph.',
215
+ competitors: true,
216
+ },
217
+ ],
218
+ },
219
+ {
220
+ type: 'boundaries',
221
+ title: 'When Work Graph fits',
222
+ items: [
223
+ {
224
+ iconName: 'check-circle',
225
+ heading: 'You want audit, not vibes',
226
+ body: 'Tasks must cite files, commands and gates before done. Regulated, platform or long-lived codebases benefit most.',
227
+ },
228
+ {
229
+ iconName: 'git-branch',
230
+ heading: 'You already live in git',
231
+ body: 'Backlog and evidence as files match PR review, forks and offline work. No vendor lock-in for the intent graph.',
232
+ },
233
+ {
234
+ iconName: 'robot',
235
+ heading: 'You run IDE agents',
236
+ body: 'Cursor or Claude Code execute; WG supplies get_work_contract, evidence tools and PolicyViolation when proof is missing.',
237
+ },
238
+ ],
239
+ },
240
+ {
241
+ type: 'boundaries',
242
+ title: 'What Work Graph is not',
243
+ variant: 'muted',
244
+ items: [
245
+ {
246
+ iconName: 'x-circle',
247
+ heading: 'Not enterprise cloud PM',
248
+ body: 'WG is a local tracker in git, not a multi-tenant SaaS like Jira Cloud. Optional sync with external PM tools is outside the core scope.',
249
+ },
250
+ {
251
+ iconName: 'x-circle',
252
+ heading: 'Not an IDE',
253
+ body: 'WG does not edit files or run models. It governs contracts around the edits your agent already makes.',
254
+ },
255
+ {
256
+ iconName: 'x-circle',
257
+ heading: 'Not fuzzy memory',
258
+ body: 'Mem0-style recall is complementary. WG memory is closed work with evidence — accountable, not conversational.',
259
+ },
260
+ ],
261
+ },
262
+ ],
263
+ };
264
+
265
+ /** @type {Record<string, Array<Record<string, unknown>>>} */
266
+ const PAGE_BLOCKS_RU = {
267
+ product: [
268
+ {
269
+ type: 'lead',
270
+ text:
271
+ 'Work Graph — локальный таск-трекер в git: доска, бэклог и статусы. Отличие в единице работы: BVC-контракт с гейтами evidence, а не тикет из чата. Решения → AN, задачи → .bvc, движение на доске, готовность через проверки, результат → память проекта.',
272
+ },
273
+ {
274
+ type: 'pipeline',
275
+ title: 'Пять шагов в одном репозитории',
276
+ intro: 'У каждого шага — экран в UI и файловый контракт в intent/.',
277
+ steps: [
278
+ { iconName: 'lightbulb', label: 'Решение (AN)', detail: 'Зафиксировать «зачем» до бэклога' },
279
+ { iconName: 'clipboard-text', label: 'Контракт (.bvc)', detail: 'Базис, Вектор, Цель и проверки' },
280
+ { iconName: 'robot', label: 'Захват агентом', detail: 'work.id, targetFiles, allowlist' },
281
+ { iconName: 'list-checks', label: 'Доказательства', detail: 'Команды, трассы, записи' },
282
+ { iconName: 'brain', label: 'Проверенная память', detail: 'Закрытая работа со ссылками в git' },
283
+ ],
284
+ },
285
+ {
286
+ type: 'siteSections',
287
+ title: 'Экраны, с которыми вы работаете',
288
+ intro: 'После init откройте npm run workgraph:ui на :4177 — тот же бэклог, что видит MCP-агент.',
289
+ sections: [
290
+ {
291
+ icon: 'chart-bar',
292
+ title: 'Аналитика',
293
+ body: 'AN-записи хранят варианты, границы и lineage до появления задач. Видно, из какого эпика или решения вырос work item — не потерянный тред в чате.',
294
+ },
295
+ {
296
+ icon: 'clipboard-text',
297
+ title: 'Задачи BVC',
298
+ body: 'Каждая задача — атом .bvc с Базисом, Вектором, Целью, checks и метками. Drawer — человеческий вид того, что get_work_contract отдаёт агенту.',
299
+ },
300
+ {
301
+ icon: 'kanban',
302
+ title: 'Доска задач',
303
+ body: 'Колонки отражают состояние контракта: backlog, ready, claimed, doing, verify. Перемещение карточки следует evidence и гейтам, а не ручной метке.',
304
+ },
305
+ {
306
+ icon: 'shield-check',
307
+ title: 'Проверки',
308
+ body: 'Матрица tier A/B/C: детерминированные команды и опциональные гейты. assert_task_ready_for_done возвращает violations[], если доказательств не хватает.',
309
+ },
310
+ {
311
+ icon: 'brain',
312
+ title: 'Память проекта',
313
+ body: 'Закрытые задачи с валидным evidence становятся записями памяти. Следующая сессия агента читает git, а не пересказ прошлого чата.',
314
+ },
315
+ ],
316
+ },
317
+ {
318
+ type: 'screenshotGallery',
319
+ title: 'Как выглядит Work Graph',
320
+ lead: 'Пройдите поверхности по порядку — аналитика, контракт, доска, проверки и память — в одном локальном UI, связанном с intent/.',
321
+ shotKeys: ['analytics', 'tasks', 'board', 'verification', 'memory', 'architecture'],
322
+ },
323
+ {
324
+ type: 'graphTrinity',
325
+ title: 'Три графа за интерфейсом',
326
+ body: 'Экраны соответствуют графам намерений, исполнения и памяти. Вместе — цикл, который человек аудирует, а агент исполняет через MCP.',
327
+ },
328
+ {
329
+ type: 'iconLabelGrid',
330
+ title: 'Что появляется в репозитории после init',
331
+ body: 'npx @work-graph/cli init . задаёт файловый контракт — без облачного аккаунта WG.',
332
+ items: ['граф intent/', 'бэклог .bvc', 'MCP для Cursor', 'UI на :4177', 'workgraph:doctor', 'llms.txt'],
333
+ },
334
+ ],
335
+ evidence: [
336
+ {
337
+ type: 'lead',
338
+ text:
339
+ 'Журнал доказательств отвечает на один вопрос: почему этот work.id можно закрыть? В нём — вывод команд, трассы файлов, результаты проверок и вердикты гейтов рядом с BVC-контрактом. Как и код, всё проходит через git.',
340
+ },
341
+ {
342
+ type: 'siteSections',
343
+ title: 'Что считается доказательством',
344
+ intro: 'Evidence машиночитаем. Сообщения в чате и слова «готово» для Tier A не достаточны.',
345
+ sections: [
346
+ {
347
+ icon: 'terminal',
348
+ title: 'Вывод команд',
349
+ body: 'npm test, bvc lint, свои скрипты — exit code, stdout/stderr и время привязаны к work.id. Результаты CI можно ссылать так же.',
350
+ },
351
+ {
352
+ icon: 'link',
353
+ title: 'Трассы связей',
354
+ body: 'work.id ↔ файлы ↔ тесты ↔ решения AN. Битые связи видны в диагностике до merge, а не после продакшена.',
355
+ },
356
+ {
357
+ icon: 'shield-check',
358
+ title: 'Tier-проверки',
359
+ body: 'Tier A требует детерминированного proof. B/C добавляют опциональные и environment-гейты. Матрица видна в UI и в метках контракта.',
360
+ },
361
+ ],
362
+ },
363
+ {
364
+ type: 'pipeline',
365
+ title: 'От захвата до done',
366
+ intro: 'MCP-инструменты задают порядок; пропуск шага оставляет задачу открытой — PolicyViolation или missing_evidence.',
367
+ steps: [
368
+ { iconName: 'hand-grabbing', label: 'claim_work_item', detail: 'Агент берёт work.id и читает контракт' },
369
+ { iconName: 'file-code', label: 'правки target_files', detail: 'Только внутри allowlist' },
370
+ { iconName: 'terminal', label: 'запуск команд', detail: 'Только разрешённые скрипты' },
371
+ { iconName: 'upload-simple', label: 'submit evidence', detail: 'JSON + логи' },
372
+ { iconName: 'seal-check', label: 'assert_task_ready_for_done', detail: 'Вердикт гейта → done' },
373
+ ],
374
+ },
375
+ {
376
+ type: 'codeShowcase',
377
+ title: 'Проверки живут в контракте',
378
+ body: 'Секция Checks в BVC и MCP-гейты используют одно определение готовности — у агента и человека один и тот же список missing[].',
379
+ },
380
+ {
381
+ type: 'screenshotGallery',
382
+ title: 'Проверки и память в UI',
383
+ lead: 'В матрице видно, чего не хватает. В памяти — что уже доказано и закрыто: аудит-след для следующей сессии.',
384
+ shotKeys: ['verification', 'memory', 'tasks'],
385
+ },
386
+ {
387
+ type: 'siteSections',
388
+ sections: [
389
+ {
390
+ title: 'Готовность к завершению',
391
+ body: 'Закрытие требует evidence, checks и трассируемого контракта. Гейт возвращает, какое поле или команда не прошли — не общую ошибку.',
392
+ icon: 'check-circle',
393
+ badges: [{ label: 'ГЕЙТ', tone: 'accent' }],
394
+ },
395
+ {
396
+ title: 'Записи доказательств',
397
+ body: 'Строки и структурированные записи связывают команды, diff, трассы и итог проверки с одним work.id. Review и blame — через git.',
398
+ icon: 'list-checks',
399
+ badges: [{ label: 'ТРАССА', tone: 'warning' }],
400
+ },
401
+ {
402
+ title: 'Локально по умолчанию',
403
+ body: 'Журнал — файлы в репозитории. Отдельной SaaS-базы нет: PR-review применим к evidence так же, как к коду.',
404
+ icon: 'hard-drives',
405
+ badges: [{ label: 'GIT', tone: 'ok' }],
406
+ },
407
+ ],
408
+ },
409
+ ],
410
+ compare: [
411
+ {
412
+ type: 'lead',
413
+ text:
414
+ 'Часто рядом с IDE-агентом стоят Jira или Linear в облаке. Work Graph — трекер в репозитории: та же доска и статусы, плюс намерение в .bvc и proof на work.id. IDE он дополняет; синхронизация с внешним PM не ядро продукта.',
415
+ },
416
+ { type: 'comparisonStrip' },
417
+ {
418
+ type: 'siteSections',
419
+ sections: [
420
+ {
421
+ title: 'Матрица конкурентов',
422
+ body: 'Work Graph — слой графа намерений: цели, решения, задачи, evidence и проверенная память в одном локальном графе.',
423
+ competitors: true,
424
+ },
425
+ ],
426
+ },
427
+ {
428
+ type: 'boundaries',
429
+ title: 'Когда Work Graph уместен',
430
+ items: [
431
+ {
432
+ iconName: 'check-circle',
433
+ heading: 'Нужен аудит, а не «вайб»',
434
+ body: 'Задачи должны ссылаться на файлы, команды и гейты до done. Особенно платформы, regulated и долгоживущие кодовые базы.',
435
+ },
436
+ {
437
+ iconName: 'git-branch',
438
+ heading: 'Вы уже в git',
439
+ body: 'Бэклог и evidence как файлы — PR, форки и офлайн. Без vendor lock-in для графа намерений.',
440
+ },
441
+ {
442
+ iconName: 'robot',
443
+ heading: 'Вы запускаете IDE-агентов',
444
+ body: 'Cursor или Claude Code исполняют; WG даёт get_work_contract, evidence-tools и PolicyViolation без proof.',
445
+ },
446
+ ],
447
+ },
448
+ {
449
+ type: 'boundaries',
450
+ title: 'Чем Work Graph не является',
451
+ variant: 'muted',
452
+ items: [
453
+ {
454
+ iconName: 'x-circle',
455
+ heading: 'Не корпоративный облачный PM',
456
+ body: 'WG — локальный трекер в git, а не multi-tenant SaaS вроде Jira Cloud. Синхронизация с внешним PM — вне ядра.',
457
+ },
458
+ {
459
+ iconName: 'x-circle',
460
+ heading: 'Не IDE',
461
+ body: 'WG не правит файлы и не запускает модели. Он управляет контрактами вокруг правок агента.',
462
+ },
463
+ {
464
+ iconName: 'x-circle',
465
+ heading: 'Не «память из чата»',
466
+ body: 'Mem0 и аналоги — комплемент. Память WG — закрытая работа с evidence, а не пересказ диалога.',
467
+ },
468
+ ],
469
+ },
470
+ ],
471
+ };
@@ -0,0 +1,203 @@
1
+ export const PUBLIC_SITE_LOCALE_STORAGE_KEY = 'workGraphPublicSiteLocale';
2
+ export const PUBLIC_SITE_THEME_STORAGE_KEY = 'workGraphPublicSiteTheme';
3
+
4
+ export function localeFromPathname(pathname) {
5
+ return pathname === '/en' || pathname.startsWith('/en/') ? 'en' : 'ru';
6
+ }
7
+
8
+ export function stripLocalePathPrefix(pathname) {
9
+ if (pathname === '/en') return '/';
10
+ if (pathname.startsWith('/en/')) return pathname.slice(3) || '/';
11
+ return pathname;
12
+ }
13
+
14
+ export function pathForLocale(pathname, locale) {
15
+ const route = stripLocalePathPrefix(pathname);
16
+ if (locale === 'en') return route === '/' ? '/en' : `/en${route}`;
17
+ return route;
18
+ }
19
+
20
+ export function withLocalePath(href, locale) {
21
+ if (
22
+ href.startsWith('#')
23
+ || href.startsWith('http')
24
+ || href.endsWith('.txt')
25
+ || href.includes('.well-known')
26
+ || href.startsWith('/api/')
27
+ ) {
28
+ return href;
29
+ }
30
+ const hashIndex = href.indexOf('#');
31
+ const hash = hashIndex >= 0 ? href.slice(hashIndex) : '';
32
+ const path = hashIndex >= 0 ? href.slice(0, hashIndex) : href;
33
+ if (path.endsWith('.md') || path.endsWith('.json') || path.endsWith('.bvc.example')) {
34
+ return `${path}${hash}`;
35
+ }
36
+ return `${pathForLocale(path, locale)}${hash}`;
37
+ }
38
+
39
+ export function renderPublicSiteBootstrapScript(fallbackLocale, fallbackTheme) {
40
+ return `(function () {
41
+ var LOCALE_KEY = '${PUBLIC_SITE_LOCALE_STORAGE_KEY}';
42
+ var THEME_KEY = '${PUBLIC_SITE_THEME_STORAGE_KEY}';
43
+ var allowedLang = ['en', 'ru'];
44
+ var allowedTheme = ['light', 'dark'];
45
+ function localeFromPath(path) {
46
+ return (path === '/en' || path.startsWith('/en/')) ? 'en' : 'ru';
47
+ }
48
+ function pathForLocale(path, lang) {
49
+ var route = path;
50
+ if (route === '/en' || route.startsWith('/en/')) route = route === '/en' ? '/' : route.slice(3) || '/';
51
+ if (lang === 'en') return route === '/' ? '/en' : '/en' + route;
52
+ return route;
53
+ }
54
+ var params = new URLSearchParams(window.location.search);
55
+ var path = window.location.pathname;
56
+ var pageLocale = localeFromPath(path);
57
+ var lang = params.get('lang') || localStorage.getItem(LOCALE_KEY) || '${fallbackLocale}';
58
+ var theme = params.get('theme') || localStorage.getItem(THEME_KEY) || '${fallbackTheme}';
59
+ if (!allowedLang.includes(lang)) lang = pageLocale;
60
+ if (!allowedTheme.includes(theme)) theme = 'light';
61
+ localStorage.setItem(LOCALE_KEY, lang);
62
+ localStorage.setItem(THEME_KEY, theme);
63
+ if (params.has('lang') || params.has('theme')) {
64
+ params.delete('lang');
65
+ params.delete('theme');
66
+ var clean = path + (params.toString() ? '?' + params.toString() : '') + window.location.hash;
67
+ window.history.replaceState(null, '', clean);
68
+ }
69
+ if (lang !== pageLocale) {
70
+ window.location.replace(pathForLocale(path, lang) + window.location.search + window.location.hash);
71
+ return;
72
+ }
73
+ document.documentElement.lang = lang;
74
+ document.documentElement.dataset.theme = theme;
75
+ })();`;
76
+ }
77
+
78
+ export function renderPublicSiteControlsScript() {
79
+ return `(function () {
80
+ var LOCALE_KEY = '${PUBLIC_SITE_LOCALE_STORAGE_KEY}';
81
+ var THEME_KEY = '${PUBLIC_SITE_THEME_STORAGE_KEY}';
82
+ function localeFromPath(path) {
83
+ return (path === '/en' || path.startsWith('/en/')) ? 'en' : 'ru';
84
+ }
85
+ function pathForLocale(path, lang) {
86
+ var route = path;
87
+ if (route === '/en' || route.startsWith('/en/')) route = route === '/en' ? '/' : route.slice(3) || '/';
88
+ if (lang === 'en') return route === '/' ? '/en' : '/en' + route;
89
+ return route;
90
+ }
91
+ function applyTheme(theme) {
92
+ document.documentElement.dataset.theme = theme;
93
+ localStorage.setItem(THEME_KEY, theme);
94
+ document.querySelectorAll('[data-hero-screenshot]').forEach(function (img) {
95
+ var light = img.getAttribute('data-light-src');
96
+ var dark = img.getAttribute('data-dark-src');
97
+ if (light && dark) img.src = theme === 'dark' ? dark : light;
98
+ });
99
+ var toggle = document.querySelector('[data-theme-toggle]');
100
+ if (!toggle) return;
101
+ var isDark = theme === 'dark';
102
+ toggle.setAttribute('aria-label', isDark ? toggle.getAttribute('data-label-light') : toggle.getAttribute('data-label-dark'));
103
+ }
104
+ document.querySelector('[data-theme-toggle]')?.addEventListener('click', function () {
105
+ var next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
106
+ applyTheme(next);
107
+ });
108
+ document.querySelector('[data-locale-toggle]')?.addEventListener('click', function () {
109
+ var next = localeFromPath(window.location.pathname) === 'ru' ? 'en' : 'ru';
110
+ localStorage.setItem(LOCALE_KEY, next);
111
+ window.location.assign(pathForLocale(window.location.pathname, next) + window.location.search + window.location.hash);
112
+ });
113
+ applyTheme(document.documentElement.dataset.theme || 'light');
114
+ function copyText(text) {
115
+ if (navigator.clipboard && navigator.clipboard.writeText) {
116
+ return navigator.clipboard.writeText(text);
117
+ }
118
+ return new Promise(function (resolve, reject) {
119
+ var area = document.createElement('textarea');
120
+ area.value = text;
121
+ area.setAttribute('readonly', '');
122
+ area.style.position = 'fixed';
123
+ area.style.left = '-9999px';
124
+ document.body.appendChild(area);
125
+ area.select();
126
+ try {
127
+ document.execCommand('copy');
128
+ resolve();
129
+ } catch (error) {
130
+ reject(error);
131
+ } finally {
132
+ document.body.removeChild(area);
133
+ }
134
+ });
135
+ }
136
+ var header = document.querySelector('.site-header');
137
+ var navToggle = document.querySelector('[data-nav-toggle]');
138
+ if (header && navToggle) {
139
+ function setNavOpen(open) {
140
+ header.classList.toggle('is-nav-open', open);
141
+ document.documentElement.classList.toggle('is-nav-scroll-locked', open);
142
+ navToggle.classList.toggle('is-open', open);
143
+ navToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
144
+ navToggle.setAttribute(
145
+ 'aria-label',
146
+ open ? navToggle.getAttribute('data-label-close') : navToggle.getAttribute('data-label-open'),
147
+ );
148
+ }
149
+ navToggle.addEventListener('click', function () {
150
+ setNavOpen(!header.classList.contains('is-nav-open'));
151
+ });
152
+ document.querySelectorAll('#site-nav a').forEach(function (link) {
153
+ link.addEventListener('click', function () {
154
+ setNavOpen(false);
155
+ });
156
+ });
157
+ document.addEventListener('keydown', function (event) {
158
+ if (event.key === 'Escape') setNavOpen(false);
159
+ });
160
+ window.matchMedia('(max-width: 1024px)').addEventListener('change', function (event) {
161
+ if (!event.matches) setNavOpen(false);
162
+ });
163
+ }
164
+ document.querySelectorAll('[data-screenshot-switcher]').forEach(function (root) {
165
+ var tabs = root.querySelectorAll('[data-screenshot-tab]');
166
+ var panels = root.querySelectorAll('[data-screenshot-panel]');
167
+ function activate(id) {
168
+ tabs.forEach(function (tab) {
169
+ var active = tab.getAttribute('data-screenshot-tab') === id;
170
+ tab.classList.toggle('is-active', active);
171
+ tab.setAttribute('aria-selected', active ? 'true' : 'false');
172
+ });
173
+ panels.forEach(function (panel) {
174
+ var active = panel.getAttribute('data-screenshot-panel') === id;
175
+ panel.classList.toggle('is-active', active);
176
+ panel.hidden = !active;
177
+ });
178
+ }
179
+ tabs.forEach(function (tab) {
180
+ tab.addEventListener('click', function () {
181
+ activate(tab.getAttribute('data-screenshot-tab'));
182
+ });
183
+ });
184
+ });
185
+ document.querySelectorAll('[data-copy-text]').forEach(function (button) {
186
+ button.addEventListener('click', function () {
187
+ var text = button.getAttribute('data-copy-text') || '';
188
+ copyText(text).then(function () {
189
+ var copied = button.getAttribute('data-copied-label') || 'Copied';
190
+ var original = button.getAttribute('data-copy-label') || button.getAttribute('aria-label') || 'Copy';
191
+ button.classList.add('is-copied');
192
+ button.setAttribute('aria-label', copied);
193
+ button.setAttribute('title', copied);
194
+ window.setTimeout(function () {
195
+ button.setAttribute('aria-label', original);
196
+ button.setAttribute('title', original);
197
+ button.classList.remove('is-copied');
198
+ }, 1600);
199
+ });
200
+ });
201
+ });
202
+ })();`;
203
+ }