@work-graph/cli 0.2.8 → 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.
- package/README.md +3 -0
- package/package.json +2 -2
- package/vendor/packages/workgraph-mcp/README.md +8 -0
- package/vendor/packages/workgraph-mcp/package.json +3 -2
- package/vendor/public/assets/favicon.svg +0 -1
- package/vendor/src/codeSyntaxHighlight.mjs +63 -0
- package/vendor/src/publicSiteContent.mjs +44 -43
- package/vendor/src/publicSitePageContent.mjs +471 -0
- package/vendor/src/publicSitePreferences.mjs +203 -0
- package/vendor/src/publicSiteServer.mjs +969 -456
- package/vendor/src/publicSiteStandaloneServer.mjs +10 -0
- package/vendor/src/ui/iconAssets.mjs +15 -9
|
@@ -8,16 +8,26 @@ import {
|
|
|
8
8
|
buildLlmsTxt,
|
|
9
9
|
buildMcpDiscovery,
|
|
10
10
|
getLocalizedFaq,
|
|
11
|
+
getPublicSiteCompetitors,
|
|
11
12
|
getPublicSiteCopy,
|
|
12
13
|
getPublicSitePage,
|
|
13
14
|
renderBvcExample,
|
|
14
15
|
renderPublicDocMarkdown,
|
|
15
16
|
} from './publicSiteContent.mjs';
|
|
17
|
+
import {
|
|
18
|
+
localeFromPathname,
|
|
19
|
+
renderPublicSiteBootstrapScript,
|
|
20
|
+
renderPublicSiteControlsScript,
|
|
21
|
+
stripLocalePathPrefix,
|
|
22
|
+
withLocalePath,
|
|
23
|
+
} from './publicSitePreferences.mjs';
|
|
16
24
|
import { renderUiBadge, UI_BADGE_CSS } from './ui/atoms/badge.mjs';
|
|
17
25
|
import { renderUiButton, UI_BUTTON_CSS } from './ui/atoms/button.mjs';
|
|
26
|
+
import { highlightBvcBlock, highlightMcpFlow } from './codeSyntaxHighlight.mjs';
|
|
18
27
|
import { renderInlineIcon, renderThemeIcon } from './ui/iconAssets.mjs';
|
|
19
28
|
|
|
20
29
|
const PUBLIC_SITE_SCHEMA = 'workgraph.public-site.v1';
|
|
30
|
+
const PUBLIC_SITE_GITHUB_URL = 'https://github.com/bvc-lang/work-graph';
|
|
21
31
|
|
|
22
32
|
function escapeHtml(value) {
|
|
23
33
|
return String(value ?? '')
|
|
@@ -39,7 +49,23 @@ function normalizeTheme(value) {
|
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
function publicSiteJsonLd(page) {
|
|
42
|
-
if (page.kind === '
|
|
52
|
+
if (page.kind === 'home') {
|
|
53
|
+
const faqLd = buildFaqJsonLd(page.locale);
|
|
54
|
+
return {
|
|
55
|
+
'@context': 'https://schema.org',
|
|
56
|
+
'@graph': [
|
|
57
|
+
{
|
|
58
|
+
'@type': 'SoftwareApplication',
|
|
59
|
+
name: page.title,
|
|
60
|
+
description: page.description,
|
|
61
|
+
applicationCategory: 'DeveloperApplication',
|
|
62
|
+
softwareHelp: '/docs',
|
|
63
|
+
codeRepository: 'local-git-workspace',
|
|
64
|
+
},
|
|
65
|
+
{ '@type': faqLd['@type'], mainEntity: faqLd.mainEntity },
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
43
69
|
return {
|
|
44
70
|
'@context': 'https://schema.org',
|
|
45
71
|
'@type': page.kind === 'doc' ? 'TechArticle' : 'SoftwareApplication',
|
|
@@ -51,73 +77,195 @@ function publicSiteJsonLd(page) {
|
|
|
51
77
|
};
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
function withLangAndTheme(href, locale, theme) {
|
|
55
|
-
if (href.startsWith('#') || href.endsWith('.txt') || href.includes('.well-known')) return href;
|
|
56
|
-
const separator = href.includes('?') ? '&' : '?';
|
|
57
|
-
return `${href}${separator}lang=${locale}&theme=${theme}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
80
|
function icon(name, size = 18) {
|
|
61
81
|
return renderInlineIcon(`${name}-bold.svg`, { className: 'site-icon', size });
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
|
|
84
|
+
const STEP_NUMBER_ICON_NAMES = [
|
|
85
|
+
'number-one',
|
|
86
|
+
'number-two',
|
|
87
|
+
'number-three',
|
|
88
|
+
'number-four',
|
|
89
|
+
'number-five',
|
|
90
|
+
'number-six',
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
function renderStepNumberIcon(index) {
|
|
94
|
+
const name = STEP_NUMBER_ICON_NAMES[index] ?? 'number-one';
|
|
95
|
+
return renderInlineIcon(`${name}-fill.svg`, { className: 'step-number-icon-svg', size: 32 }, 'fill');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function featureIcon(name) {
|
|
99
|
+
return renderInlineIcon(`${name}-fill.svg`, { className: 'feature-column-icon-svg', size: 40 }, 'fill');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function comparisonStripIcon(name) {
|
|
103
|
+
return renderInlineIcon(`${name}-fill.svg`, { className: 'comparison-strip-icon-svg', size: 40 }, 'fill');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function workflowPipelineIcon(name) {
|
|
107
|
+
return renderInlineIcon(`${name}-fill.svg`, { className: 'workflow-pipeline-icon-svg', size: 32 }, 'fill');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderFeatureColumns({ title, items }) {
|
|
111
|
+
return `<section class="feature-columns-section">
|
|
112
|
+
<h2 class="feature-columns-heading">${escapeHtml(title)}</h2>
|
|
113
|
+
<div class="feature-columns">${items.map(({ iconName, heading, body }) => `<article class="feature-column">
|
|
114
|
+
<span class="feature-column-icon" aria-hidden="true">${featureIcon(iconName)}</span>
|
|
115
|
+
<h3>${escapeHtml(heading)}</h3>
|
|
116
|
+
<p>${escapeHtml(body)}</p>
|
|
117
|
+
</article>`).join('')}</div>
|
|
118
|
+
</section>`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderIconLabelGridIcon() {
|
|
122
|
+
return renderInlineIcon('check-bold.svg', { className: 'icon-label-grid-icon-svg', size: 22 }, 'bold');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Full-viewport background band; content stays in __inner max-width column. */
|
|
126
|
+
function wrapSiteSectionBand(content, { tone = 'muted', innerClass = '' } = {}) {
|
|
127
|
+
const innerAttr = innerClass ? ` ${innerClass}` : '';
|
|
128
|
+
return `<div class="site-section-band site-section-band--${tone}">
|
|
129
|
+
<div class="site-section-band__inner${innerAttr}">${content}</div>
|
|
130
|
+
</div>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderIconLabelGrid({ title, body, items }) {
|
|
134
|
+
return `<section class="icon-label-grid-section">
|
|
135
|
+
<div class="icon-label-grid-heading">
|
|
136
|
+
<h2>${escapeHtml(title)}</h2>
|
|
137
|
+
${body ? `<p>${escapeHtml(body)}</p>` : ''}
|
|
138
|
+
</div>
|
|
139
|
+
<div class="icon-label-grid">${items.map((label) => `<div class="icon-label-grid-item">
|
|
140
|
+
<span class="icon-label-grid-icon" aria-hidden="true"><span class="icon-label-grid-icon-box">${renderIconLabelGridIcon()}</span></span>
|
|
141
|
+
<span class="icon-label-grid-label">${escapeHtml(label)}</span>
|
|
142
|
+
</div>`).join('')}</div>
|
|
143
|
+
</section>`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderThemeToggleIcons() {
|
|
147
|
+
const moon = renderThemeIcon('moon', { className: 'site-icon theme-icon theme-icon-moon', size: 18 });
|
|
148
|
+
const sun = renderThemeIcon('sun', { className: 'site-icon theme-icon theme-icon-sun', size: 18 });
|
|
149
|
+
return `<span class="theme-toggle-icons" aria-hidden="true">${moon}${sun}</span>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function renderNavToggle(locale) {
|
|
65
153
|
const copy = getPublicSiteCopy(locale);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
154
|
+
return `<button type="button" class="site-nav-toggle site-control-btn" data-nav-toggle aria-controls="site-nav" aria-expanded="false" aria-label="${escapeAttr(copy.nav.menuOpen)}" data-label-open="${escapeAttr(copy.nav.menuOpen)}" data-label-close="${escapeAttr(copy.nav.menuClose)}"><span class="site-nav-toggle-bars" aria-hidden="true"></span></button>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderHeaderGithubButton(locale) {
|
|
158
|
+
const label = locale === 'ru' ? 'GitHub репозиторий Work Graph' : 'Work Graph on GitHub';
|
|
159
|
+
const icon = renderInlineIcon('github-logo-fill.svg', { className: 'site-github-icon-svg', size: 20 }, 'fill');
|
|
160
|
+
return `<a class="site-control-btn site-github-btn" href="${escapeAttr(PUBLIC_SITE_GITHUB_URL)}" target="_blank" rel="noopener noreferrer" aria-label="${escapeAttr(label)}">${icon}</a>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderThemeLocaleControls(locale, theme) {
|
|
164
|
+
const localeLabel = locale === 'ru' ? 'En' : 'Ru';
|
|
165
|
+
const themeAriaDark = locale === 'ru' ? 'Тёмная тема' : 'Dark theme';
|
|
166
|
+
const themeAriaLight = locale === 'ru' ? 'Светлая тема' : 'Light theme';
|
|
167
|
+
const localeAria = locale === 'ru' ? 'English' : 'Русский';
|
|
168
|
+
return `<div class="site-controls" aria-label="${escapeAttr(getPublicSiteCopy(locale).localeLabel)}">
|
|
169
|
+
<button type="button" class="site-control-btn theme-toggle" data-theme-toggle data-label-dark="${escapeAttr(themeAriaDark)}" data-label-light="${escapeAttr(themeAriaLight)}" aria-label="${escapeAttr(theme === 'dark' ? themeAriaLight : themeAriaDark)}">${renderThemeToggleIcons()}</button>
|
|
170
|
+
<button type="button" class="site-control-btn locale-toggle" data-locale-toggle aria-label="${escapeAttr(localeAria)}">${escapeHtml(localeLabel)}</button>
|
|
73
171
|
</div>`;
|
|
74
172
|
}
|
|
75
173
|
|
|
76
|
-
function
|
|
174
|
+
function renderSiteBrand(locale) {
|
|
175
|
+
const home = withLocalePath('/', locale);
|
|
176
|
+
const label = 'Work Graph';
|
|
177
|
+
return `<a class="site-brand" href="${escapeAttr(home)}" aria-label="${escapeAttr(label)}">
|
|
178
|
+
<img class="site-brand-logo" src="/assets/workgraph-logo.svg" width="188" height="24" alt="${escapeAttr(label)}" decoding="async">
|
|
179
|
+
<img class="site-brand-emblem" src="/assets/workgraph-emblem.svg" width="41" height="24" alt="" aria-hidden="true" decoding="async">
|
|
180
|
+
</a>`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isNavLinkActive(href, activeRoute) {
|
|
184
|
+
if (href === '/') return activeRoute === '/';
|
|
185
|
+
if (href === '/docs') return activeRoute === '/docs' || activeRoute.startsWith('/docs/');
|
|
186
|
+
return activeRoute === href;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function renderNav(locale, activeRoute = '/') {
|
|
77
190
|
const copy = getPublicSiteCopy(locale);
|
|
78
191
|
const links = [
|
|
79
|
-
['/', 'Work Graph'],
|
|
80
192
|
['/product', copy.nav.product],
|
|
81
193
|
['/evidence-ledger', copy.nav.evidence],
|
|
82
194
|
['/compare', copy.nav.compare],
|
|
83
|
-
['/faq', 'FAQ'],
|
|
84
195
|
['/docs', copy.nav.docs],
|
|
85
196
|
];
|
|
86
|
-
return `<nav class="site-nav" aria-label="Work Graph public navigation">
|
|
87
|
-
${links.map(([href, label]) =>
|
|
197
|
+
return `<nav class="site-nav" id="site-nav" aria-label="Work Graph public navigation">
|
|
198
|
+
${links.map(([href, label]) => {
|
|
199
|
+
const active = isNavLinkActive(href, activeRoute);
|
|
200
|
+
const current = active ? ' aria-current="page"' : '';
|
|
201
|
+
return `<a href="${withLocalePath(href, locale)}"${current}>${escapeHtml(label)}</a>`;
|
|
202
|
+
}).join('')}
|
|
88
203
|
</nav>`;
|
|
89
204
|
}
|
|
90
205
|
|
|
206
|
+
const SCREENSHOT_KEY_INDEX = {
|
|
207
|
+
analytics: 0,
|
|
208
|
+
tasks: 1,
|
|
209
|
+
board: 2,
|
|
210
|
+
verification: 3,
|
|
211
|
+
memory: 4,
|
|
212
|
+
architecture: 5,
|
|
213
|
+
};
|
|
214
|
+
|
|
91
215
|
const SCREENSHOTS = [
|
|
92
|
-
{
|
|
93
|
-
src: '/assets/img/work-graph-kanban-board-light.png',
|
|
94
|
-
title: { en: 'Kanban board', ru: 'Доска задач' },
|
|
95
|
-
body: { en: 'Local backlog columns with BVC work items and agent ownership.', ru: 'Локальная доска с BVC-задачами и владельцами.' },
|
|
96
|
-
},
|
|
97
216
|
{
|
|
98
217
|
src: '/assets/img/work-graph-analytics-list.png',
|
|
99
|
-
title: { en: 'Analytics
|
|
100
|
-
|
|
218
|
+
title: { en: 'Analytics', ru: 'Аналитика' },
|
|
219
|
+
headline: { en: 'Analytics links decisions to delivery work', ru: 'Аналитика связывает решения с задачами реализации' },
|
|
220
|
+
body: {
|
|
221
|
+
en: 'AN records capture reasoning, options and boundaries before work enters the backlog. Lineage, epic links and implementation ties stay in the intent graph inside git — not in a separate doc or chat summary.',
|
|
222
|
+
ru: 'AN-записи фиксируют аргументацию, варианты и границы до появления work items. Видны lineage, связи с эпиками и реализацией — не отдельный документ, а часть графа намерений в репозитории.',
|
|
223
|
+
},
|
|
101
224
|
},
|
|
102
225
|
{
|
|
103
226
|
src: '/assets/img/work-graph-task-drawer.png',
|
|
104
|
-
title: { en: '
|
|
105
|
-
|
|
227
|
+
title: { en: 'Tasks', ru: 'Задачи' },
|
|
228
|
+
headline: { en: 'A task is a machine-readable BVC contract', ru: 'Задача — машиночитаемый BVC-контракт, а не тикет из чата' },
|
|
229
|
+
body: {
|
|
230
|
+
en: 'The drawer shows Basis, Vector, Goal, analysis, decisions, checks and evidence in one place. Agents read projection via get_work_contract and know which files, commands and gates are required before done.',
|
|
231
|
+
ru: 'В панели задачи: Базис, Вектор, Цель, анализ, решения, проверки и evidence. Агент читает projection через get_work_contract и знает, какие файлы, команды и гейты обязательны до закрытия.',
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
src: '/assets/img/work-graph-kanban-board-light.png',
|
|
236
|
+
title: { en: 'Kanban board', ru: 'Доска задач' },
|
|
237
|
+
headline: { en: 'The board shows how work is moving', ru: 'Доска показывает, как движется работа' },
|
|
238
|
+
body: {
|
|
239
|
+
en: 'Columns from backlog through ready, doing and done with BVC work items and owners. Status follows contract and evidence — not an arbitrary label someone typed in a thread.',
|
|
240
|
+
ru: 'Колонки от backlog до ready, doing и done с BVC-задачами и владельцами. Статус — следствие контракта и evidence, а не произвольная метка в переписке.',
|
|
241
|
+
},
|
|
106
242
|
},
|
|
107
243
|
{
|
|
108
244
|
src: '/assets/img/work-graph-verification-matrix.png',
|
|
109
|
-
title: { en: 'Verification
|
|
110
|
-
|
|
245
|
+
title: { en: 'Verification', ru: 'Проверки' },
|
|
246
|
+
headline: { en: 'Verification decides when a task can close', ru: 'Проверки решают, можно ли закрыть задачу' },
|
|
247
|
+
body: {
|
|
248
|
+
en: 'Tier A/B/C matrix: deterministic commands, optional checks and environment gates. assert_task_ready_for_done returns violations[] — a contract verdict, not agent prose.',
|
|
249
|
+
ru: 'Матрица tier A/B/C: детерминированные команды, опциональные проверки и environment-гейты. assert_task_ready_for_done возвращает violations[] — вердикт контракта, а не слова агента.',
|
|
250
|
+
},
|
|
111
251
|
},
|
|
112
252
|
{
|
|
113
|
-
src: '/assets/img/work-graph-
|
|
114
|
-
title: { en: '
|
|
115
|
-
|
|
253
|
+
src: '/assets/img/work-graph-memory-list.png',
|
|
254
|
+
title: { en: 'Project memory', ru: 'Память проекта' },
|
|
255
|
+
headline: { en: 'Project memory keeps verified outcomes', ru: 'Память проекта хранит проверенные результаты' },
|
|
256
|
+
body: {
|
|
257
|
+
en: 'Closed tasks with valid evidence become memory records linked to work.id and files. The next session pulls context from git, not from a recap of what the model said last time.',
|
|
258
|
+
ru: 'Закрытые задачи с валидным evidence становятся записями памяти со ссылками на work.id и файлы. Следующая сессия опирается на git, а не на пересказ прошлого чата.',
|
|
259
|
+
},
|
|
116
260
|
},
|
|
117
261
|
{
|
|
118
|
-
src: '/assets/img/work-graph-
|
|
119
|
-
title: { en: '
|
|
120
|
-
|
|
262
|
+
src: '/assets/img/work-graph-architecture-drawer.png',
|
|
263
|
+
title: { en: 'Architecture', ru: 'Архитектура' },
|
|
264
|
+
headline: { en: 'Architecture orients you in a large repo', ru: 'Архитектура помогает ориентироваться в большом репозитории' },
|
|
265
|
+
body: {
|
|
266
|
+
en: 'Blocks from architecture/main.bvc and derived projections map domains and containers without breaking away from the intent graph and backlog you execute against.',
|
|
267
|
+
ru: 'Блоки architecture/main.bvc и производные проекции показывают домены и контейнеры без отрыва от intent-графа и бэклога, по которому идёт работа.',
|
|
268
|
+
},
|
|
121
269
|
},
|
|
122
270
|
];
|
|
123
271
|
|
|
@@ -133,7 +281,7 @@ function renderSection(section, copy) {
|
|
|
133
281
|
? `<ul class="doc-list">${section.docs.map((doc) => `<li><a href="/docs/${escapeAttr(doc.slug)}">${escapeHtml(doc.title)}</a><p>${escapeHtml(doc.description)}</p></li>`).join('')}</ul>`
|
|
134
282
|
: '';
|
|
135
283
|
const competitors = section.competitors
|
|
136
|
-
? `<table><thead><tr><th>${escapeHtml(copy.labels.tableCompetitor)}</th><th>${escapeHtml(copy.labels.tableLayer)}</th><th>${escapeHtml(copy.labels.tableEvidence)}</th><th>${escapeHtml(copy.labels.tableStance)}</th></tr></thead><tbody>${section.competitors.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table>`
|
|
284
|
+
? `<div class="table-scroll" tabindex="0"><table><thead><tr><th>${escapeHtml(copy.labels.tableCompetitor)}</th><th>${escapeHtml(copy.labels.tableLayer)}</th><th>${escapeHtml(copy.labels.tableEvidence)}</th><th>${escapeHtml(copy.labels.tableStance)}</th></tr></thead><tbody>${section.competitors.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`
|
|
137
285
|
: '';
|
|
138
286
|
const badges = section.badges
|
|
139
287
|
? `<div class="badge-row">${section.badges.map((badge) => renderUiBadge(badge)).join('')}</div>`
|
|
@@ -147,142 +295,196 @@ function renderSection(section, copy) {
|
|
|
147
295
|
</section>`;
|
|
148
296
|
}
|
|
149
297
|
|
|
150
|
-
function renderHeroVisual(locale) {
|
|
298
|
+
function renderHeroVisual(locale, theme) {
|
|
299
|
+
const lightSrc = '/assets/img/work-graph-kanban-board-light.png';
|
|
300
|
+
const darkSrc = '/assets/img/work-graph-kanban-board-dark.png';
|
|
301
|
+
const src = theme === 'dark' ? darkSrc : lightSrc;
|
|
151
302
|
return `<figure class="template-visual screenshot-hero" aria-label="Work Graph board preview">
|
|
152
|
-
<img src="
|
|
303
|
+
<img src="${escapeAttr(src)}" data-hero-screenshot data-light-src="${escapeAttr(lightSrc)}" data-dark-src="${escapeAttr(darkSrc)}" alt="${escapeAttr(locale === 'ru' ? 'Доска Work Graph' : 'Work Graph kanban board')}" loading="eager" decoding="async">
|
|
153
304
|
<figcaption>${escapeHtml(locale === 'ru' ? 'Локальная доска Work Graph: backlog, ready, in progress and done.' : 'Local Work Graph board: backlog, ready, in progress and done.')}</figcaption>
|
|
154
305
|
</figure>`;
|
|
155
306
|
}
|
|
156
307
|
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
<p>${escapeHtml(locale === 'ru' ? 'Реальные экраны локального UI: доска, аналитика, контракты задач, проверки и архитектура.' : 'Real local UI screens: board, analytics, task contracts, verification and architecture.')}</p>
|
|
163
|
-
</div>
|
|
164
|
-
<div class="screenshot-grid">
|
|
165
|
-
${SCREENSHOTS.map((shot) => `<figure class="screenshot-card">
|
|
166
|
-
<img src="${escapeAttr(shot.src)}" alt="${escapeAttr(screenshotText(shot.title, locale))}" loading="lazy" decoding="async">
|
|
167
|
-
<figcaption>
|
|
168
|
-
<strong>${escapeHtml(screenshotText(shot.title, locale))}</strong>
|
|
169
|
-
<span>${escapeHtml(screenshotText(shot.body, locale))}</span>
|
|
170
|
-
</figcaption>
|
|
171
|
-
</figure>`).join('')}
|
|
172
|
-
</div>
|
|
173
|
-
</section>`;
|
|
308
|
+
function resolveScreenshotList(shotKeys) {
|
|
309
|
+
if (!shotKeys?.length) return SCREENSHOTS;
|
|
310
|
+
return shotKeys
|
|
311
|
+
.map((key) => SCREENSHOTS[SCREENSHOT_KEY_INDEX[key]])
|
|
312
|
+
.filter(Boolean);
|
|
174
313
|
}
|
|
175
314
|
|
|
176
|
-
function
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
315
|
+
function renderScreenshotGallery(locale, options = {}) {
|
|
316
|
+
const shots = resolveScreenshotList(options.shotKeys);
|
|
317
|
+
const tablistLabel = locale === 'ru' ? 'Экраны Work Graph' : 'Work Graph screens';
|
|
318
|
+
const tabs = shots.map((shot, index) => {
|
|
319
|
+
const id = `screenshot-${index}`;
|
|
320
|
+
const label = screenshotText(shot.title, locale);
|
|
321
|
+
const active = index === 0;
|
|
322
|
+
return `<button type="button" class="screenshot-tab${active ? ' is-active' : ''}" role="tab" id="${id}-tab" data-screenshot-tab="${id}" aria-selected="${active ? 'true' : 'false'}" aria-controls="${id}-panel">${escapeHtml(label)}</button>`;
|
|
323
|
+
}).join('');
|
|
324
|
+
const panels = shots.map((shot, index) => {
|
|
325
|
+
const id = `screenshot-${index}`;
|
|
326
|
+
const title = screenshotText(shot.title, locale);
|
|
327
|
+
const headline = screenshotText(shot.headline ?? shot.title, locale);
|
|
328
|
+
const body = screenshotText(shot.body, locale);
|
|
329
|
+
const active = index === 0;
|
|
330
|
+
return `<article class="screenshot-panel${active ? ' is-active' : ''}" role="tabpanel" id="${id}-panel" data-screenshot-panel="${id}" aria-labelledby="${id}-tab"${active ? '' : ' hidden'}>
|
|
331
|
+
<div class="screenshot-panel-copy">
|
|
332
|
+
<h3>${escapeHtml(headline)}</h3>
|
|
333
|
+
<p>${escapeHtml(body)}</p>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="screenshot-panel-visual">
|
|
336
|
+
<div class="screenshot-panel-frame">
|
|
337
|
+
<img src="${escapeAttr(shot.src)}" alt="${escapeAttr(title)}" loading="${active ? 'eager' : 'lazy'}" decoding="async">
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</article>`;
|
|
341
|
+
}).join('');
|
|
342
|
+
const sectionIntro = options.lead ?? (locale === 'ru'
|
|
343
|
+
? 'Локальный UI Work Graph ведёт полный цикл: от аналитического разбора и BVC-задач до доски, проверок, памяти и архитектуры. Переключайте экраны и смотрите, как решения, контракты и доказательства связаны в одном репозитории.'
|
|
344
|
+
: 'The local Work Graph UI runs the full loop: from analytics review and BVC tasks to the board, verification, memory and architecture. Switch screens to see how decisions, contracts and evidence connect in one repository.');
|
|
345
|
+
const galleryTitle = options.title ?? (locale === 'ru' ? 'Как выглядит Work Graph' : 'What Work Graph looks like');
|
|
346
|
+
const gallery = `<section class="screenshot-gallery" aria-label="${escapeAttr(locale === 'ru' ? 'Скриншоты Work Graph' : 'Work Graph screenshots')}">
|
|
347
|
+
<div class="screenshot-gallery-heading">
|
|
348
|
+
<h2>${escapeHtml(galleryTitle)}</h2>
|
|
349
|
+
<p class="screenshot-gallery-lead">${escapeHtml(sectionIntro)}</p>
|
|
188
350
|
</div>
|
|
189
|
-
|
|
351
|
+
<div class="screenshot-switcher" data-screenshot-switcher>
|
|
352
|
+
<div class="screenshot-tablist" role="tablist" aria-label="${escapeAttr(tablistLabel)}">${tabs}</div>
|
|
353
|
+
<div class="screenshot-panels">${panels}</div>
|
|
354
|
+
</div>
|
|
355
|
+
</section>`;
|
|
356
|
+
return wrapSiteSectionBand(gallery, { tone: 'muted' });
|
|
190
357
|
}
|
|
191
358
|
|
|
192
359
|
function renderSteps(copy, locale) {
|
|
193
360
|
const steps = locale === 'ru'
|
|
194
361
|
? [
|
|
195
|
-
['
|
|
196
|
-
['
|
|
197
|
-
['
|
|
198
|
-
['
|
|
199
|
-
['
|
|
362
|
+
['Установите Work Graph в проект', 'Выполните npx @work-graph/cli init . и npm install — появятся intent/, конфиг и MCP для Cursor или другого клиента.'],
|
|
363
|
+
['Исследуйте вопрос', 'Попросите ИИ провести аналитический разбор — он оформит его как AN-запись в проекте.'],
|
|
364
|
+
['Добавьте задачи', 'Попросите ИИ на основе разбора создать задачи или целый эпик с BVC-контрактами.'],
|
|
365
|
+
['Выполнение', 'Агент захватывает work.id, меняет код в рамках контракта и прикладывает evidence; статусы видны на доске.'],
|
|
366
|
+
['Проверки', 'Детерминированные и опциональные гейты решают, можно ли закрыть задачу — не слова агента, а контракт.'],
|
|
367
|
+
['Память', 'После готовности проверенный результат становится памятью проекта со связями к задачам и файлам.'],
|
|
200
368
|
]
|
|
201
369
|
: [
|
|
202
|
-
['
|
|
203
|
-
['
|
|
204
|
-
['
|
|
205
|
-
['
|
|
206
|
-
['
|
|
370
|
+
['Install Work Graph in your repo', 'Run npx @work-graph/cli init . and npm install — you get intent/, config, and MCP for Cursor or another client.'],
|
|
371
|
+
['Explore the question', 'Ask the agent for an analytics review — it records the outcome as an AN entry in the project.'],
|
|
372
|
+
['Add work items', 'Ask the agent to create tasks or a full epic from the review, each with a BVC contract.'],
|
|
373
|
+
['Execution', 'The agent claims a work.id, edits code within the contract, and attaches evidence; the board shows progress.'],
|
|
374
|
+
['Verification', 'Deterministic and optional gates decide when a task can close — contract verdict, not agent prose.'],
|
|
375
|
+
['Memory', 'After readiness, the verified outcome becomes project memory linked to work items and files.'],
|
|
207
376
|
];
|
|
208
377
|
return `<section class="steps-section">
|
|
209
378
|
<h2>${escapeHtml(locale === 'ru' ? 'Как начать с Work Graph' : 'How to get started with Work Graph')}</h2>
|
|
210
379
|
<ol class="jira-steps">${steps.map(([title, body], index) => `<li>
|
|
211
|
-
<span class="step-number">${index
|
|
212
|
-
<
|
|
380
|
+
<span class="step-number" aria-hidden="true">${renderStepNumberIcon(index)}</span>
|
|
381
|
+
<h3><strong>${escapeHtml(title)}</strong> ${escapeHtml(body)}</h3>
|
|
213
382
|
</li>`).join('')}</ol>
|
|
214
383
|
</section>`;
|
|
215
384
|
}
|
|
216
385
|
|
|
217
|
-
function renderRelatedTemplates(locale, theme) {
|
|
218
|
-
const cards = locale === 'ru'
|
|
219
|
-
? [
|
|
220
|
-
['Контракт задачи', 'BVC-атом с Базисом, Вектором, Целью и проверками.', '/docs/bvc-spec'],
|
|
221
|
-
['Матрица проверок', 'Tier A/B/C readiness для работы агентов.', '/docs/verification-matrix'],
|
|
222
|
-
['MCP-инструменты', 'Контракты tools для MCP-клиентов (Cursor, Claude Code, …).', '/docs/mcp-tools'],
|
|
223
|
-
]
|
|
224
|
-
: [
|
|
225
|
-
['Work contract', 'BVC atom with Basis, Vector, Goal and checks.', '/docs/bvc-spec'],
|
|
226
|
-
['Verification matrix', 'Tier A/B/C readiness for agent work.', '/docs/verification-matrix'],
|
|
227
|
-
['MCP tools', 'Tool contracts for MCP clients (Cursor, Claude Code, …).', '/docs/mcp-tools'],
|
|
228
|
-
];
|
|
229
|
-
return `<section class="related-templates">
|
|
230
|
-
<div class="related-inner">
|
|
231
|
-
<h2>${escapeHtml(locale === 'ru' ? 'Связанные шаблоны' : 'Related templates')}</h2>
|
|
232
|
-
<div class="related-grid">${cards.map(([title, body, href]) => `<article class="related-card">
|
|
233
|
-
<div class="related-preview"><span></span><span></span><span></span></div>
|
|
234
|
-
<h3>${escapeHtml(title)}</h3>
|
|
235
|
-
<p>${escapeHtml(body)}</p>
|
|
236
|
-
<a href="${withLangAndTheme(href, locale, theme)}">${escapeHtml(locale === 'ru' ? 'Открыть →' : 'Open →')}</a>
|
|
237
|
-
</article>`).join('')}</div>
|
|
238
|
-
</div>
|
|
239
|
-
</section>`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
386
|
function renderBottomCta(copy, locale, theme) {
|
|
387
|
+
const installHref = `${withLocalePath('/', locale)}#install`;
|
|
243
388
|
return `<section class="bottom-cta">
|
|
244
|
-
<
|
|
245
|
-
|
|
389
|
+
<div class="bottom-cta__inner">
|
|
390
|
+
<h2>${escapeHtml(locale === 'ru' ? 'Готовы поставить Work Graph локально?' : 'Ready to install Work Graph locally?')}</h2>
|
|
391
|
+
${renderUiButton({ href: installHref, label: copy.hero.primary, variant: 'primary', size: 'lg' })}
|
|
392
|
+
</div>
|
|
246
393
|
</section>`;
|
|
247
394
|
}
|
|
248
395
|
|
|
249
|
-
function
|
|
396
|
+
function renderFooter(locale) {
|
|
397
|
+
const year = new Date().getFullYear();
|
|
398
|
+
const home = withLocalePath('/', locale);
|
|
399
|
+
const brandLinks = locale === 'ru'
|
|
400
|
+
? [
|
|
401
|
+
['https://github.com/bvc-lang/work-graph', 'GitHub'],
|
|
402
|
+
[withLocalePath('/docs', locale), 'Документация'],
|
|
403
|
+
['/llms.txt', 'llms.txt'],
|
|
404
|
+
]
|
|
405
|
+
: [
|
|
406
|
+
['https://github.com/bvc-lang/work-graph', 'GitHub'],
|
|
407
|
+
[withLocalePath('/docs', locale), 'Documentation'],
|
|
408
|
+
['/llms.txt', 'llms.txt'],
|
|
409
|
+
];
|
|
250
410
|
const columns = locale === 'ru'
|
|
251
411
|
? [
|
|
252
|
-
['Продукт', [
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
412
|
+
['Продукт', [
|
|
413
|
+
[withLocalePath('/product', locale), 'Аналитика'],
|
|
414
|
+
[withLocalePath('/product', locale), 'Задачи BVC'],
|
|
415
|
+
[withLocalePath('/product', locale), 'Доска'],
|
|
416
|
+
[withLocalePath('/evidence-ledger', locale), 'Проверки'],
|
|
417
|
+
]],
|
|
418
|
+
['Документация', [
|
|
419
|
+
[withLocalePath('/docs/bvc-spec', locale), 'BVC'],
|
|
420
|
+
[withLocalePath('/docs/mcp-tools', locale), 'MCP'],
|
|
421
|
+
[withLocalePath('/docs/verification-matrix', locale), 'Матрица проверок'],
|
|
422
|
+
[withLocalePath('/docs', locale), 'Все документы'],
|
|
423
|
+
]],
|
|
424
|
+
['Для агентов', [
|
|
425
|
+
['/llms.txt', 'llms.txt'],
|
|
426
|
+
[withLocalePath('/docs', locale), 'Markdown'],
|
|
427
|
+
['/.well-known/mcp.json', 'MCP discovery'],
|
|
428
|
+
[withLocalePath('/docs', locale), 'JSON contexts'],
|
|
429
|
+
]],
|
|
256
430
|
]
|
|
257
431
|
: [
|
|
258
|
-
['Product', [
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
432
|
+
['Product', [
|
|
433
|
+
[withLocalePath('/product', locale), 'Analytics'],
|
|
434
|
+
[withLocalePath('/product', locale), 'Work items'],
|
|
435
|
+
[withLocalePath('/product', locale), 'Board'],
|
|
436
|
+
[withLocalePath('/evidence-ledger', locale), 'Verification'],
|
|
437
|
+
]],
|
|
438
|
+
['Docs', [
|
|
439
|
+
[withLocalePath('/docs/bvc-spec', locale), 'BVC'],
|
|
440
|
+
[withLocalePath('/docs/mcp-tools', locale), 'MCP'],
|
|
441
|
+
[withLocalePath('/docs/verification-matrix', locale), 'Verification matrix'],
|
|
442
|
+
[withLocalePath('/docs', locale), 'All docs'],
|
|
443
|
+
]],
|
|
444
|
+
['For agents', [
|
|
445
|
+
['/llms.txt', 'llms.txt'],
|
|
446
|
+
[withLocalePath('/docs', locale), 'Markdown'],
|
|
447
|
+
['/.well-known/mcp.json', 'MCP discovery'],
|
|
448
|
+
[withLocalePath('/docs', locale), 'JSON contexts'],
|
|
449
|
+
]],
|
|
262
450
|
];
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function renderProofStats(locale) {
|
|
267
|
-
const stats = locale === 'ru'
|
|
451
|
+
const legal = locale === 'ru'
|
|
268
452
|
? [
|
|
269
|
-
['
|
|
270
|
-
['
|
|
271
|
-
['3', 'MCP-инструмента для агента'],
|
|
272
|
-
['0', 'обязательных SaaS/БД'],
|
|
453
|
+
[withLocalePath('/docs', locale), 'Документация'],
|
|
454
|
+
['https://github.com/bvc-lang/work-graph', 'GitHub'],
|
|
273
455
|
]
|
|
274
456
|
: [
|
|
275
|
-
['
|
|
276
|
-
['
|
|
277
|
-
['3', 'MCP tools for agents'],
|
|
278
|
-
['0', 'required SaaS/database'],
|
|
457
|
+
[withLocalePath('/docs', locale), 'Documentation'],
|
|
458
|
+
['https://github.com/bvc-lang/work-graph', 'GitHub'],
|
|
279
459
|
];
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
460
|
+
const external = (href) => href.startsWith('http');
|
|
461
|
+
return `<footer class="site-footer">
|
|
462
|
+
<div class="site-footer__panel">
|
|
463
|
+
<div class="site-footer__grid">
|
|
464
|
+
<div class="site-footer__brand">
|
|
465
|
+
<a class="site-footer__logo" href="${escapeAttr(home)}" aria-label="Work Graph">
|
|
466
|
+
<img src="/assets/workgraph-logo.svg" width="148" height="22" alt="Work Graph" decoding="async">
|
|
467
|
+
</a>
|
|
468
|
+
<nav class="site-footer__brand-links" aria-label="${escapeAttr(locale === 'ru' ? 'Быстрые ссылки' : 'Quick links')}">
|
|
469
|
+
${brandLinks.map(([href, label]) => `<a href="${escapeAttr(href)}"${external(href) ? ' target="_blank" rel="noopener noreferrer"' : ''}>${escapeHtml(label)}</a>`).join('')}
|
|
470
|
+
</nav>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="footer-columns">${columns.map(([title, items]) => `<div class="footer-column">
|
|
473
|
+
<h3>${escapeHtml(title)}</h3>
|
|
474
|
+
${items.map(([href, label]) => `<a href="${escapeAttr(href)}"${external(href) ? ' target="_blank" rel="noopener noreferrer"' : ''}>${escapeHtml(label)}</a>`).join('')}
|
|
475
|
+
</div>`).join('')}</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
<div class="site-footer__bottom">
|
|
479
|
+
<p class="site-footer__copy">${escapeHtml(locale === 'ru' ? `© ${year} Work Graph` : `Copyright © ${year} Work Graph`)}</p>
|
|
480
|
+
<nav class="site-footer__legal" aria-label="${escapeAttr(locale === 'ru' ? 'Правовая информация' : 'Legal')}">
|
|
481
|
+
${legal.map(([href, label]) => `<a href="${escapeAttr(href)}"${external(href) ? ' target="_blank" rel="noopener noreferrer"' : ''}>${escapeHtml(label)}</a>`).join('')}
|
|
482
|
+
</nav>
|
|
483
|
+
</div>
|
|
484
|
+
</footer>`;
|
|
283
485
|
}
|
|
284
486
|
|
|
285
|
-
function renderGraphTrinity(locale) {
|
|
487
|
+
function renderGraphTrinity(locale, options = {}) {
|
|
286
488
|
const graphs = locale === 'ru'
|
|
287
489
|
? [
|
|
288
490
|
['Граф намерений', 'Что и зачем', 'BVC-атомы, AN → Epic → Work Item, связи depends_on и trace.*.'],
|
|
@@ -294,194 +496,353 @@ function renderGraphTrinity(locale) {
|
|
|
294
496
|
['Execution Graph', 'How and by whom', 'work.id tasks, evidence, todo → ready → doing → done/blocked states and gates.'],
|
|
295
497
|
['Memory Graph', 'What was decided and why', 'Closed tasks with valid evidence, audit trail and RAG context from git.'],
|
|
296
498
|
];
|
|
499
|
+
const title = options.title ?? (locale === 'ru' ? 'Три графа — один цикл разработки' : 'Three graphs, one development loop');
|
|
500
|
+
const body = options.body ?? (locale === 'ru'
|
|
501
|
+
? 'Каждый граф — самостоятельный слой с чётким контрактом. Вместе они дают цикл, который агент может исполнять, а человек — аудировать.'
|
|
502
|
+
: 'Each graph is a standalone layer with a clear contract. Together they form a loop an agent can execute and a human can audit.');
|
|
503
|
+
const cards = graphs.map(([graphTitle, subtitle, graphBody], index) => `<article class="graph-flow-card">
|
|
504
|
+
<span class="graph-flow-step" aria-hidden="true">${index + 1}</span>
|
|
505
|
+
<h3 class="graph-flow-card-title">${escapeHtml(graphTitle)}</h3>
|
|
506
|
+
<p class="graph-flow-card-lead">${escapeHtml(subtitle)}</p>
|
|
507
|
+
<p class="graph-flow-card-body">${escapeHtml(graphBody)}</p>
|
|
508
|
+
</article>`);
|
|
509
|
+
const flow = cards
|
|
510
|
+
.flatMap((card, index) => (index < cards.length - 1 ? [card, '<span class="graph-flow-arrow" aria-hidden="true">→</span>'] : [card]))
|
|
511
|
+
.join('');
|
|
297
512
|
return `<section class="graph-trinity">
|
|
298
|
-
<div class="
|
|
299
|
-
<
|
|
300
|
-
<h2>${escapeHtml(locale === 'ru' ? 'Три графа — один цикл разработки' : 'Three graphs, one development loop')}</h2>
|
|
301
|
-
<p>${escapeHtml(locale === 'ru' ? 'Каждый граф — самостоятельный слой с чётким контрактом. Вместе они дают цикл, который агент может исполнять, а человек — аудировать.' : 'Each graph is a standalone layer with a clear contract. Together they form a loop an agent can execute and a human can audit.')}</p>
|
|
302
|
-
</div>
|
|
303
|
-
<div class="graph-flow">${graphs.map(([title, subtitle, body], index) => `<article>
|
|
304
|
-
<span>${index + 1}</span>
|
|
305
|
-
<h3>${escapeHtml(title)}</h3>
|
|
306
|
-
<strong>${escapeHtml(subtitle)}</strong>
|
|
513
|
+
<div class="graph-trinity-heading">
|
|
514
|
+
<h2>${escapeHtml(title)}</h2>
|
|
307
515
|
<p>${escapeHtml(body)}</p>
|
|
308
|
-
</
|
|
516
|
+
</div>
|
|
517
|
+
<div class="graph-flow">${flow}</div>
|
|
309
518
|
</section>`;
|
|
310
519
|
}
|
|
311
520
|
|
|
312
521
|
function renderProductPillars(locale) {
|
|
313
|
-
const
|
|
314
|
-
? [
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
['Readiness gates', 'done without evidence is a policy error. assert_task_ready_for_done returns ok or violations[].'],
|
|
324
|
-
['Audit memory', 'Memory is derived from closed tasks with valid evidence, not from a chat summary.'],
|
|
325
|
-
];
|
|
326
|
-
return `<section class="pillar-section">
|
|
327
|
-
<div class="wide-heading">
|
|
328
|
-
<p class="eyebrow">${escapeHtml(locale === 'ru' ? 'Что внутри' : 'What is inside')}</p>
|
|
329
|
-
<h2>${escapeHtml(locale === 'ru' ? 'Контрактный контур: намерение → исполнение → память' : 'Contract loop: intent → execution → memory')}</h2>
|
|
330
|
-
</div>
|
|
331
|
-
<div class="pillar-grid">${pillars.map(([title, body], index) => `<article>
|
|
332
|
-
<span>${index + 1}</span>
|
|
333
|
-
<h3>${escapeHtml(title)}</h3>
|
|
334
|
-
<p>${escapeHtml(body)}</p>
|
|
335
|
-
</article>`).join('')}</div>
|
|
336
|
-
</section>`;
|
|
522
|
+
const items = locale === 'ru'
|
|
523
|
+
? ['Аналитика', 'Задачи BVC', 'Доска задач', 'Проверки', 'Память проекта', 'MCP-инструменты']
|
|
524
|
+
: ['Analytics', 'BVC work items', 'Kanban board', 'Verification', 'Project memory', 'MCP tools'];
|
|
525
|
+
return renderIconLabelGrid({
|
|
526
|
+
title: locale === 'ru' ? 'Контрактный контур: намерение → исполнение → память' : 'Contract loop: intent → execution → memory',
|
|
527
|
+
body: locale === 'ru'
|
|
528
|
+
? 'Свяжите стратегию с исполнением: от AN-разбора до проверенной памяти в git — в одном локальном графе работ.'
|
|
529
|
+
: 'Connect strategy to execution: from AN review to verified memory in git — in one local work graph.',
|
|
530
|
+
items,
|
|
531
|
+
});
|
|
337
532
|
}
|
|
338
533
|
|
|
339
|
-
function renderCodeShowcase(locale) {
|
|
534
|
+
function renderCodeShowcase(locale, options = {}) {
|
|
340
535
|
const bvc = locale === 'ru'
|
|
341
|
-
? `#ImplementTraceLinksV1@ru<[
|
|
342
|
-
|
|
343
|
-
|
|
536
|
+
? `#ImplementTraceLinksV1@ru<[
|
|
537
|
+
Базис:
|
|
538
|
+
Текущая трассировка шагов не валидируется в CI
|
|
539
|
+
Нет связи work.id ↔ файлы ↔ тесты
|
|
540
|
+
Вектор:
|
|
541
|
+
Реализовать валидатор трассировки
|
|
542
|
+
Добавить MCP-инструмент get_unified_linkage
|
|
543
|
+
Цель:
|
|
544
|
+
Любая задача с trace.* метками имеет автоматическую проверку целостности
|
|
545
|
+
|
|
546
|
+
Метки:
|
|
547
|
+
profile: work_item
|
|
548
|
+
tier: A
|
|
549
|
+
trace.codegen: false
|
|
550
|
+
|
|
551
|
+
Checks:
|
|
552
|
+
npm run test:deterministic
|
|
553
|
+
bvc lint intent/**/implement-trace-links-v1.work.bvc
|
|
554
|
+
]>`
|
|
555
|
+
: `#ImplementTraceLinksV1@en<[
|
|
556
|
+
Basis:
|
|
557
|
+
Current step tracing is not validated in CI
|
|
558
|
+
There is no work.id ↔ files ↔ tests linkage
|
|
559
|
+
Vector:
|
|
560
|
+
Implement trace validator
|
|
561
|
+
Add MCP tool get_unified_linkage
|
|
562
|
+
Goal:
|
|
563
|
+
Any task with trace.* labels has automatic integrity checks
|
|
564
|
+
|
|
565
|
+
Labels:
|
|
566
|
+
profile: work_item
|
|
567
|
+
tier: A
|
|
568
|
+
trace.codegen: false
|
|
569
|
+
|
|
570
|
+
Checks:
|
|
571
|
+
npm run test:deterministic
|
|
572
|
+
bvc lint intent/**/implement-trace-links-v1.work.bvc
|
|
573
|
+
]>`;
|
|
574
|
+
const mcp = `claim_work_item("implement-trace-links-v1")
|
|
575
|
+
→ get_work_contract(work_id)
|
|
576
|
+
→ edit target_files
|
|
577
|
+
→ run allowed commands
|
|
578
|
+
→ validate_evidence(structured_json)
|
|
579
|
+
→ assert_task_ready_for_done(work_id)
|
|
580
|
+
→ add_work_item_evidence + complete`;
|
|
581
|
+
const title = options.title ?? (locale === 'ru' ? 'Задача читается человеком, git и агентом' : 'A task is readable by humans, git and agents');
|
|
582
|
+
const body = options.body ?? (locale === 'ru'
|
|
583
|
+
? 'BVC описывает намерение, projection задаёт исполнение, evidence превращает результат в память.'
|
|
584
|
+
: 'BVC describes intent, projection defines execution and evidence turns the result into memory.');
|
|
344
585
|
return `<section class="code-showcase">
|
|
345
586
|
<div>
|
|
346
|
-
<
|
|
347
|
-
<
|
|
348
|
-
<p>${escapeHtml(locale === 'ru' ? 'BVC описывает намерение, projection задаёт исполнение, evidence превращает результат в память.' : 'BVC describes intent, projection defines execution and evidence turns the result into memory.')}</p>
|
|
587
|
+
<h2>${escapeHtml(title)}</h2>
|
|
588
|
+
<p>${escapeHtml(body)}</p>
|
|
349
589
|
</div>
|
|
350
590
|
<div class="code-tabs">
|
|
351
|
-
<div><strong>work.bvc</strong><pre><code>${
|
|
352
|
-
<div><strong>MCP flow</strong><pre><code>${
|
|
591
|
+
<div><strong>work.bvc</strong><pre><code class="code-block language-bvc">${highlightBvcBlock(bvc)}</code></pre></div>
|
|
592
|
+
<div><strong>MCP flow</strong><pre><code class="code-block language-mcp">${highlightMcpFlow(mcp)}</code></pre></div>
|
|
353
593
|
</div>
|
|
354
594
|
</section>`;
|
|
355
595
|
}
|
|
356
596
|
|
|
357
|
-
function renderAudience(locale) {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
597
|
+
function renderAudience(locale, copy) {
|
|
598
|
+
const ru = locale === 'ru';
|
|
599
|
+
return renderSiteSectionsBlock(
|
|
600
|
+
{
|
|
601
|
+
title: ru ? 'Для кого Work Graph' : 'Who Work Graph is for',
|
|
602
|
+
sections: ru
|
|
603
|
+
? [
|
|
604
|
+
{ title: 'Для техлида и архитектора', body: 'Видите связь решений, задач и evidence в одном графе: AN-разборы, BVC-контракты и проверки остаются в git, а не в пересказе чата.', icon: 'eye' },
|
|
605
|
+
{ title: 'Для разработчика', body: 'Один бэклог и понятный get_work_contract перед кодом: меньше импровизации, ясные targetFiles и статусы на доске.', icon: 'users-three' },
|
|
606
|
+
{ title: 'Для агента', body: 'Явные input/output/verification, allowlist команд и structured evidence: агент исполняет контракт, а не объявляет «готово» словами.', icon: 'chart-line-up' },
|
|
607
|
+
]
|
|
608
|
+
: [
|
|
609
|
+
{ title: 'For tech leads and architects', body: 'See decisions, work items and evidence in one graph: AN reviews, BVC contracts and checks stay in git, not in chat summaries.', icon: 'eye' },
|
|
610
|
+
{ title: 'For developers', body: 'One backlog and a clear get_work_contract before coding: less improvisation, explicit targetFiles and board states.', icon: 'users-three' },
|
|
611
|
+
{ title: 'For agents', body: 'Explicit input/output/verification, command allowlists and structured evidence: the agent executes the contract instead of saying done in prose.', icon: 'chart-line-up' },
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
copy,
|
|
615
|
+
locale,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function renderInstallCopyIcon() {
|
|
620
|
+
return renderInlineIcon('copy-bold.svg', { className: 'install-copy-icon', size: 18 });
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function renderInstallCodeBlock(command, locale) {
|
|
624
|
+
const copyLabel = locale === 'ru' ? 'Копировать' : 'Copy';
|
|
625
|
+
const copiedLabel = locale === 'ru' ? 'Скопировано' : 'Copied';
|
|
626
|
+
return `<div class="install-code">
|
|
627
|
+
<code>${escapeHtml(command)}</code>
|
|
628
|
+
<button type="button" class="install-copy-btn" data-copy-text="${escapeAttr(command)}" data-copy-label="${escapeAttr(copyLabel)}" data-copied-label="${escapeAttr(copiedLabel)}" aria-label="${escapeAttr(copyLabel)}" title="${escapeAttr(copyLabel)}">${renderInstallCopyIcon()}<span class="install-copy-btn-text">${escapeHtml(copyLabel)}</span></button>
|
|
629
|
+
</div>`;
|
|
373
630
|
}
|
|
374
631
|
|
|
375
632
|
function renderInstallInstructions(locale) {
|
|
376
|
-
const
|
|
377
|
-
?
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
633
|
+
const copy = locale === 'ru'
|
|
634
|
+
? {
|
|
635
|
+
title: 'Как установить Work Graph',
|
|
636
|
+
lead: 'Установка в существующий репозиторий: локальный бэклог, UI и MCP для агента. Нужны Node.js 20+ и npm.',
|
|
637
|
+
projectDir: 'В каталоге проекта выполните:',
|
|
638
|
+
script: 'cd /path/to/your-project\nnpx @work-graph/cli init .\nnpm install\nnpm run workgraph:ui',
|
|
639
|
+
openUi: 'Откройте в браузере:',
|
|
640
|
+
uiUrl: 'http://127.0.0.1:4177/',
|
|
641
|
+
agentLead: 'Для агентов:',
|
|
642
|
+
agentQuote: 'Установи Work Graph в этот проект https://www.npmjs.com/package/@work-graph/cli и открой локальный UI.',
|
|
643
|
+
detail: 'Команда init создаёт .work-graph/config.json, intent/, npm-скрипты и при необходимости .cursor/mcp.json (npx -y @work-graph/mcp, WORKGRAPH_ROOT). Существующие intent/index.bvc и architecture/main.bvc сохраняются. После npm install перезагрузите MCP в IDE.',
|
|
644
|
+
verify: 'Проверка: npm run workgraph:doctor',
|
|
645
|
+
guideLabel: 'Подробная инструкция',
|
|
646
|
+
guideHref: 'https://github.com/bvc-lang/work-graph/blob/main/docs/getting-started.md',
|
|
647
|
+
}
|
|
648
|
+
: {
|
|
649
|
+
title: 'How to install Work Graph',
|
|
650
|
+
lead: 'Install into an existing repository: local backlog, operator UI, and MCP for your agent. Requires Node.js 20+ and npm.',
|
|
651
|
+
projectDir: 'In your project directory, run:',
|
|
652
|
+
script: 'cd /path/to/your-project\nnpx @work-graph/cli init .\nnpm install\nnpm run workgraph:ui',
|
|
653
|
+
openUi: 'Then open:',
|
|
654
|
+
uiUrl: 'http://127.0.0.1:4177/',
|
|
655
|
+
agentLead: 'For agents:',
|
|
656
|
+
agentQuote: 'Install Work Graph in this project https://www.npmjs.com/package/@work-graph/cli and open the local UI.',
|
|
657
|
+
detail: 'init writes .work-graph/config.json, intent/, npm scripts, and optional IDE files (for example .cursor/mcp.json with npx -y @work-graph/mcp and WORKGRAPH_ROOT). Existing intent/index.bvc and architecture/main.bvc are preserved. Reload MCP in your IDE after npm install.',
|
|
658
|
+
verify: 'Verify: npm run workgraph:doctor',
|
|
659
|
+
guideLabel: 'Detailed install guide',
|
|
660
|
+
guideHref: 'https://github.com/bvc-lang/work-graph/blob/main/docs/getting-started.md',
|
|
661
|
+
};
|
|
389
662
|
return `<section id="install" class="install-section">
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
<p>${escapeHtml(
|
|
663
|
+
<h2>${escapeHtml(copy.title)}</h2>
|
|
664
|
+
<p class="install-lead">${escapeHtml(copy.lead)}</p>
|
|
665
|
+
<div class="install-block">
|
|
666
|
+
<p class="install-block-label">${escapeHtml(copy.agentLead)}</p>
|
|
667
|
+
${renderInstallCodeBlock(copy.agentQuote, locale)}
|
|
394
668
|
</div>
|
|
395
|
-
<
|
|
396
|
-
<
|
|
397
|
-
|
|
398
|
-
</
|
|
669
|
+
<div class="install-block">
|
|
670
|
+
<p class="install-block-label">${escapeHtml(copy.projectDir)}</p>
|
|
671
|
+
${renderInstallCodeBlock(copy.script, locale)}
|
|
672
|
+
</div>
|
|
673
|
+
<div class="install-block">
|
|
674
|
+
<p class="install-block-label">${escapeHtml(copy.openUi)}</p>
|
|
675
|
+
${renderInstallCodeBlock(copy.uiUrl, locale)}
|
|
676
|
+
</div>
|
|
677
|
+
<p class="install-detail">${escapeHtml(copy.detail)}</p>
|
|
678
|
+
<p class="install-detail">${escapeHtml(copy.verify)}</p>
|
|
679
|
+
<p class="install-guide"><a class="install-guide-link" href="${escapeAttr(copy.guideHref)}" target="_blank" rel="noopener noreferrer">${escapeHtml(copy.guideLabel)} →</a></p>
|
|
399
680
|
</section>`;
|
|
400
681
|
}
|
|
401
682
|
|
|
402
683
|
function renderComparisonStrip(locale) {
|
|
403
684
|
const rows = locale === 'ru'
|
|
404
685
|
? [
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
686
|
+
{ iconName: 'robot', name: 'Обычный AI-воркфлоу', does: 'намерение в голове или чате', gap: 'готово = слова агента' },
|
|
687
|
+
{ iconName: 'kanban', name: 'Jira / Linear', does: 'планирует работу и статусы в облаке', gap: 'контракт и evidence не в git-репозитории' },
|
|
688
|
+
{ iconName: 'test-tube', name: 'CI / тесты', does: 'проверяет команды', gap: 'не знает зачем была задача' },
|
|
689
|
+
{ iconName: 'graph', name: 'Work Graph', does: 'связывает намерение, исполнение и память', gap: 'готово = evidence + verified gate' },
|
|
409
690
|
]
|
|
410
691
|
: [
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
692
|
+
{ iconName: 'robot', name: 'Plain AI workflow', does: 'intent lives in heads or chats', gap: 'done = agent words' },
|
|
693
|
+
{ iconName: 'kanban', name: 'Jira / Linear', does: 'plans work and statuses in the cloud', gap: 'contract and evidence live outside the repo' },
|
|
694
|
+
{ iconName: 'test-tube', name: 'CI / tests', does: 'checks commands', gap: 'does not know why the task exists' },
|
|
695
|
+
{ iconName: 'graph', name: 'Work Graph', does: 'links intent, execution and memory', gap: 'done = evidence + verified gate' },
|
|
415
696
|
];
|
|
416
697
|
return `<section class="comparison-strip">
|
|
417
|
-
<h2>${escapeHtml(locale === 'ru' ? 'Ключевое отличие от обычного AI-воркфлоу' : 'What changes compared to a plain AI workflow')}</h2>
|
|
418
|
-
<div>${rows.map((
|
|
419
|
-
<
|
|
420
|
-
<
|
|
421
|
-
<
|
|
698
|
+
<h2 class="comparison-strip-heading">${escapeHtml(locale === 'ru' ? 'Ключевое отличие от обычного AI-воркфлоу' : 'What changes compared to a plain AI workflow')}</h2>
|
|
699
|
+
<div class="comparison-strip-grid">${rows.map(({ iconName, name, does, gap }) => `<article class="comparison-strip-card">
|
|
700
|
+
<span class="comparison-strip-icon" aria-hidden="true">${comparisonStripIcon(iconName)}</span>
|
|
701
|
+
<h3 class="comparison-strip-card-title">${escapeHtml(name)}</h3>
|
|
702
|
+
<p class="comparison-strip-card-lead">${escapeHtml(does)}</p>
|
|
703
|
+
<p class="comparison-strip-card-gap">${escapeHtml(gap)}</p>
|
|
422
704
|
</article>`).join('')}</div>
|
|
423
705
|
</section>`;
|
|
424
706
|
}
|
|
425
707
|
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
['Does it need a database?', 'Not for the public site. Static export builds a folder that can be hosted as plain files.'],
|
|
440
|
-
];
|
|
441
|
-
return `<section class="roadmap-faq">
|
|
442
|
-
<div>
|
|
443
|
-
<h2>${escapeHtml(locale === 'ru' ? 'Roadmap' : 'Roadmap')}</h2>
|
|
444
|
-
<ul>${roadmap.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
|
|
445
|
-
</div>
|
|
446
|
-
<div>
|
|
447
|
-
<h2>${escapeHtml(locale === 'ru' ? 'Вопросы и ответы' : 'FAQ')}</h2>
|
|
448
|
-
${faq.map(([question, answer]) => `<details><summary>${escapeHtml(question)}</summary><p>${escapeHtml(answer)}</p></details>`).join('')}
|
|
708
|
+
function renderFaqToggleIcon() {
|
|
709
|
+
return '<span class="faq-toggle" aria-hidden="true"></span>';
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function renderHomeFaq(locale) {
|
|
713
|
+
const items = getLocalizedFaq(locale).flatMap((category) => category.items);
|
|
714
|
+
return `<section id="faq" class="home-faq" aria-labelledby="home-faq-title">
|
|
715
|
+
<h2 id="home-faq-title" class="home-faq-title">FAQ</h2>
|
|
716
|
+
<div class="faq-accordion">
|
|
717
|
+
${items.map((item) => `<details class="faq-item">
|
|
718
|
+
<summary><span class="faq-question">${escapeHtml(item.question)}</span>${renderFaqToggleIcon()}</summary>
|
|
719
|
+
<div class="faq-answer"><p>${escapeHtml(item.answer)}</p></div>
|
|
720
|
+
</details>`).join('')}
|
|
449
721
|
</div>
|
|
722
|
+
<p class="faq-json-note">${escapeHtml(locale === 'ru' ? 'Для LLM и интеграций: ' : 'For LLMs and integrations: ')}<a href="/faq.json">/faq.json</a></p>
|
|
450
723
|
</section>`;
|
|
451
724
|
}
|
|
452
725
|
|
|
453
|
-
function
|
|
454
|
-
|
|
455
|
-
|
|
726
|
+
function renderPageSectionsGrid(sections, copy) {
|
|
727
|
+
return `<div class="content-layout page-sections">
|
|
728
|
+
<div class="article-column">
|
|
729
|
+
<div class="section-grid">${sections.map((section) => renderSection(section, copy)).join('')}</div>
|
|
730
|
+
</div>
|
|
731
|
+
</div>`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function renderSiteSectionsBlock(block, copy, locale) {
|
|
735
|
+
const sections = resolveSiteSections(block.sections ?? [], locale);
|
|
736
|
+
const heading = block.title
|
|
737
|
+
? `<div class="wide-heading site-sections-block__heading"><h2>${escapeHtml(block.title)}</h2>${block.intro ? `<p>${escapeHtml(block.intro)}</p>` : ''}</div>`
|
|
738
|
+
: '';
|
|
739
|
+
return `${heading}${renderPageSectionsGrid(sections, copy)}`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function featureColumnsToSiteSections(block) {
|
|
743
|
+
return {
|
|
744
|
+
title: block.title,
|
|
745
|
+
intro: block.intro,
|
|
746
|
+
sections: (block.items ?? []).map(({ iconName, heading, body, badges }) => ({
|
|
747
|
+
title: heading,
|
|
748
|
+
body,
|
|
749
|
+
icon: iconName,
|
|
750
|
+
badges,
|
|
751
|
+
})),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function renderPageLead(block) {
|
|
756
|
+
return `<section class="page-lead wide-heading"><p>${escapeHtml(block.text)}</p></section>`;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function renderWorkflowPipeline(block) {
|
|
760
|
+
return `<section class="workflow-pipeline">
|
|
456
761
|
<div class="wide-heading">
|
|
457
|
-
<
|
|
458
|
-
|
|
459
|
-
<p>${escapeHtml(locale === 'ru' ? 'Раздел структурирован для быстрого поиска человеком и для точного парсинга LLM-агентами.' : 'Structured for fast human search and precise LLM-agent parsing.')}</p>
|
|
762
|
+
<h2>${escapeHtml(block.title)}</h2>
|
|
763
|
+
${block.intro ? `<p>${escapeHtml(block.intro)}</p>` : ''}
|
|
460
764
|
</div>
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
</details>`).join('')}
|
|
467
|
-
</section>`).join('')}
|
|
468
|
-
<section class="faq-json-note">
|
|
469
|
-
<h3>${escapeHtml(locale === 'ru' ? 'Для разработчиков и LLM' : 'For developers and LLMs')}</h3>
|
|
470
|
-
<p>${escapeHtml(locale === 'ru' ? 'Структурированная версия доступна как Schema.org FAQPage по адресу /faq.json.' : 'A structured Schema.org FAQPage version is available at /faq.json.')}</p>
|
|
471
|
-
<a href="/faq.json">/faq.json</a>
|
|
472
|
-
</section>
|
|
765
|
+
<ol class="workflow-pipeline-steps">${block.steps.map(({ label, detail, iconName }) => `<li>
|
|
766
|
+
${iconName ? `<span class="workflow-pipeline-icon" aria-hidden="true">${workflowPipelineIcon(iconName)}</span>` : ''}
|
|
767
|
+
<span class="workflow-pipeline-label">${escapeHtml(label)}</span>
|
|
768
|
+
<span class="workflow-pipeline-detail">${escapeHtml(detail)}</span>
|
|
769
|
+
</li>`).join('')}</ol>
|
|
473
770
|
</section>`;
|
|
474
771
|
}
|
|
475
772
|
|
|
476
|
-
function
|
|
477
|
-
|
|
773
|
+
function renderCompareBoundaries(block) {
|
|
774
|
+
const muted = block.variant === 'muted';
|
|
775
|
+
return `<section class="compare-boundaries${muted ? ' compare-boundaries--muted' : ''}">
|
|
776
|
+
<h2 class="compare-boundaries-heading">${escapeHtml(block.title)}</h2>
|
|
777
|
+
<div class="compare-boundaries-grid">${block.items.map(({ iconName, heading, body }) => `<article>
|
|
778
|
+
<span class="compare-boundaries-icon" aria-hidden="true">${featureIcon(iconName)}</span>
|
|
779
|
+
<h3>${escapeHtml(heading)}</h3>
|
|
780
|
+
<p>${escapeHtml(body)}</p>
|
|
781
|
+
</article>`).join('')}</div>
|
|
782
|
+
</section>`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function resolveSiteSections(sections, locale) {
|
|
786
|
+
return sections.map((section) => {
|
|
787
|
+
if (section.competitors) {
|
|
788
|
+
const { competitors: _flag, ...rest } = section;
|
|
789
|
+
return { ...rest, competitors: getPublicSiteCompetitors(locale) };
|
|
790
|
+
}
|
|
791
|
+
return section;
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function renderPageBlock(block, locale, copy) {
|
|
796
|
+
switch (block.type) {
|
|
797
|
+
case 'lead':
|
|
798
|
+
return renderPageLead(block);
|
|
799
|
+
case 'pipeline':
|
|
800
|
+
return renderWorkflowPipeline(block);
|
|
801
|
+
case 'featureColumns':
|
|
802
|
+
return renderSiteSectionsBlock(featureColumnsToSiteSections(block), copy, locale);
|
|
803
|
+
case 'iconLabelGrid':
|
|
804
|
+
return renderIconLabelGrid(block);
|
|
805
|
+
case 'screenshotGallery':
|
|
806
|
+
return renderScreenshotGallery(locale, block);
|
|
807
|
+
case 'graphTrinity':
|
|
808
|
+
return renderGraphTrinity(locale, block);
|
|
809
|
+
case 'codeShowcase':
|
|
810
|
+
return renderCodeShowcase(locale, block);
|
|
811
|
+
case 'comparisonStrip':
|
|
812
|
+
return renderComparisonStrip(locale);
|
|
813
|
+
case 'siteSections':
|
|
814
|
+
return renderSiteSectionsBlock(block, copy, locale);
|
|
815
|
+
case 'boundaries':
|
|
816
|
+
return renderCompareBoundaries(block);
|
|
817
|
+
default:
|
|
818
|
+
return '';
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function renderPageBlocks(page, locale, copy) {
|
|
823
|
+
const blocks = page.blocks ?? [];
|
|
824
|
+
if (blocks.length === 0 && page.sections?.length) {
|
|
825
|
+
return `<div class="page-flow">${renderPageSectionsGrid(page.sections, copy)}</div>`;
|
|
826
|
+
}
|
|
827
|
+
return `<div class="page-flow">${blocks.map((block) => renderPageBlock(block, locale, copy)).join('')}</div>`;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function renderHomePageSections(locale, copy, page) {
|
|
831
|
+
const introSections = page.sections.map((section) => renderSection(section, copy)).join('');
|
|
832
|
+
return `${renderSteps(copy, locale)}
|
|
833
|
+
${renderInstallInstructions(locale)}
|
|
834
|
+
${renderScreenshotGallery(locale)}
|
|
478
835
|
${renderGraphTrinity(locale)}
|
|
479
836
|
${renderProductPillars(locale)}
|
|
480
|
-
|
|
837
|
+
<div class="content-layout home-pillars">
|
|
838
|
+
<div class="article-column">
|
|
839
|
+
<div class="section-grid">${introSections}</div>
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
481
842
|
${renderCodeShowcase(locale)}
|
|
482
|
-
${renderAudience(locale)}
|
|
483
843
|
${renderComparisonStrip(locale)}
|
|
484
|
-
${
|
|
844
|
+
${renderAudience(locale, copy)}
|
|
845
|
+
${renderHomeFaq(locale)}`;
|
|
485
846
|
}
|
|
486
847
|
|
|
487
848
|
export function renderPublicSiteHtml(page, options = {}) {
|
|
@@ -490,8 +851,9 @@ export function renderPublicSiteHtml(page, options = {}) {
|
|
|
490
851
|
page.locale = locale;
|
|
491
852
|
const copy = getPublicSiteCopy(locale);
|
|
492
853
|
const jsonLd = JSON.stringify(publicSiteJsonLd(page));
|
|
854
|
+
const installHref = page.kind === 'home' ? '#install' : `${withLocalePath('/', locale)}#install`;
|
|
493
855
|
const primaryButton = renderUiButton({
|
|
494
|
-
href:
|
|
856
|
+
href: installHref,
|
|
495
857
|
label: copy.hero.primary,
|
|
496
858
|
variant: 'primary',
|
|
497
859
|
size: 'lg',
|
|
@@ -502,37 +864,19 @@ export function renderPublicSiteHtml(page, options = {}) {
|
|
|
502
864
|
variant: 'secondary',
|
|
503
865
|
size: 'lg',
|
|
504
866
|
});
|
|
867
|
+
const documentTitle = page.documentTitle
|
|
868
|
+
?? (/^Work Graph\b/u.test(page.title) ? page.title : `${page.title} · Work Graph`);
|
|
505
869
|
return `<!doctype html>
|
|
506
870
|
<html lang="${locale}" data-theme="${theme}">
|
|
507
871
|
<head>
|
|
508
872
|
<meta charset="utf-8">
|
|
509
873
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
510
|
-
<title>${escapeHtml(
|
|
874
|
+
<title>${escapeHtml(documentTitle)}</title>
|
|
511
875
|
<meta name="description" content="${escapeAttr(page.description)}">
|
|
512
876
|
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
|
|
513
877
|
<link rel="stylesheet" href="/assets/fonts/GraphikLCG/stylesheet.css">
|
|
514
878
|
<link rel="stylesheet" href="/assets/design-tokens-workgraph-dark.css">
|
|
515
|
-
<script>
|
|
516
|
-
(function () {
|
|
517
|
-
var params = new URLSearchParams(window.location.search);
|
|
518
|
-
var allowedLang = ['en', 'ru'];
|
|
519
|
-
var allowedTheme = ['light', 'dark'];
|
|
520
|
-
var lang = params.get('lang') || localStorage.getItem('workGraphPublicSiteLocale') || '${locale}';
|
|
521
|
-
var theme = params.get('theme') || localStorage.getItem('workGraphPublicSiteTheme') || '${theme}';
|
|
522
|
-
if (!allowedLang.includes(lang)) lang = 'en';
|
|
523
|
-
if (!allowedTheme.includes(theme)) theme = 'light';
|
|
524
|
-
localStorage.setItem('workGraphPublicSiteLocale', lang);
|
|
525
|
-
localStorage.setItem('workGraphPublicSiteTheme', theme);
|
|
526
|
-
document.documentElement.lang = lang;
|
|
527
|
-
document.documentElement.dataset.theme = theme;
|
|
528
|
-
if (!params.get('lang') || !params.get('theme')) {
|
|
529
|
-
var next = new URL(window.location.href);
|
|
530
|
-
next.searchParams.set('lang', lang);
|
|
531
|
-
next.searchParams.set('theme', theme);
|
|
532
|
-
window.history.replaceState(null, '', next);
|
|
533
|
-
}
|
|
534
|
-
})();
|
|
535
|
-
</script>
|
|
879
|
+
<script>${renderPublicSiteBootstrapScript(locale, theme)}</script>
|
|
536
880
|
<script type="application/ld+json">${jsonLd}</script>
|
|
537
881
|
<style>
|
|
538
882
|
:root {
|
|
@@ -558,11 +902,17 @@ export function renderPublicSiteHtml(page, options = {}) {
|
|
|
558
902
|
--ui-control-bg-rgb: 244 245 247;
|
|
559
903
|
--ui-control-bg-hover-rgb: 235 236 240;
|
|
560
904
|
--ui-radius-control: 3px;
|
|
905
|
+
--code-surface: #f4f5f7;
|
|
906
|
+
--code-header: #ebecf0;
|
|
907
|
+
--code-text: #172b4d;
|
|
908
|
+
--site-section-spacing: clamp(96px, 10vw, 128px);
|
|
909
|
+
--footer-heading-color: #172b4d;
|
|
910
|
+
--footer-link-color: #44546f;
|
|
561
911
|
}
|
|
562
912
|
html[data-theme="dark"] {
|
|
563
913
|
color-scheme: dark;
|
|
564
914
|
--bg: #1d2125;
|
|
565
|
-
--header-bg: #
|
|
915
|
+
--header-bg: #1d2125;
|
|
566
916
|
--card: #282e33;
|
|
567
917
|
--card-muted: #22272b;
|
|
568
918
|
--text: #d6dde5;
|
|
@@ -571,157 +921,332 @@ export function renderPublicSiteHtml(page, options = {}) {
|
|
|
571
921
|
--accent: #85b8ff;
|
|
572
922
|
--accent-soft: #092957;
|
|
573
923
|
--shadow: 0 1px 1px rgba(0, 0, 0, .32);
|
|
924
|
+
--code-surface: #22272b;
|
|
925
|
+
--code-header: #2c333a;
|
|
926
|
+
--code-text: #dfe1e6;
|
|
927
|
+
--footer-heading-color: #d6dde5;
|
|
928
|
+
--footer-link-color: #9fadbc;
|
|
574
929
|
}
|
|
575
930
|
* { box-sizing: border-box; }
|
|
576
931
|
body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--brand-font-sans, 'Graphik LCG', ui-sans-serif, system-ui, sans-serif); line-height: 1.55; }
|
|
577
932
|
a { color: var(--accent); }
|
|
578
|
-
.site-header
|
|
579
|
-
.site-header { align-items: center; background: color-mix(in srgb, var(--header-bg) 96%, transparent); backdrop-filter: blur(14px); border-bottom:
|
|
580
|
-
.site-
|
|
581
|
-
.site-
|
|
582
|
-
.site-
|
|
583
|
-
.site-
|
|
933
|
+
.site-header { border-color: var(--border); padding: 16px clamp(18px, 5vw, 72px); }
|
|
934
|
+
.site-header { align-items: center; background: color-mix(in srgb, var(--header-bg) 96%, transparent); backdrop-filter: blur(14px); border-bottom: none; display: flex; flex-wrap: nowrap; gap: 22px; justify-content: space-between; position: sticky; top: 0; z-index: 10; }
|
|
935
|
+
html[data-theme="dark"] .site-header { background: var(--bg); backdrop-filter: none; }
|
|
936
|
+
.site-brand { align-items: center; display: inline-flex; flex: none; line-height: 0; text-decoration: none; }
|
|
937
|
+
.site-brand-logo { display: block; height: 24px; max-width: min(188px, 42vw); width: auto; }
|
|
938
|
+
.site-brand-emblem { display: none; height: 24px; width: auto; }
|
|
939
|
+
html[data-theme="dark"] .site-brand-logo { filter: brightness(0) invert(1); }
|
|
940
|
+
.site-nav { display: flex; flex: 1; flex-wrap: nowrap; gap: 2px; justify-content: center; min-width: 0; }
|
|
941
|
+
.site-nav a { border-radius: 3px; color: var(--text); font-size: 1.0625rem; font-weight: 500; padding: 8px 12px; text-decoration: none; transition: color .15s ease; }
|
|
942
|
+
.site-nav a:hover { background: transparent; color: var(--accent); }
|
|
943
|
+
.site-nav a[aria-current="page"] { color: var(--accent); }
|
|
944
|
+
.site-header-actions { align-items: center; display: flex; flex: none; gap: 8px; }
|
|
945
|
+
.site-control-btn.site-nav-toggle { display: none; padding: 0; width: 36px; }
|
|
946
|
+
.site-nav-toggle-bars { background: currentColor; border-radius: 1px; display: block; height: 2px; position: relative; width: 18px; }
|
|
947
|
+
.site-nav-toggle-bars::before, .site-nav-toggle-bars::after { background: currentColor; border-radius: 1px; content: ''; height: 2px; left: 0; position: absolute; width: 18px; }
|
|
948
|
+
.site-nav-toggle-bars::before { top: -6px; transition: transform .2s ease, top .2s ease; }
|
|
949
|
+
.site-nav-toggle-bars::after { top: 6px; transition: transform .2s ease, top .2s ease; }
|
|
950
|
+
.site-nav-toggle.is-open .site-nav-toggle-bars { background: transparent; }
|
|
951
|
+
.site-nav-toggle.is-open .site-nav-toggle-bars::before { top: 0; transform: rotate(45deg); }
|
|
952
|
+
.site-nav-toggle.is-open .site-nav-toggle-bars::after { top: 0; transform: rotate(-45deg); }
|
|
584
953
|
.site-controls { align-items: center; display: flex; gap: 8px; }
|
|
954
|
+
.site-control-btn { align-items: center; background: var(--card-muted); border: 1px solid var(--border); border-radius: 999px; color: var(--text); cursor: pointer; display: inline-flex; font: inherit; font-size: 13px; font-weight: 700; height: 36px; justify-content: center; line-height: 1; padding: 0; }
|
|
955
|
+
.site-control-btn:hover { background: var(--card); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
|
|
956
|
+
.site-control-btn.theme-toggle { width: 36px; }
|
|
957
|
+
.site-control-btn.locale-toggle { letter-spacing: .03em; min-width: 42px; padding: 0 10px; }
|
|
958
|
+
.site-github-btn { flex: none; text-decoration: none; width: 36px; }
|
|
959
|
+
.site-github-icon-svg { display: block; }
|
|
960
|
+
.site-github-icon-svg path { fill: currentColor; }
|
|
961
|
+
.theme-toggle-icons { align-items: center; display: inline-flex; height: 18px; justify-content: center; position: relative; width: 18px; }
|
|
962
|
+
.theme-toggle-icons .theme-icon { inset: 0; position: absolute; }
|
|
963
|
+
html[data-theme="light"] .theme-icon-sun { display: none; }
|
|
964
|
+
html[data-theme="dark"] .theme-icon-moon { display: none; }
|
|
585
965
|
.site-icon, .header-theme-toggle-icon { fill: currentColor; flex: none; vertical-align: -0.15em; }
|
|
586
|
-
.site-
|
|
587
|
-
|
|
588
|
-
.
|
|
589
|
-
.
|
|
966
|
+
.site-icon path, .header-theme-toggle-icon path { fill: currentColor; }
|
|
967
|
+
html { overflow-x: clip; }
|
|
968
|
+
.site-main { overflow-x: clip; padding: 0; }
|
|
969
|
+
.site-shell { --site-band-inner-max: 1280px; --site-shell-inline-pad: clamp(18px, 5vw, 72px); margin: 0 auto; max-width: 1360px; padding: clamp(34px, 6vw, 78px) var(--site-shell-inline-pad); }
|
|
970
|
+
.site-section-band { box-sizing: border-box; margin-inline: calc(50% - 50vw); max-width: 100vw; padding-block: clamp(48px, 6vw, 80px); padding-inline: max(var(--site-shell-inline-pad), calc((100vw - var(--site-band-inner-max)) / 2)); width: 100vw; }
|
|
971
|
+
.site-section-band--muted { background: var(--card-muted); }
|
|
972
|
+
.site-section-band--plain { background: var(--card); }
|
|
973
|
+
.site-section-band--surface { background: var(--bg); }
|
|
974
|
+
.site-shell > .site-section-band { margin-inline: calc(-1 * var(--site-shell-inline-pad)); max-width: none; padding-inline: var(--site-shell-inline-pad); width: auto; }
|
|
975
|
+
.site-section-band__inner { margin-inline: auto; max-width: var(--site-band-inner-max); min-width: 0; width: 100%; }
|
|
976
|
+
.hero { margin: 0 auto 48px; max-width: 980px; text-align: center; }
|
|
977
|
+
.site-shell--home .hero { margin-bottom: clamp(28px, 4vw, 40px); }
|
|
978
|
+
.site-shell--page .hero { margin-inline: 0; margin-bottom: clamp(32px, 4vw, 48px); max-width: 720px; text-align: left; }
|
|
979
|
+
.site-shell--page .hero p { margin-inline: 0; max-width: 58ch; }
|
|
980
|
+
.site-shell--page .hero .cta-row { justify-content: flex-start; }
|
|
981
|
+
.site-shell--page .page-flow { margin-inline: 0; }
|
|
982
|
+
.site-shell--page .page-flow.page-flow--narrow { margin-inline: 0; max-width: 720px; }
|
|
983
|
+
.site-shell--page .content-layout { justify-content: start; margin-inline: 0; max-width: 100%; }
|
|
984
|
+
.site-shell--page .section-heading { align-items: flex-start; }
|
|
985
|
+
.site-shell--page .site-section { text-align: left; }
|
|
986
|
+
.site-shell--page .doc-list { list-style: none; margin: 20px 0 0; padding: 0; }
|
|
987
|
+
.site-shell--page .doc-list li { margin: 0 0 22px; }
|
|
988
|
+
.site-shell--page .doc-list li:last-child { margin-bottom: 0; }
|
|
989
|
+
.site-shell--page .doc-list a { font-size: 1.0625rem; font-weight: 600; }
|
|
990
|
+
.site-shell--page .doc-list p { margin: 6px 0 0; max-width: 58ch; }
|
|
991
|
+
.home-hero-preview-wrap { margin: 0 auto var(--site-section-spacing); max-width: 1040px; width: 100%; }
|
|
992
|
+
.home-hero-preview-wrap .template-visual { margin: 0; }
|
|
993
|
+
.home-flow, .page-flow { display: flex; flex-direction: column; gap: var(--site-section-spacing); margin: 0 auto; max-width: 1200px; width: 100%; }
|
|
994
|
+
.page-flow .content-layout, .page-flow.page-flow--narrow .content-layout { margin-bottom: 0; max-width: 100%; }
|
|
995
|
+
.page-flow.page-flow--narrow { max-width: 820px; }
|
|
996
|
+
.home-flow > .graph-trinity, .home-flow > .icon-label-grid-section, .home-flow > .install-section, .home-flow > .site-section-band, .home-flow > .code-showcase, .home-flow > .comparison-strip, .home-flow > .home-faq, .home-flow > .steps-section, .page-flow > .page-lead, .page-flow > .workflow-pipeline, .page-flow > .graph-trinity, .page-flow > .icon-label-grid-section, .page-flow > .site-section-band, .page-flow > .code-showcase, .page-flow > .comparison-strip, .page-flow > .compare-boundaries, .page-flow > .content-layout, .page-flow > .site-sections-block__heading { margin-top: 0; }
|
|
997
|
+
.home-flow > .content-layout { margin-bottom: 0; max-width: 1200px; }
|
|
998
|
+
.home-flow .content-layout.home-pillars { justify-content: start; margin-inline: 0; max-width: 1200px; width: 100%; }
|
|
999
|
+
.home-flow .home-pillars .section-grid { gap: clamp(32px, 4vw, 56px) clamp(28px, 4vw, 48px); grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1000
|
+
.home-flow .home-pillars .site-section { border-bottom: 0; display: flex; flex-direction: column; padding: 0; text-align: left; }
|
|
1001
|
+
.home-flow .home-pillars .section-heading { align-items: flex-start; display: block; }
|
|
1002
|
+
.home-flow .home-pillars .section-heading h2 { font-size: clamp(1.375rem, 2.1vw, 1.75rem); font-weight: 800; letter-spacing: -.02em; line-height: 1.25; margin: 0 0 12px; }
|
|
1003
|
+
.home-flow .home-pillars .site-section p { flex: 1 1 auto; font-size: 1rem; line-height: 1.65; margin: 0; }
|
|
1004
|
+
.home-flow .home-pillars .badge-row { margin: 16px 0 0; }
|
|
1005
|
+
.site-sections-block__heading { margin-bottom: clamp(20px, 3vw, 32px); }
|
|
1006
|
+
.site-sections-block__heading p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0; max-width: 58ch; }
|
|
590
1007
|
h1 { font-size: clamp(2.25rem, 4.8vw, 4rem); letter-spacing: -.035em; line-height: 1.04; margin: 12px 0 18px; }
|
|
591
1008
|
h2 { font-size: clamp(1.35rem, 2.2vw, 1.9rem); letter-spacing: -.015em; line-height: 1.18; margin: 0 0 10px; }
|
|
592
|
-
.hero p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; max-width: 820px; }
|
|
1009
|
+
.hero p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin-left: auto; margin-right: auto; max-width: 820px; }
|
|
1010
|
+
.hero .cta-row { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 24px; }
|
|
593
1011
|
.cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
|
|
594
|
-
.template-visual { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow-raised); margin: 0 auto
|
|
1012
|
+
.template-visual { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow-raised); margin: 0 auto; max-width: 1040px; overflow: hidden; }
|
|
595
1013
|
.screenshot-hero img { display: block; height: auto; width: 100%; }
|
|
596
1014
|
.screenshot-hero figcaption { border-top: 1px solid var(--border); color: var(--muted); font-size: 13px; padding: 12px 16px; }
|
|
597
|
-
.
|
|
598
|
-
.
|
|
599
|
-
.
|
|
600
|
-
.
|
|
601
|
-
.visual-column h3 { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: .04em; margin: 0 0 10px; text-transform: uppercase; }
|
|
602
|
-
.visual-card { background: var(--card); border-left: 3px solid var(--accent); border-radius: 3px; box-shadow: var(--shadow); display: grid; gap: 7px; margin-bottom: 10px; min-height: 66px; padding: 10px; }
|
|
603
|
-
.visual-card.is-1 { border-left-color: #36b37e; }
|
|
604
|
-
.visual-card.is-2 { border-left-color: #ffab00; }
|
|
605
|
-
.visual-card.is-3 { border-left-color: #6554c0; }
|
|
606
|
-
.visual-card span, .visual-card p, .visual-card small, .related-preview span { background: var(--border); border-radius: 999px; display: block; height: 6px; }
|
|
607
|
-
.visual-card p { width: 80%; }
|
|
608
|
-
.visual-card small { width: 44%; }
|
|
609
|
-
.content-layout { align-items: start; display: grid; gap: 56px; grid-template-columns: 300px minmax(0, 820px); justify-content: center; }
|
|
610
|
-
.template-aside { position: sticky; top: 92px; }
|
|
611
|
-
.aside-card { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow); padding: 22px; }
|
|
612
|
-
.aside-icon { align-items: center; background: var(--accent-soft); border-radius: 4px; color: var(--accent); display: flex; height: 42px; justify-content: center; margin-bottom: 14px; width: 42px; }
|
|
613
|
-
.aside-card h2 { font-size: 22px; line-height: 1.18; }
|
|
614
|
-
.aside-card p { color: var(--muted); font-size: 14px; }
|
|
615
|
-
.aside-card .wg-btn { justify-content: center; width: 100%; }
|
|
616
|
-
.aside-card dl { display: grid; gap: 0; margin: 20px 0 0; }
|
|
617
|
-
.aside-card dl div { align-items: center; border-top: 1px solid var(--border); display: flex; justify-content: space-between; padding: 11px 0 0; margin-top: 11px; }
|
|
618
|
-
.aside-card dt { color: var(--muted); font-size: 12px; }
|
|
619
|
-
.aside-card dd { margin: 0; }
|
|
620
|
-
.article-column { min-width: 0; }
|
|
621
|
-
.section-grid { display: grid; gap: 36px; grid-template-columns: 1fr; margin-top: 0; }
|
|
622
|
-
.site-section { background: transparent; border: 0; border-bottom: 1px solid var(--border); border-radius: 0; box-shadow: none; padding: 0 0 34px; }
|
|
1015
|
+
.content-layout { display: grid; justify-content: center; margin-bottom: 24px; max-width: 820px; margin-left: auto; margin-right: auto; width: 100%; }
|
|
1016
|
+
.article-column { min-width: 0; width: 100%; }
|
|
1017
|
+
.section-grid { display: grid; gap: 48px; grid-template-columns: 1fr; margin-top: 0; }
|
|
1018
|
+
.site-section { background: transparent; border: 0; border-bottom: 1px solid var(--border); border-radius: 0; box-shadow: none; padding: 0 0 44px; }
|
|
623
1019
|
.section-heading { align-items: center; display: flex; gap: 10px; }
|
|
624
|
-
.section-icon { align-items: center; background: var(--accent-soft); border-radius: 4px; color: var(--accent); display: inline-flex; height: 34px; justify-content: center; width:
|
|
1020
|
+
.section-icon { align-items: center; background: var(--accent-soft); border-radius: 4px; box-sizing: border-box; color: var(--accent); display: inline-flex; flex: none; height: 34px; justify-content: center; width: 52px; }
|
|
625
1021
|
.site-section p, .doc-list p { color: var(--muted); font-size: 16px; line-height: 1.7; }
|
|
626
1022
|
.badge-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
|
|
627
1023
|
.flow-list { display: grid; gap: 8px; padding-left: 22px; }
|
|
628
|
-
table {
|
|
629
|
-
|
|
630
|
-
.
|
|
631
|
-
.
|
|
632
|
-
.
|
|
633
|
-
.
|
|
634
|
-
.
|
|
635
|
-
.
|
|
636
|
-
.
|
|
637
|
-
.
|
|
638
|
-
.
|
|
639
|
-
.
|
|
640
|
-
.
|
|
1024
|
+
.table-scroll { -webkit-overflow-scrolling: touch; margin-top: 16px; max-width: 100%; overflow-x: auto; }
|
|
1025
|
+
.table-scroll table { border-collapse: collapse; min-width: 720px; width: 100%; }
|
|
1026
|
+
.table-scroll th, .table-scroll td { border-top: 1px solid var(--border); padding: 8px 10px; text-align: left; vertical-align: top; white-space: normal; word-break: break-word; }
|
|
1027
|
+
.article-column, .site-section { max-width: 100%; min-width: 0; }
|
|
1028
|
+
.graph-trinity, .icon-label-grid-section, .install-section, .code-showcase, .feature-columns-section, .comparison-strip, .home-faq { margin: var(--site-section-spacing) auto 0; max-width: 1200px; width: 100%; }
|
|
1029
|
+
.icon-label-grid-section { background: transparent; box-sizing: border-box; padding: 0; text-align: left; }
|
|
1030
|
+
.icon-label-grid-heading { margin: 0; max-width: 720px; }
|
|
1031
|
+
.icon-label-grid-heading h2 { font-size: clamp(1.5rem, 2.8vw, 2.125rem); letter-spacing: -.02em; margin: 0 0 16px; }
|
|
1032
|
+
.icon-label-grid-heading p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0; max-width: 58ch; }
|
|
1033
|
+
.icon-label-grid { column-gap: clamp(24px, 4vw, 48px); display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: clamp(44px, 6vw, 64px); row-gap: clamp(48px, 6vw, 72px); }
|
|
1034
|
+
.icon-label-grid-item { align-items: center; display: flex; flex-direction: row; gap: 16px; justify-content: flex-start; text-align: left; }
|
|
1035
|
+
.icon-label-grid-icon { display: block; flex: none; line-height: 0; }
|
|
1036
|
+
.icon-label-grid-icon-box { align-items: center; background: var(--accent); border-radius: 6px; color: #fff; display: inline-flex; height: 40px; justify-content: center; width: 56px; }
|
|
1037
|
+
.icon-label-grid-icon-svg { color: #fff; display: block; flex: none; }
|
|
1038
|
+
.icon-label-grid-icon-svg polyline { stroke: currentColor; }
|
|
1039
|
+
.icon-label-grid-label { color: var(--text); font-size: 1.375rem; font-weight: 700; letter-spacing: -.015em; line-height: 1.35; max-width: none; }
|
|
1040
|
+
.feature-columns-section { text-align: center; }
|
|
1041
|
+
.feature-columns-heading { font-size: clamp(1.35rem, 2.2vw, 1.9rem); letter-spacing: -.015em; margin: 0 0 clamp(32px, 5vw, 48px); }
|
|
1042
|
+
.feature-columns { display: grid; gap: clamp(28px, 4vw, 56px); grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
1043
|
+
.feature-column { align-items: center; display: flex; flex-direction: column; text-align: center; }
|
|
1044
|
+
.feature-column-icon { align-items: center; color: var(--accent); display: inline-flex; height: 56px; justify-content: center; margin-bottom: 20px; width: 56px; }
|
|
1045
|
+
.feature-column-icon-svg { display: block; flex: none; }
|
|
1046
|
+
.feature-column-icon-svg path { fill: currentColor; }
|
|
1047
|
+
.feature-column h3 { font-size: 1.375rem; font-weight: 700; letter-spacing: -.02em; line-height: 1.3; margin: 0 auto 12px; max-width: 24ch; }
|
|
1048
|
+
.feature-column p { color: var(--muted); font-size: 1rem; line-height: 1.65; margin: 0 auto; max-width: 36ch; }
|
|
1049
|
+
.screenshot-gallery { background: transparent; margin: 0; max-width: 100%; min-width: 0; overflow: hidden; padding: 0; width: 100%; }
|
|
1050
|
+
.screenshot-gallery-heading, .screenshot-switcher { margin-left: auto; margin-right: auto; max-width: 100%; min-width: 0; }
|
|
1051
|
+
.screenshot-gallery-heading { text-align: center; }
|
|
1052
|
+
.screenshot-gallery-heading h2 { color: var(--text); font-size: clamp(1.85rem, 3.4vw, 2.5rem); font-weight: 800; letter-spacing: -.03em; margin: 10px 0 18px; }
|
|
1053
|
+
.screenshot-gallery-lead { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0 auto; max-width: 58ch; }
|
|
1054
|
+
.screenshot-switcher { margin-top: clamp(36px, 4vw, 52px); }
|
|
1055
|
+
.screenshot-tablist { display: flex; flex-wrap: wrap; gap: 8px 20px; justify-content: center; margin: 0 0 clamp(40px, 5vw, 56px); padding: 0; }
|
|
1056
|
+
.screenshot-tab { background: transparent; border: 0; border-radius: 999px; color: color-mix(in srgb, var(--text) 72%, transparent); cursor: pointer; font: inherit; font-size: 1.2rem; font-weight: 400; line-height: 1.2; padding: 8px 16px; transition: background .15s ease, color .15s ease; }
|
|
1057
|
+
.screenshot-tab:hover { color: var(--accent); }
|
|
1058
|
+
.screenshot-tab.is-active { background: var(--accent); color: #fff; }
|
|
1059
|
+
html[data-theme="dark"] .screenshot-tab.is-active { color: #fff; }
|
|
1060
|
+
.screenshot-panels { min-height: 320px; min-width: 0; position: relative; }
|
|
1061
|
+
.screenshot-panel { align-items: start; display: none; gap: clamp(24px, 3vw, 40px); grid-template-columns: minmax(0, 0.4fr) minmax(0, 0.6fr); min-width: 0; }
|
|
1062
|
+
.screenshot-panel.is-active { display: grid; }
|
|
1063
|
+
.screenshot-panel-copy { grid-column: 1; max-width: 28rem; padding-top: 16px; text-align: left; }
|
|
1064
|
+
.screenshot-panel-copy h3 { color: var(--text); font-size: clamp(1.625rem, 2.5vw, 2.25rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.2; margin: 0 0 20px; }
|
|
1065
|
+
.screenshot-panel-copy p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0; }
|
|
1066
|
+
.screenshot-panel-visual { grid-column: 2; min-width: 0; width: 100%; }
|
|
1067
|
+
.screenshot-panel-frame { background: var(--card); border-radius: 12px; box-shadow: 0 12px 36px rgba(9, 30, 66, .12), 0 0 1px rgba(9, 30, 66, .16); overflow: hidden; }
|
|
1068
|
+
.screenshot-panel-frame img { border: 0; border-radius: 0; box-shadow: none; display: block; height: auto; width: 100%; }
|
|
641
1069
|
.wide-heading { max-width: 900px; }
|
|
642
|
-
.
|
|
643
|
-
.
|
|
644
|
-
.
|
|
645
|
-
.
|
|
646
|
-
.
|
|
647
|
-
.
|
|
648
|
-
.
|
|
649
|
-
.
|
|
650
|
-
.
|
|
651
|
-
.
|
|
652
|
-
.
|
|
653
|
-
.
|
|
654
|
-
.
|
|
655
|
-
.
|
|
656
|
-
.
|
|
657
|
-
.
|
|
658
|
-
.
|
|
1070
|
+
.page-lead p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0; max-width: 58ch; }
|
|
1071
|
+
.workflow-pipeline-steps { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); list-style: none; margin: 24px 0 0; padding: 0; }
|
|
1072
|
+
.workflow-pipeline-steps li { background: var(--card); border: 1px solid var(--border); border-radius: 8px; display: flex; flex-direction: column; gap: 8px; padding: 20px 18px; }
|
|
1073
|
+
.workflow-pipeline-icon { align-items: flex-start; color: var(--text); display: inline-flex; flex: none; margin-bottom: 4px; }
|
|
1074
|
+
.workflow-pipeline-icon-svg { display: block; flex: none; }
|
|
1075
|
+
.workflow-pipeline-icon-svg path { fill: currentColor; }
|
|
1076
|
+
.workflow-pipeline-label { color: var(--text); font-size: 0.9375rem; font-weight: 700; line-height: 1.35; }
|
|
1077
|
+
.workflow-pipeline-detail { color: var(--muted); font-size: 0.875rem; line-height: 1.5; }
|
|
1078
|
+
.compare-boundaries { margin-top: 0; }
|
|
1079
|
+
.compare-boundaries-heading { font-size: clamp(1.35rem, 2.4vw, 1.75rem); font-weight: 800; letter-spacing: -.02em; margin: 0 0 24px; }
|
|
1080
|
+
.compare-boundaries-grid { display: grid; gap: 20px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
1081
|
+
.compare-boundaries-grid article { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 22px; }
|
|
1082
|
+
.compare-boundaries--muted .compare-boundaries-grid article { background: var(--card-muted); }
|
|
1083
|
+
.compare-boundaries-icon { color: var(--text); display: inline-flex; margin-bottom: 14px; }
|
|
1084
|
+
.compare-boundaries-grid h3 { font-size: 1.125rem; font-weight: 700; margin: 0 0 8px; }
|
|
1085
|
+
.compare-boundaries-grid p { color: var(--muted); font-size: 0.9375rem; line-height: 1.6; margin: 0; }
|
|
1086
|
+
.graph-trinity { text-align: left; }
|
|
1087
|
+
.graph-trinity-heading { margin: 0 0 clamp(24px, 3vw, 36px); max-width: 58ch; }
|
|
1088
|
+
.graph-trinity-heading h2 { font-size: clamp(1.35rem, 2.2vw, 1.9rem); letter-spacing: -.015em; line-height: 1.2; margin: 0 0 12px; }
|
|
1089
|
+
.graph-trinity-heading p { color: var(--muted); font-size: 1.0625rem; line-height: 1.65; margin: 0; }
|
|
1090
|
+
.graph-flow { align-items: stretch; display: grid; gap: clamp(10px, 1.5vw, 16px); grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); margin: 0; }
|
|
1091
|
+
.graph-flow-card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); box-sizing: border-box; display: flex; flex-direction: column; min-height: 100%; padding: clamp(20px, 2.5vw, 24px); }
|
|
1092
|
+
.graph-flow-step { align-items: center; background: var(--accent-soft); border-radius: 999px; color: var(--accent); display: inline-flex; flex: none; font-size: 0.875rem; font-weight: 800; height: 32px; justify-content: center; line-height: 1; margin: 0 0 14px; width: 32px; }
|
|
1093
|
+
.graph-flow-card-title { color: var(--text); font-size: 1.125rem; font-weight: 700; letter-spacing: -.01em; line-height: 1.3; margin: 0 0 6px; }
|
|
1094
|
+
.graph-flow-card-lead { color: var(--text); flex: none; font-size: 1rem; font-weight: 700; line-height: 1.35; margin: 0 0 10px; }
|
|
1095
|
+
.graph-flow-card-body { color: var(--muted); flex: 1 1 auto; font-size: 0.9375rem; line-height: 1.6; margin: 0; }
|
|
1096
|
+
.graph-flow-arrow { align-self: center; color: var(--accent); display: flex; flex: none; font-size: 1.5rem; font-weight: 800; justify-content: center; line-height: 1; padding: 0 2px; user-select: none; }
|
|
1097
|
+
.comparison-strip-heading { font-size: clamp(1.75rem, 3vw, 2.25rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.2; margin: 0 0 clamp(28px, 4vw, 40px); }
|
|
1098
|
+
.comparison-strip-grid { display: grid; gap: 20px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
1099
|
+
.comparison-strip-card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; box-shadow: none; display: flex; flex-direction: column; min-height: clamp(260px, 24vw, 320px); padding: 32px 28px 36px; }
|
|
1100
|
+
.comparison-strip-icon { align-items: flex-start; color: var(--text); display: inline-flex; flex: none; margin: 0 0 24px; min-height: 40px; }
|
|
1101
|
+
.comparison-strip-icon-svg { display: block; flex: none; }
|
|
1102
|
+
.comparison-strip-icon-svg path { fill: currentColor; }
|
|
1103
|
+
.comparison-strip-card-title { color: var(--text); font-size: 1.375rem; font-weight: 700; letter-spacing: -.02em; line-height: 1.3; margin: 0 0 12px; }
|
|
1104
|
+
.comparison-strip-card-lead { color: var(--muted); flex: 1 1 auto; font-size: 0.9375rem; line-height: 1.55; margin: 0; }
|
|
1105
|
+
.comparison-strip-card-gap { color: var(--text); flex: none; font-size: 0.9375rem; font-weight: 700; line-height: 1.45; margin: auto 0 0; padding-top: 16px; }
|
|
1106
|
+
.home-faq .wide-heading p, .code-showcase p { color: var(--muted); }
|
|
1107
|
+
.install-section { background: var(--card-muted); border: 1px solid var(--border); border-radius: 8px; box-sizing: border-box; display: grid; gap: 0; grid-template-columns: 1fr; margin-inline: auto; max-width: min(100%, 720px); padding: clamp(24px, 4vw, 32px); width: 100%; }
|
|
1108
|
+
.install-section h2 { font-size: clamp(1.5rem, 2.8vw, 2rem); font-weight: 800; letter-spacing: -.02em; line-height: 1.15; margin: 0 0 14px; }
|
|
1109
|
+
.install-lead { color: var(--muted); font-size: 1rem; line-height: 1.65; margin: 0 0 22px; }
|
|
1110
|
+
.install-block { margin: 0 0 20px; }
|
|
1111
|
+
.install-block-label { color: var(--text); font-size: 0.9375rem; font-weight: 600; line-height: 1.4; margin: 0 0 8px; }
|
|
1112
|
+
.install-detail { color: var(--muted); font-size: 0.9375rem; line-height: 1.65; margin: 0 0 12px; }
|
|
1113
|
+
.install-guide { margin: 4px 0 0; }
|
|
1114
|
+
.install-guide-link { font-size: 0.9375rem; font-weight: 600; text-decoration: none; }
|
|
1115
|
+
.install-guide-link:hover { text-decoration: underline; }
|
|
1116
|
+
.install-code { position: relative; }
|
|
1117
|
+
.install-code code { background: var(--code-surface); border: 1px solid var(--border); border-radius: 8px; color: var(--code-text); display: block; font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; font-size: 15px; line-height: 1.45; padding: 14px 48px 14px 14px; white-space: pre-wrap; word-break: break-word; }
|
|
1118
|
+
.install-copy-btn { align-items: center; background: transparent; border: 0; border-radius: 6px; color: var(--muted); cursor: pointer; display: inline-flex; height: 32px; justify-content: center; padding: 0; position: absolute; right: 6px; top: 6px; width: 32px; }
|
|
1119
|
+
.install-copy-btn-text { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; white-space: nowrap; width: 1px; }
|
|
1120
|
+
.install-copy-icon { display: block; flex: none; }
|
|
1121
|
+
.install-copy-icon polyline, .install-copy-icon rect { stroke: currentColor; }
|
|
1122
|
+
.install-copy-btn:hover { background: var(--accent-soft); color: var(--accent); }
|
|
1123
|
+
.install-copy-btn.is-copied { background: var(--accent-soft); color: var(--accent); }
|
|
659
1124
|
.code-showcase { align-items: start; display: grid; gap: 24px; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); }
|
|
660
1125
|
.code-tabs { display: grid; gap: 14px; }
|
|
661
|
-
.code-tabs > div { background:
|
|
662
|
-
.code-tabs strong { background:
|
|
663
|
-
pre { margin: 0; overflow-x: auto; padding:
|
|
664
|
-
code { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; font-size:
|
|
665
|
-
.
|
|
666
|
-
.
|
|
667
|
-
.
|
|
668
|
-
.
|
|
669
|
-
.
|
|
670
|
-
.
|
|
671
|
-
.
|
|
672
|
-
.
|
|
673
|
-
.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
.
|
|
677
|
-
.
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
.
|
|
681
|
-
.
|
|
682
|
-
.
|
|
683
|
-
.
|
|
684
|
-
.
|
|
685
|
-
.
|
|
686
|
-
.
|
|
687
|
-
.
|
|
688
|
-
.
|
|
689
|
-
.
|
|
690
|
-
.
|
|
691
|
-
.
|
|
692
|
-
.
|
|
693
|
-
.
|
|
694
|
-
.
|
|
695
|
-
.
|
|
696
|
-
.
|
|
697
|
-
.
|
|
698
|
-
.
|
|
1126
|
+
.code-tabs > div { background: var(--code-surface); border: 1px solid var(--border); border-radius: 4px; color: var(--code-text); overflow: hidden; }
|
|
1127
|
+
.code-tabs strong { background: var(--code-header); border-bottom: 1px solid var(--border); color: var(--code-text); display: block; font-size: 0.9375rem; padding: 12px 16px; }
|
|
1128
|
+
pre { background: transparent; color: inherit; margin: 0; overflow-x: auto; padding: 16px 18px; white-space: pre-wrap; }
|
|
1129
|
+
.code-tabs code { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; font-size: 0.875rem; line-height: 1.55; }
|
|
1130
|
+
.code-tabs .code-hl-key { color: #0d7a6f; }
|
|
1131
|
+
.code-tabs .code-hl-string { color: #7b4bb7; }
|
|
1132
|
+
.code-tabs .code-hl-keyword { color: #0052cc; }
|
|
1133
|
+
.code-tabs .code-hl-number { color: #de350b; }
|
|
1134
|
+
.code-tabs .code-hl-punct { color: #6b778c; }
|
|
1135
|
+
.code-tabs .code-hl-comment { color: #6b778c; }
|
|
1136
|
+
html[data-theme="dark"] .code-tabs .code-hl-key { color: #4ec9b0; }
|
|
1137
|
+
html[data-theme="dark"] .code-tabs .code-hl-string { color: #c792ea; }
|
|
1138
|
+
html[data-theme="dark"] .code-tabs .code-hl-keyword { color: #569cd6; }
|
|
1139
|
+
html[data-theme="dark"] .code-tabs .code-hl-number { color: #f78c6c; }
|
|
1140
|
+
html[data-theme="dark"] .code-tabs .code-hl-punct { color: #a8b3cf; }
|
|
1141
|
+
html[data-theme="dark"] .code-tabs .code-hl-comment { color: #8b9cb3; }
|
|
1142
|
+
.home-faq { margin-left: auto; margin-right: auto; max-width: 920px; padding-bottom: 12px; text-align: center; }
|
|
1143
|
+
.home-faq-title { font-size: clamp(2.35rem, 5vw, 3.25rem); font-weight: 800; letter-spacing: -.03em; margin: 0 0 40px; text-align: center; }
|
|
1144
|
+
.faq-accordion { border-top: 1px solid var(--border); text-align: left; }
|
|
1145
|
+
.faq-item { border-bottom: 1px solid var(--border); }
|
|
1146
|
+
.faq-item summary { align-items: center; cursor: pointer; display: flex; gap: 14px; justify-content: space-between; list-style: none; padding: 22px 0; }
|
|
1147
|
+
.faq-item summary::-webkit-details-marker { display: none; }
|
|
1148
|
+
.faq-question { color: var(--text); flex: 1; font-size: 1.3125rem; font-weight: 600; line-height: 1.45; min-width: 0; transition: color .15s ease; }
|
|
1149
|
+
.faq-item summary:hover .faq-question { color: var(--accent); }
|
|
1150
|
+
.faq-toggle { align-items: center; background: var(--accent); border-radius: 50%; color: #fff; display: inline-flex; flex: none; height: 32px; justify-content: center; padding: 0; width: 32px; }
|
|
1151
|
+
.faq-toggle::before { content: '+'; display: block; font-size: 22px; font-weight: 500; line-height: 1; }
|
|
1152
|
+
.faq-item[open] .faq-toggle::before { content: '−'; font-size: 24px; }
|
|
1153
|
+
.faq-answer { padding: 0 0 24px; }
|
|
1154
|
+
.faq-answer p { color: var(--muted); font-size: 1.0625rem; line-height: 1.7; margin: 0; max-width: 820px; }
|
|
1155
|
+
.home-faq .faq-json-note { color: var(--muted); font-size: 13px; margin-top: 28px; text-align: center; }
|
|
1156
|
+
.steps-section { box-sizing: border-box; margin-top: 0; max-width: 1200px; padding: clamp(8px, 2vw, 24px) 0 0; width: 100%; }
|
|
1157
|
+
.steps-section h2 { font-size: clamp(1.75rem, 3.2vw, 2.375rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.15; margin: 0 0 clamp(36px, 5vw, 56px); }
|
|
1158
|
+
.jira-steps { display: grid; gap: clamp(28px, 4vw, 40px) clamp(32px, 4vw, 56px); grid-template-columns: repeat(2, minmax(0, 1fr)); list-style: none; margin: 0; padding: 0; }
|
|
1159
|
+
.jira-steps li { align-items: start; display: grid; gap: 16px; grid-template-columns: 40px 1fr; }
|
|
1160
|
+
.step-number { align-items: center; color: var(--accent); display: inline-flex; flex: none; height: 32px; justify-content: center; margin-top: 2px; width: 32px; }
|
|
1161
|
+
.step-number-icon-svg { display: block; flex: none; }
|
|
1162
|
+
.step-number-icon-svg path { fill: currentColor; }
|
|
1163
|
+
.jira-steps h3 { color: var(--text); font-size: clamp(1.125rem, 2vw, 1.3125rem); font-weight: 400; line-height: 1.55; margin: 0; }
|
|
1164
|
+
.jira-steps h3 strong { font-weight: 700; }
|
|
1165
|
+
.bottom-cta { background: rgb(var(--ui-accent-rgb, 0 82 204)); box-sizing: border-box; color: #fff; margin: clamp(80px, 10vw, 120px) calc(50% - 50vw) 0; max-width: none; padding: clamp(64px, 8vw, 96px) max(clamp(18px, 5vw, 72px), calc((100vw - 1280px) / 2)); text-align: center; width: 100vw; }
|
|
1166
|
+
.bottom-cta__inner { margin-inline: auto; max-width: 900px; }
|
|
1167
|
+
.bottom-cta h2 { color: #fff; font-size: clamp(2rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.15; margin: 0 0 clamp(24px, 3vw, 36px); }
|
|
1168
|
+
.bottom-cta .wg-btn { background: #fff; color: rgb(var(--ui-accent-rgb, 0 82 204)); }
|
|
1169
|
+
.bottom-cta .wg-btn:hover:not(:disabled) { background: #ebecf0; color: rgb(var(--ui-accent-hover-rgb, 0 101 255)); }
|
|
1170
|
+
.site-footer { background: var(--bg); color: var(--muted); margin-top: 0; padding: clamp(48px, 6vw, 72px) clamp(18px, 5vw, 72px) clamp(32px, 4vw, 48px); }
|
|
1171
|
+
.site-footer__panel { background: var(--card-muted); border-radius: 16px; margin: 0 auto; max-width: 1200px; padding: clamp(40px, 5vw, 56px) clamp(28px, 4vw, 48px); }
|
|
1172
|
+
.site-footer__grid { align-items: start; display: grid; gap: clamp(32px, 4vw, 48px); grid-template-columns: minmax(180px, 1.2fr) repeat(3, minmax(0, 1fr)); }
|
|
1173
|
+
.site-footer__brand { display: flex; flex-direction: column; gap: 24px; }
|
|
1174
|
+
.site-footer__logo { display: inline-block; line-height: 0; text-decoration: none; }
|
|
1175
|
+
.site-footer__logo img { display: block; height: 22px; max-width: min(168px, 100%); width: auto; }
|
|
1176
|
+
html[data-theme="dark"] .site-footer__logo img { filter: brightness(0) invert(1); }
|
|
1177
|
+
.site-footer__brand-links { display: flex; flex-direction: column; gap: 10px; }
|
|
1178
|
+
.site-footer__brand-links a, .footer-column a { color: var(--footer-link-color); display: block; font-size: 1rem; font-weight: 400; line-height: 1.5; text-decoration: none; }
|
|
1179
|
+
.site-footer__brand-links a:hover, .footer-column a:hover { color: var(--accent); text-decoration: none; }
|
|
1180
|
+
.footer-columns { display: contents; }
|
|
1181
|
+
.footer-column { display: flex; flex-direction: column; }
|
|
1182
|
+
.footer-column h3 { color: var(--footer-heading-color); font-size: 0.9375rem; font-weight: 700; letter-spacing: .05em; line-height: 1.35; margin: 0 0 20px; text-transform: uppercase; }
|
|
1183
|
+
.footer-column a { margin: 0 0 10px; }
|
|
1184
|
+
.footer-column a:last-child { margin-bottom: 0; }
|
|
1185
|
+
.site-footer__bottom { align-items: center; display: flex; flex-wrap: wrap; gap: 16px 28px; justify-content: space-between; margin: 28px auto 0; max-width: 1200px; }
|
|
1186
|
+
.site-footer__copy { color: var(--muted); font-size: 1rem; line-height: 1.5; margin: 0; }
|
|
1187
|
+
.site-footer__legal { align-items: center; display: flex; flex-wrap: wrap; gap: 8px 24px; }
|
|
1188
|
+
.site-footer__legal a { color: var(--footer-link-color); font-size: 1rem; font-weight: 400; text-decoration: none; }
|
|
1189
|
+
.site-footer__legal a:hover { color: var(--accent); }
|
|
699
1190
|
${UI_BUTTON_CSS}
|
|
1191
|
+
.site-main .wg-btn, .bottom-cta .wg-btn { border-radius: 999px; font-weight: 600; }
|
|
1192
|
+
.site-main .wg-btn--lg, .bottom-cta .wg-btn--lg { font-size: 1.0625rem; padding: 12px 22px; }
|
|
1193
|
+
.site-main .wg-btn--md { font-size: 0.9375rem; padding: 10px 18px; }
|
|
1194
|
+
.site-main .wg-btn--sm { font-size: 0.875rem; padding: 8px 16px; }
|
|
1195
|
+
.site-control-btn { font-size: 0.875rem; font-weight: 600; }
|
|
700
1196
|
${UI_BADGE_CSS}
|
|
701
|
-
@media (max-width:
|
|
702
|
-
.site-header {
|
|
703
|
-
.site-nav {
|
|
704
|
-
|
|
1197
|
+
@media (min-width: 1025px) and (max-width: 1280px) {
|
|
1198
|
+
.site-header { gap: 14px; }
|
|
1199
|
+
.site-nav a { font-size: 0.9375rem; padding: 8px 8px; }
|
|
1200
|
+
}
|
|
1201
|
+
@media (max-width: 1024px) {
|
|
1202
|
+
html.is-nav-scroll-locked { overflow: hidden; }
|
|
1203
|
+
.site-header { align-items: center; backdrop-filter: none; background: var(--bg); flex-wrap: nowrap; gap: 0 12px; }
|
|
1204
|
+
html[data-theme="dark"] .site-header { background: var(--bg); }
|
|
1205
|
+
.site-brand { flex: 1; min-width: 0; }
|
|
1206
|
+
.site-brand .site-brand-logo { display: none; }
|
|
1207
|
+
.site-brand .site-brand-emblem { display: block; height: 24px; }
|
|
1208
|
+
.site-header-actions { flex: none; margin-left: auto; }
|
|
1209
|
+
.site-control-btn.site-nav-toggle { display: inline-flex; }
|
|
1210
|
+
.site-nav { display: none; flex: none; min-width: 0; }
|
|
1211
|
+
.site-header.is-nav-open { align-content: start; background: var(--bg); box-sizing: border-box; display: grid; gap: 0 12px; grid-template-areas: "brand actions" "nav nav"; grid-template-columns: 1fr auto; grid-template-rows: auto minmax(0, 1fr); height: 100dvh; inset: 0; padding: calc(16px + env(safe-area-inset-top, 0px)) clamp(18px, 5vw, 32px) max(16px, env(safe-area-inset-bottom, 0px)); position: fixed; width: 100vw; z-index: 110; }
|
|
1212
|
+
.site-header.is-nav-open .site-brand { align-self: center; grid-area: brand; }
|
|
1213
|
+
.site-header.is-nav-open .site-header-actions { align-self: center; grid-area: actions; margin-left: 0; }
|
|
1214
|
+
.site-header.is-nav-open .site-nav { align-items: stretch; background: transparent; border-top: none; box-sizing: border-box; display: flex; flex-direction: column; gap: 0; grid-area: nav; height: auto; inset: auto; justify-content: flex-start; margin: 4px 0 0; max-height: none; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding: 0; position: relative; width: 100%; -webkit-overflow-scrolling: touch; }
|
|
1215
|
+
.site-header.is-nav-open .site-nav a { font-size: 1.25rem; padding: 10px 4px; white-space: normal; }
|
|
705
1216
|
h1 { font-size: clamp(2rem, 12vw, 3.1rem); }
|
|
706
|
-
.
|
|
707
|
-
.
|
|
708
|
-
.
|
|
709
|
-
.
|
|
710
|
-
.
|
|
711
|
-
|
|
1217
|
+
.site-section-band { padding-inline: var(--site-shell-inline-pad); }
|
|
1218
|
+
.screenshot-tab { font-size: 1.125rem; padding: 7px 14px; }
|
|
1219
|
+
.screenshot-tablist { flex-wrap: nowrap; justify-content: flex-start; margin-bottom: 28px; -webkit-overflow-scrolling: touch; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; }
|
|
1220
|
+
.screenshot-panel.is-active { grid-template-columns: 1fr; }
|
|
1221
|
+
.screenshot-panel-copy, .screenshot-panel-visual { grid-column: 1; }
|
|
1222
|
+
.screenshot-panel-copy { max-width: none; }
|
|
1223
|
+
.screenshot-panel-visual { margin-top: 8px; }
|
|
1224
|
+
.jira-steps { gap: 28px; grid-template-columns: 1fr; }
|
|
1225
|
+
.home-flow .home-pillars .section-grid { gap: 40px; grid-template-columns: 1fr; }
|
|
1226
|
+
.graph-flow { gap: 16px; grid-template-columns: 1fr; }
|
|
1227
|
+
.graph-flow-arrow { justify-self: center; padding: 4px 0; transform: rotate(90deg); }
|
|
1228
|
+
.icon-label-grid, .install-section, .code-showcase, .feature-columns, .comparison-strip-grid, .workflow-pipeline-steps, .compare-boundaries-grid { grid-template-columns: 1fr; }
|
|
1229
|
+
.site-footer__grid { grid-template-columns: 1fr; }
|
|
1230
|
+
.footer-columns { display: grid; gap: 32px; grid-template-columns: 1fr; }
|
|
1231
|
+
.icon-label-grid { column-gap: 20px; grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 48px; }
|
|
1232
|
+
.icon-label-grid-label { max-width: none; }
|
|
1233
|
+
.feature-column h3, .feature-column p { max-width: none; }
|
|
712
1234
|
}
|
|
713
1235
|
</style>
|
|
714
1236
|
</head>
|
|
715
1237
|
<body>
|
|
716
1238
|
<header class="site-header">
|
|
717
|
-
|
|
718
|
-
${renderNav(locale,
|
|
719
|
-
|
|
1239
|
+
${renderSiteBrand(locale)}
|
|
1240
|
+
${renderNav(locale, page.route ?? '/')}
|
|
1241
|
+
<div class="site-header-actions">
|
|
1242
|
+
${renderHeaderGithubButton(locale)}
|
|
1243
|
+
${renderThemeLocaleControls(locale, theme)}
|
|
1244
|
+
${renderNavToggle(locale)}
|
|
1245
|
+
</div>
|
|
720
1246
|
</header>
|
|
721
1247
|
<main class="site-main">
|
|
722
|
-
<article class="site-shell">
|
|
1248
|
+
<article class="site-shell${page.kind === 'home' ? ' site-shell--home' : ' site-shell--page'}">
|
|
723
1249
|
<header class="hero">
|
|
724
|
-
<p class="eyebrow">${escapeHtml(copy.hero.eyebrow)}</p>
|
|
725
1250
|
<h1>${escapeHtml(page.title)}</h1>
|
|
726
1251
|
<p>${escapeHtml(page.description)}</p>
|
|
727
1252
|
<div class="cta-row">
|
|
@@ -729,31 +1254,15 @@ export function renderPublicSiteHtml(page, options = {}) {
|
|
|
729
1254
|
${secondaryButton}
|
|
730
1255
|
</div>
|
|
731
1256
|
</header>
|
|
732
|
-
${page.kind === '
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
<div class="section-grid">${page.sections.map((section) => renderSection(section, copy)).join('')}</div>
|
|
737
|
-
${renderSteps(copy, locale)}
|
|
738
|
-
</div>
|
|
739
|
-
</div>
|
|
740
|
-
${page.kind === 'home' ? `${renderScreenshotGallery(locale)}${renderHomeExpansion(locale)}` : ''}`}
|
|
1257
|
+
${page.kind === 'home' || page.kind === 'product' ? `<div class="home-hero-preview-wrap">${renderHeroVisual(locale, theme)}</div>` : ''}
|
|
1258
|
+
${page.kind === 'home'
|
|
1259
|
+
? `<div class="home-flow">${renderHomePageSections(locale, copy, page)}</div>`
|
|
1260
|
+
: renderPageBlocks(page, locale, copy)}
|
|
741
1261
|
</article>
|
|
742
|
-
${renderRelatedTemplates(locale, theme)}
|
|
743
1262
|
${renderBottomCta(copy, locale, theme)}
|
|
744
1263
|
</main>
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
<span>schema: ${PUBLIC_SITE_SCHEMA}</span>
|
|
748
|
-
</footer>
|
|
749
|
-
<script>
|
|
750
|
-
document.addEventListener('click', function (event) {
|
|
751
|
-
var link = event.target.closest('a[data-locale-value], a[data-theme-toggle]');
|
|
752
|
-
if (!link) return;
|
|
753
|
-
if (link.dataset.localeValue) localStorage.setItem('workGraphPublicSiteLocale', link.dataset.localeValue);
|
|
754
|
-
if (link.dataset.themeValue) localStorage.setItem('workGraphPublicSiteTheme', link.dataset.themeValue);
|
|
755
|
-
});
|
|
756
|
-
</script>
|
|
1264
|
+
${renderFooter(locale)}
|
|
1265
|
+
<script>${renderPublicSiteControlsScript()}</script>
|
|
757
1266
|
</body>
|
|
758
1267
|
</html>`;
|
|
759
1268
|
}
|
|
@@ -774,40 +1283,43 @@ export function handlePublicSiteRequest(request, response, url) {
|
|
|
774
1283
|
const method = request.method ?? 'GET';
|
|
775
1284
|
if (method !== 'GET') return false;
|
|
776
1285
|
|
|
777
|
-
const locale = normalizeLocale(
|
|
1286
|
+
const locale = normalizeLocale(
|
|
1287
|
+
url.searchParams.get('lang') ?? localeFromPathname(url.pathname),
|
|
1288
|
+
);
|
|
778
1289
|
const theme = normalizeTheme(url.searchParams.get('theme') ?? 'light');
|
|
1290
|
+
const routePathname = stripLocalePathPrefix(url.pathname);
|
|
779
1291
|
|
|
780
|
-
if (
|
|
1292
|
+
if (routePathname === '/llms.txt') {
|
|
781
1293
|
sendText(response, 200, buildLlmsTxt(), 'text/plain');
|
|
782
1294
|
return true;
|
|
783
1295
|
}
|
|
784
1296
|
|
|
785
|
-
if (
|
|
1297
|
+
if (routePathname === '/.well-known/mcp.json') {
|
|
786
1298
|
sendJson(response, 200, buildMcpDiscovery());
|
|
787
1299
|
return true;
|
|
788
1300
|
}
|
|
789
1301
|
|
|
790
|
-
if (
|
|
1302
|
+
if (routePathname === '/faq.json') {
|
|
791
1303
|
sendJson(response, 200, buildFaqJsonLd(locale));
|
|
792
1304
|
return true;
|
|
793
1305
|
}
|
|
794
1306
|
|
|
795
|
-
if (
|
|
1307
|
+
if (routePathname === '/api/docs/bvc-authoring-context') {
|
|
796
1308
|
sendJson(response, 200, buildDocsContext('bvc-authoring'));
|
|
797
1309
|
return true;
|
|
798
1310
|
}
|
|
799
1311
|
|
|
800
|
-
if (
|
|
1312
|
+
if (routePathname === '/api/docs/mcp-tools-context') {
|
|
801
1313
|
sendJson(response, 200, buildDocsContext('mcp-tools'));
|
|
802
1314
|
return true;
|
|
803
1315
|
}
|
|
804
1316
|
|
|
805
|
-
if (
|
|
1317
|
+
if (routePathname === '/api/docs/errors-context') {
|
|
806
1318
|
sendJson(response, 200, buildDocsContext('errors'));
|
|
807
1319
|
return true;
|
|
808
1320
|
}
|
|
809
1321
|
|
|
810
|
-
const markdownMatch =
|
|
1322
|
+
const markdownMatch = routePathname.match(/^\/docs\/([^/.]+)\.md$/u);
|
|
811
1323
|
if (markdownMatch) {
|
|
812
1324
|
const markdown = renderPublicDocMarkdown(markdownMatch[1], locale);
|
|
813
1325
|
if (markdown == null) return false;
|
|
@@ -815,7 +1327,7 @@ export function handlePublicSiteRequest(request, response, url) {
|
|
|
815
1327
|
return true;
|
|
816
1328
|
}
|
|
817
1329
|
|
|
818
|
-
const bvcExampleMatch =
|
|
1330
|
+
const bvcExampleMatch = routePathname.match(/^\/docs\/([^/.]+)\.bvc\.example$/u);
|
|
819
1331
|
if (bvcExampleMatch) {
|
|
820
1332
|
const example = renderBvcExample(bvcExampleMatch[1]);
|
|
821
1333
|
if (example == null) return false;
|
|
@@ -823,17 +1335,18 @@ export function handlePublicSiteRequest(request, response, url) {
|
|
|
823
1335
|
return true;
|
|
824
1336
|
}
|
|
825
1337
|
|
|
826
|
-
if (
|
|
1338
|
+
if (routePathname === '/docs.md') {
|
|
1339
|
+
const docsPrefix = locale === 'en' ? '/en' : '';
|
|
827
1340
|
const body = `# Work Graph Docs\n\n${PUBLIC_DOCS.map((doc) => {
|
|
828
1341
|
const localized = getPublicSitePage(`/docs/${doc.slug}`, locale);
|
|
829
|
-
return `- [${localized.title}](/docs/${doc.slug}.md
|
|
1342
|
+
return `- [${localized.title}](${docsPrefix}/docs/${doc.slug}.md): ${localized.description}`;
|
|
830
1343
|
}).join('\n')}\n`;
|
|
831
1344
|
sendText(response, 200, body, 'text/markdown');
|
|
832
1345
|
return true;
|
|
833
1346
|
}
|
|
834
1347
|
|
|
835
1348
|
if (url.searchParams.get('format') === 'markdown') {
|
|
836
|
-
const docSlug =
|
|
1349
|
+
const docSlug = routePathname === '/docs' ? null : routePathname.match(/^\/docs\/([^/.]+)$/u)?.[1];
|
|
837
1350
|
const markdown = docSlug
|
|
838
1351
|
? renderPublicDocMarkdown(docSlug, locale)
|
|
839
1352
|
: `# Work Graph\n\n${buildLlmsTxt()}`;
|
|
@@ -842,9 +1355,9 @@ export function handlePublicSiteRequest(request, response, url) {
|
|
|
842
1355
|
return true;
|
|
843
1356
|
}
|
|
844
1357
|
|
|
845
|
-
const page = getPublicSitePage(
|
|
1358
|
+
const page = getPublicSitePage(routePathname, locale);
|
|
846
1359
|
if (!page) return false;
|
|
847
|
-
sendText(response, 200, renderPublicSiteHtml(page, { locale, theme }), 'text/html');
|
|
1360
|
+
sendText(response, 200, renderPublicSiteHtml(page, { locale, theme, route: routePathname }), 'text/html');
|
|
848
1361
|
return true;
|
|
849
1362
|
}
|
|
850
1363
|
|