feat: добавить skill для SLM Design
All checks were successful
CI/CD Pipeline / build (push) Successful in 43s
CI/CD Pipeline / docker (push) Successful in 1m18s
CI/CD Pipeline / deploy (push) Successful in 6s

- добавлена сборка self-contained skill для Claude Code и opencode

- добавлен install-ready архив skill в public/slm-design/skill

- обновлена карточка SLM Design с меню действий открыть/скачать

- добавлен static fallback главной страницы из общего конфига

- подключены Mantine Menu и Phosphor Icons для действий карточки
This commit is contained in:
2026-05-22 23:23:14 +03:00
parent bdb99ade62
commit 9a962f37b5
13 changed files with 1186 additions and 164 deletions

View File

@@ -86,6 +86,50 @@
margin-top: 14px;
}
.static-actions-list,
.static-action-list {
display: grid;
gap: 8px;
margin: 14px 0 0;
padding-left: 20px;
}
.static-action-list {
margin-top: 6px;
}
.static-action-list-nested {
gap: 10px;
}
.static-action-sections {
display: grid;
gap: 18px;
margin-top: 18px;
}
.static-action-section,
.static-action-group {
display: grid;
gap: 8px;
}
.static-action-title {
color: color-mix(in srgb, LinkText 80%, CanvasText);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.static-action-group-title {
color: color-mix(in srgb, CanvasText 58%, Canvas);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.static-shell a {
color: LinkText;
}
@@ -111,6 +155,7 @@
</div>
</header>
<!-- STATIC_DOCS_START -->
<section aria-labelledby="static-docs-title">
<h2 id="static-docs-title">Список документаций</h2>
<ul class="static-docs">
@@ -118,57 +163,96 @@
<article>
<div class="static-meta">Архитектура · Доступно</div>
<h2><a href="/slm-design/">SLM Design</a></h2>
<p>
Архитектура frontend-приложений, где слои задают направление зависимостей,
модули становятся границами ответственности, а явный DI через фабрики удерживает домены изолированными и предсказуемыми.
</p>
<div class="static-links" aria-label="LLM-артефакты SLM Design">
<a href="/slm-design/llms.txt">llms.txt</a>
<a href="/slm-design/llms-full.txt">llms-full.txt</a>
</div>
<p>Архитектура frontend-приложений, где слои задают направление зависимостей, модули становятся границами ответственности, а явный DI через фабрики удерживает домены изолированными и предсказуемыми.</p>
<ul class="static-actions-list">
<li>
<span class="static-action-title">Открыть</span>
<ul class="static-action-list static-action-list-nested">
<li>
<span class="static-action-group-title">Читать</span>
<ul class="static-action-list"><li><a href="/slm-design/" target="_blank" rel="noopener noreferrer">SLM Документация</a></li></ul>
</li>
<li>
<span class="static-action-group-title">Skill для CLI-агентов</span>
<ul class="static-action-list"><li><a href="/slm-design/skill/.opencode/skills/slm-design/SKILL.md" target="_blank" rel="noopener noreferrer">slm-design/SKILL.md</a></li></ul>
</li>
<li>
<span class="static-action-group-title">AI агентам</span>
<ul class="static-action-list"><li><a href="/slm-design/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/slm-design/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
</li>
</ul>
</li>
<li>
<span class="static-action-title">Скачать</span>
<ul class="static-action-list static-action-list-nested">
<li>
<span class="static-action-group-title">Документация MD</span>
<ul class="static-action-list"><li><a href="/slm-design/slm-design.zip" download>slm-design.zip</a></li></ul>
</li>
<li>
<span class="static-action-group-title">Skills (Claude code / OpenCode)</span>
<ul class="static-action-list"><li><a href="/slm-design/skill/slm-design.skill.zip" download>slm-design.skill.zip</a></li></ul>
</li>
</ul>
</li>
</ul>
</article>
</li>
<li class="static-card">
<article>
<div class="static-meta">Стайлгайд · Доступно</div>
<h2><a href="/nextjs-style-guide/">NextJS Style Guide</a></h2>
<p>
Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.
</p>
<div class="static-links" aria-label="LLM-артефакты NextJS Style Guide">
<a href="/nextjs-style-guide/llms.txt">llms.txt</a>
<a href="/nextjs-style-guide/llms-full.txt">llms-full.txt</a>
</div>
<p>Практический стайлгайд для разработки frontend-приложений на Next.js и TypeScript.</p>
<ul class="static-actions-list">
<li>
<span class="static-action-title">AI</span>
<ul class="static-action-list"><li><a href="/nextjs-style-guide/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/nextjs-style-guide/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
</li>
</ul>
</article>
</li>
<li class="static-card">
<article>
<div class="static-meta">Стайлгайд · Скоро</div>
<h2>React Style Guide</h2>
<p>
Практический стайлгайд для разработки frontend-приложений на React и TypeScript.
</p>
<p>Практический стайлгайд для разработки frontend-приложений на React и TypeScript.</p>
<ul class="static-actions-list">
<li>
<span class="static-action-title">AI</span>
<ul class="static-action-list"><li><a href="/react-style-guide/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/react-style-guide/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
</li>
</ul>
</article>
</li>
<li class="static-card">
<article>
<div class="static-meta">Макеты · Доступно</div>
<h2><a href="/figma-adaptive-standards/">Figma Adaptive Standards</a></h2>
<p>
Стандарты и требования к подготовке адаптивных макетов в Figma: брейкпоинты,
ресайз в диапазоне, Auto Layout/Constraints, компоненты, сетка, типографика, состояния UI, A11y и передача в разработку.
</p>
<div class="static-links" aria-label="LLM-артефакты Figma Adaptive Standards">
<a href="/figma-adaptive-standards/llms.txt">llms.txt</a>
<a href="/figma-adaptive-standards/llms-full.txt">llms-full.txt</a>
</div>
<p>Стандарты и требования к подготовке адаптивных макетов в Figma: брейкпоинты, ресайз в диапазоне, Auto Layout/Constraints, компоненты, сетка, типографика, состояния UI, A11y и передача в разработку.</p>
<ul class="static-actions-list">
<li>
<span class="static-action-title">AI</span>
<ul class="static-action-list"><li><a href="/figma-adaptive-standards/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/figma-adaptive-standards/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
</li>
</ul>
</article>
</li>
<li class="static-card">
<article>
<div class="static-meta">Стратегия · Доступно</div>
<h2><a href="/template-sync-strategy/">Template Sync Strategy</a></h2>
<p>Стратегия как поддерживать проекты на общей шаблонной базе: отделять изменения шаблона от бизнес-кода и проводить обновления через контролируемый merge-процесс.</p>
<ul class="static-actions-list">
<li>
<span class="static-action-title">AI</span>
<ul class="static-action-list"><li><a href="/template-sync-strategy/llms.txt" target="_blank" rel="noopener noreferrer">llms.txt</a></li><li><a href="/template-sync-strategy/llms-full.txt" target="_blank" rel="noopener noreferrer">llms-full.txt</a></li></ul>
</li>
</ul>
</article>
</li>
</ul>
</section>
<!-- STATIC_DOCS_END -->
<footer class="static-footer">
Автор документации: <a href="https://gromlab.ru/gromov">Сергей Громов</a>

329
package-lock.json generated
View File

@@ -8,6 +8,9 @@
"name": "all-docs",
"version": "0.0.0",
"dependencies": {
"@mantine/core": "^9.2.1",
"@mantine/hooks": "^9.2.1",
"@phosphor-icons/react": "^2.1.10",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
@@ -191,7 +194,6 @@
"integrity": "sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@algolia/client-common": "5.52.1",
"@algolia/requester-browser-xhr": "5.52.1",
@@ -314,7 +316,6 @@
"version": "7.29.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -591,17 +592,6 @@
}
}
},
"node_modules/@docsearch/js/node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -1257,6 +1247,59 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.19",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz",
"integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.8",
"@floating-ui/utils": "^0.2.11",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
"integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.6"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@humanfs/core": {
"version": "0.19.2",
"dev": true,
@@ -1370,6 +1413,46 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@mantine/core": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-9.2.1.tgz",
"integrity": "sha512-CicPg9i2dM2pGp1jj+dMiR/63OFDsPjgJke4v5+0nbfJ+C7gn4C+7ltrp4RIETDMZHcj0fFuDRG0qtbiyBxvWA==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.19",
"clsx": "^2.1.1",
"react-number-format": "^5.4.5",
"react-remove-scroll": "^2.7.2",
"type-fest": "^5.6.0"
},
"peerDependencies": {
"@mantine/hooks": "9.2.1",
"react": "^19.2.0",
"react-dom": "^19.2.0"
}
},
"node_modules/@mantine/hooks": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-9.2.1.tgz",
"integrity": "sha512-IX/ztVG9eWmQTRsN7G8odyW4JckNvN8qv5A2ULzXyazjtAKLuaUpuMz0c6XhRp10J0g4bVfV3rhrTgWeImqxqg==",
"license": "MIT",
"peerDependencies": {
"react": "^19.2.0"
}
},
"node_modules/@phosphor-icons/react": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
"integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">= 16.8",
"react-dom": ">= 16.8"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -1939,24 +2022,14 @@
"version": "24.12.4",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@types/react": {
"version": "19.2.14",
"dev": true,
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2022,7 +2095,6 @@
"version": "8.59.3",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.3",
@@ -2502,7 +2574,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2541,7 +2612,6 @@
"integrity": "sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@algolia/abtesting": "1.18.1",
"@algolia/client-abtesting": "5.52.1",
@@ -2664,7 +2734,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -2784,6 +2853,15 @@
"node": ">=12"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2858,7 +2936,7 @@
},
"node_modules/csstype": {
"version": "3.2.3",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -2906,6 +2984,12 @@
"node": ">=6"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -3016,7 +3100,6 @@
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3375,7 +3458,6 @@
"integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tabbable": "^6.4.0"
}
@@ -3422,6 +3504,15 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-tsconfig": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
@@ -3806,20 +3897,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
@@ -4693,7 +4770,6 @@
"version": "4.0.4",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -4794,7 +4870,6 @@
"node_modules/react": {
"version": "19.2.6",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4809,6 +4884,16 @@
"react": "^19.2.6"
}
},
"node_modules/react-number-format": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.5.tgz",
"integrity": "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==",
"license": "MIT",
"peerDependencies": {
"react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -4819,6 +4904,75 @@
"node": ">=0.10.0"
}
},
"node_modules/react-remove-scroll": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
"integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.3",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
"react-style-singleton": "^2.2.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
"get-nonce": "^1.0.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz",
@@ -5206,9 +5360,20 @@
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
"integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
"dev": true,
"license": "MIT"
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"dev": true,
@@ -5264,6 +5429,12 @@
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
@@ -5728,13 +5899,27 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz",
"integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==",
"license": "(MIT OR CC0-1.0)",
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5923,6 +6108,49 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"license": "MIT",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
@@ -5959,7 +6187,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -6091,7 +6318,6 @@
"integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.34",
"@vue/compiler-sfc": "3.5.34",
@@ -6207,7 +6433,6 @@
"version": "4.4.3",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -4,9 +4,10 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "npm run prepare:static-index && vite",
"build": "tsx build.ts",
"app:build": "tsc -b && vite build",
"app:build": "npm run prepare:static-index && tsc -b && vite build",
"prepare:static-index": "tsx scripts/prepare-static-index.ts",
"build:slm-design": "tsx projects/slm-design/build.ts",
"build:nextjs-style-guide": "tsx projects/nextjs-style-guide/build.ts",
"build:figma-adaptive-standards": "tsx projects/figma-adaptive-standards/build.ts",
@@ -15,6 +16,9 @@
"preview": "vite preview"
},
"dependencies": {
"@mantine/core": "^9.2.1",
"@mantine/hooks": "^9.2.1",
"@phosphor-icons/react": "^2.1.10",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},

View File

@@ -18,7 +18,7 @@ function collectFiles(dir: string, baseDir = dir, archiveRoot = path.basename(di
return [
{
name: `${archiveRoot}/${relativePath}`,
name: archiveRoot ? `${archiveRoot}/${relativePath}` : relativePath,
content: fs.readFileSync(entryPath),
},
];

View File

@@ -18,3 +18,5 @@ if (config.archive) {
writeZipFromDirectory(path.join(docsDir, 'content'), zipPath, config.slug);
console.log(`Собран ${path.relative(rootDir, zipPath)}`);
}
await import('./scripts/build-skill');

View File

@@ -17,6 +17,12 @@ Scoped Layered Module Design — модульная архитектура фр
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
## AI Skill
Готовый self-contained skill для Claude Code и opencode можно скачать как архив: [slm-design.skill.zip](/slm-design/skill/slm-design.skill.zip).
Архив можно распаковать в корень другого проекта. Он добавит рабочие файлы `.claude/skills/slm-design/SKILL.md` и `.opencode/skills/slm-design/SKILL.md`.
## Преимущества
### Вертикальная организация домена

View File

@@ -0,0 +1,140 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { writeZipFromDirectory } from '../../_shared/lib/zip';
import config from '../project.config';
const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const rootDir = path.resolve(projectDir, '../..');
const templatePath = path.join(projectDir, 'skill', 'slm-design.skill.md');
const outputDir = path.join(rootDir, 'public', config.slug, 'skill');
const skillZipPath = path.join(outputDir, 'slm-design.skill.zip');
const description = 'Use this skill ONLY when working in a project that uses SLM Design (Scoped Layered Module Design) — a frontend architecture with layers app, layouts, screens, widgets, business, infra, ui, shared; modules with public API via index.ts; business-domain factories; and segments like ui/, parts/, hooks/, services/, mappers/. Apply when designing new modules, deciding where to place code, reviewing imports and dependency direction, refactoring existing SLM code, or planning monorepo placement (apps/, packages/ui, packages/infra, packages/shared). Project signals that SLM is active (strongest first): src/screens/ + src/widgets/ combination (highly specific to SLM — FSD uses pages/, Atomic and Clean don\'t have this layer); *.factory.ts files in business modules with index.ts exporting only factory and type-only exports; src/screens/ alongside src/ui/ and src/shared/; in monorepo, the same structure inside apps/{app}/src/. Note: src/business/ is a strong confirmation when present, but small or early-stage SLM projects may not have it yet — absence of business/ does NOT rule out SLM. Conflicting signals indicating other architectures: src/features/ or src/entities/ or src/pages/ (FSD); src/atoms/ or src/molecules/ (Atomic); src/domain/ or src/useCases/ (Clean). User signals (Russian or English): "SLM", "SLM Design", "Scoped Layered Module Design", "куда положить", "where to place", "какой слой", "which layer", "это модуль или компонент", "module or component", "можно ли так импортировать", "can I import", "deep import", "фабрика", "factory", "публичный API", "public API", "parts/", "business-домен", "business domain", "композиция фабрик", "factory composition". Do NOT use this skill for: projects on Feature-Sliced Design (FSD), Atomic Design, Clean Architecture, or any other frontend architecture — SLM has its own rules and is NOT a synonym for these; legacy codebases without explicit SLM structure (do not propose migration unless the user explicitly asks); small isolated tasks like styling, single-file bug fixes, CSS, or build tooling where architectural placement is not the question; backend architecture. When project architecture is ambiguous, ask the user before applying SLM rules.';
const canonPages = [
{
anchor: 'canon-architecture-index',
source: 'canons/architecture/index.md',
},
{
anchor: 'canon-architecture-layers',
source: 'canons/architecture/layers.md',
},
{
anchor: 'canon-architecture-modules',
source: 'canons/architecture/modules.md',
},
{
anchor: 'canon-architecture-segments',
source: 'canons/architecture/segments.md',
},
{
anchor: 'canon-architecture-monorepo',
source: 'canons/architecture/monorepo.md',
},
{
anchor: 'canon-examples-react-factory',
source: 'canons/examples/react/factory.md',
},
{
anchor: 'canon-examples-react-factory-composition',
source: 'canons/examples/react/factory-composition.md',
},
{
anchor: 'canon-examples-react-composition-provider',
source: 'canons/examples/react/composition-provider.md',
},
] as const;
const routeAnchors = new Map([
['/architecture', 'canon-architecture-index'],
['/architecture/', 'canon-architecture-index'],
['/architecture/layers', 'canon-architecture-layers'],
['/architecture/modules', 'canon-architecture-modules'],
['/architecture/segments', 'canon-architecture-segments'],
['/architecture/monorepo', 'canon-architecture-monorepo'],
['/examples/react/factory', 'canon-examples-react-factory'],
['/examples/react/factory-composition', 'canon-examples-react-factory-composition'],
['/examples/react/composition-provider', 'canon-examples-react-composition-provider'],
]);
function stripFrontmatter(content: string) {
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
}
function transformOutsideCode(content: string, transformLine: (line: string) => string) {
let inFence = false;
return content
.split('\n')
.map((line) => {
if (/^\s*(```|~~~)/.test(line)) {
inFence = !inFence;
return line;
}
if (inFence) return line;
return transformLine(line);
})
.join('\n');
}
function shiftHeadings(content: string) {
return transformOutsideCode(content, (line) => line.replace(/^(#{1,6})\s/, '##$1 '));
}
function rewriteMarkdownLinks(content: string) {
return transformOutsideCode(content, (line) => line.replace(/\]\((\/[^)\s#]+\/?)(#[^)\s]+)?\)/g, (match, route: string, hash = '') => {
const normalizedRoute = route.length > 1 && route.endsWith('/') ? route.slice(0, -1) : route;
const anchor = routeAnchors.get(route) ?? routeAnchors.get(normalizedRoute);
if (!anchor) return match;
return `](${hash || `#${anchor}`})`;
}));
}
function prepareCanon(source: string, anchor: string) {
const sourcePath = path.join(projectDir, source);
const content = fs.readFileSync(sourcePath, 'utf8');
const prepared = rewriteMarkdownLinks(shiftHeadings(stripFrontmatter(content).trim()));
return `<a id="${anchor}"></a>\n\n${prepared}`;
}
function createSkillBody() {
const template = fs.readFileSync(templatePath, 'utf8').trim();
const canonContent = canonPages.map((page) => prepareCanon(page.source, page.anchor)).join('\n\n---\n\n');
return template.replace('<!-- SLM_CANON_CONTENT -->', canonContent);
}
function createSkill(frontmatter: string) {
return `${frontmatter.trim()}\n\n${createSkillBody()}\n`;
}
function writeFile(filePath: string, content: string) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content, 'utf8');
}
function buildSkill() {
const frontmatter = `---\nname: slm-design\ndescription: ${description}\n---`;
const skillContent = createSkill(frontmatter);
fs.rmSync(outputDir, { recursive: true, force: true });
writeFile(path.join(outputDir, '.claude', 'skills', 'slm-design', 'SKILL.md'), skillContent);
writeFile(path.join(outputDir, '.opencode', 'skills', 'slm-design', 'SKILL.md'), skillContent);
fs.rmSync(skillZipPath, { force: true });
writeZipFromDirectory(outputDir, skillZipPath, '');
console.log(`Собран ${path.relative(rootDir, outputDir)}`);
console.log(`Собран ${path.relative(rootDir, skillZipPath)}`);
}
buildSkill();

View File

@@ -0,0 +1,98 @@
# SLM Design Skill
SLM Design — новая архитектура. Не заменяй её правилами Feature-Sliced Design, Atomic Design, Clean Architecture или другими привычными схемами. Если в проекте нет локальной документации SLM, считай этот файл единственным источником правды.
## Определение Контекста
Перед применением правил убедись, что проект использует SLM Design.
1. Проверь характерные слои SLM. Сильные сигналы: одновременное наличие `src/screens/` и `src/widgets/` (комбинация специфична именно для SLM — в FSD это `pages/`, в Atomic и Clean таких слоёв нет); файлы `*.factory.ts` в business-модулях с `index.ts`, экспортирующим только фабрику и type-only. Дополнительные сигналы: `src/ui/`, `src/shared/`, `src/layouts/` как слои. В монорепо проверяй то же внутри `apps/{app}/src/`.
ВАЖНО: отсутствие `src/business/` НЕ означает, что проект не на SLM. В маленьких или ранних проектах бизнес-доменов может ещё не быть.
2. Проверь, что business-модули (если они есть) имеют `*.factory.ts` в корне и `index.ts` экспортирует только фабрику и type-only экспорты.
3. Если видишь признаки другой архитектуры — `features/`, `entities/`, `pages/` (FSD), `atoms/`, `molecules/` (Atomic), `domain/`, `useCases/` (Clean) — это НЕ SLM. Не применяй правила этого skill.
4. Если структура неоднозначна или проект новый — спроси пользователя, какая архитектура используется, прежде чем применять правила.
5. Если пользователь работает в legacy-коде без SLM и НЕ просит миграцию — не предлагай рефакторинг по SLM. Соблюдай существующие паттерны кода.
## Порядок Работы
1. Найди границу приложения: `src/` или `apps/{app}/src`.
2. Определи ответственность изменения: запуск приложения, layout, screen, widget, business-domain, infra-service, UI-kit или shared resource.
3. Размещай код на самом низком подходящем уровне и поднимай выше только при реальной потребности переиспользования.
4. Проверяй, является сущность модулем или компонентом. Если сущность получает данные, владеет сценарием, композирует зависимости или имеет внутреннюю архитектуру — это модуль, а не компонент в `ui/`.
5. Все внешние импорты между модулями делай только через публичный API (`index.ts`). Deep imports запрещены.
6. Для `business` runtime-зависимостей между доменами используй фабрики. Не импортируй runtime-код одного business-домена напрямую в другой.
7. Перед завершением проверь слой, модульную границу, сегменты, публичный API, направление импортов и monorepo-ограничения.
## Жёсткие Правила
- Направление зависимостей внутри приложения: `app → [ layouts | screens ] → widgets → business → infra → ui → shared`.
- `layouts` и `screens` параллельны и не импортируют друг друга.
- Модули одного слоя в группе «Композиция» изолированы друг от друга.
- Runtime-импорты между `business`-доменами запрещены. Cross-domain runtime-зависимости передаются только через аргументы фабрики.
- `import type` в группе «Ядро» разрешён в обоих направлениях, потому что не создаёт runtime-зависимость.
- Каждый внешний импорт модуля идёт через `index.ts` модуля или публичный API пакета.
- `business/{name}/index.ts` экспортирует только фабрику и type-only экспорты.
- Компонент в `ui/` родительского модуля не импортирует проектный код за пределами родительского модуля, не получает данные, не вызывает сценарные хуки и не содержит бизнес-логику.
- `parts/` содержит только вложенные модули, не произвольные `.tsx`, стили или хуки.
- `shared/` не знает о продукте, бизнес-доменах, UI-kit сущностях и runtime-состоянии.
- В monorepo SLM применяется внутри каждого `apps/{app}/src`.
- В `packages/*` можно выносить только общие `ui`, `infra` и `shared`. `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты.
## Выбор Места Для Кода
- Код нужен одной странице или layout: оставь его внутри `screens/{name}/parts/` или `layouts/{name}/parts/`.
- Абстрактный UI без бизнес-логики и сценариев: `ui/{name}/`.
- Составной блок интерфейса без принадлежности конкретному домену, используемый в нескольких screens/layouts: `widgets/{name}/`.
- Код принадлежит бизнес-домену: `business/{domain}/`, даже если переиспользуется.
- Технический сервис приложения: `infra/{service}/`.
- Чистая утилита или фундаментальный ресурс без знания о продукте: `shared/`.
- В monorepo общий UI/infra/shared код, потенциально нужный двум и более frontend-приложениям: `packages/ui/*`, `packages/infra/*`, `packages/shared`.
## Шаблон Business-Модуля
```text
business/{name}/
├── {name}.factory.ts
├── hooks/
├── services/
├── mappers/
├── types/
│ ├── {name}-api.type.ts
│ ├── {name}-deps.type.ts
│ └── {name}-factory.type.ts
├── ui/
└── index.ts
```
Фабрика лежит в корне business-модуля и возвращает публичный runtime API. Если модулю нужны другие домены, принимай зависимости аргументом фабрики доменными именами.
```ts
export { customerFactory } from './customer.factory'
export type { Customer } from './types/customer.type'
export type { CustomerApi } from './types/customer-api.type'
export type { CustomerDeps } from './types/customer-deps.type'
export type { CustomerFactory } from './types/customer-factory.type'
```
## Чеклист Ревью
Проверяй архитектуру в таком порядке:
1. Правильно ли выбран слой по ответственности?
2. Не вынесен ли код выше, чем нужно для текущего использования?
3. Не лежит ли модульная сущность в `ui/` как компонент?
4. Не содержит ли компонент в `ui/` данные, сценарии, внешние импорты или вложенную архитектуру?
5. Есть ли у каждого модуля публичный API и нет ли deep imports?
6. Соблюдено ли направление зависимостей?
7. Нет ли runtime-импортов между business-доменами?
8. Экспортирует ли `business/index.ts` только фабрику и типы?
9. Не попали ли продуктовые типы, конфиги или стили в `shared/`?
10. В monorepo не вынесены ли `business`, `screens`, `layouts` или `widgets` в `packages/*`?
При ревью сначала перечисляй нарушения с файлами и причиной. Затем предлагай минимальное исправление.
## Каноническая Спецификация
Ниже находится полная спецификация SLM Design из канонов проекта. Не сокращай и не переинтерпретируй эти правила.
<!-- SLM_CANON_CONTENT -->

View File

@@ -0,0 +1,135 @@
import fs from 'node:fs'
import path from 'node:path'
import { docs, type DocAction, type DocActionCollection, type DocActionGroup } from '../src/config/docs.config'
const rootDir = path.resolve(import.meta.dirname, '..')
const indexPath = path.join(rootDir, 'index.html')
const startMarker = '<!-- STATIC_DOCS_START -->'
const endMarker = '<!-- STATIC_DOCS_END -->'
function escapeHtml(value: string) {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
}
function isActionGroup(action: DocAction | DocActionGroup): action is DocActionGroup {
return 'actions' in action
}
function splitActionCollection(collection: DocActionCollection | undefined) {
if (!collection?.length) return { actions: [], groups: [] }
if (isActionGroup(collection[0])) {
const groups = collection as DocActionGroup[]
return {
actions: [],
groups: groups.filter((group) => group.actions.length > 0),
}
}
return {
actions: collection as DocAction[],
groups: [],
}
}
function renderLink(action: DocAction, type: 'download' | 'open') {
const attrs = [
`href="${escapeHtml(action.href)}"`,
type === 'open' ? 'target="_blank"' : '',
type === 'open' ? 'rel="noopener noreferrer"' : '',
type === 'download' ? 'download' : '',
].filter(Boolean).join(' ')
return `<li><a ${attrs}>${escapeHtml(action.label)}</a></li>`
}
function renderActionList(actions: DocAction[], type: 'download' | 'open') {
return `<ul class="static-action-list">${actions.map((action) => renderLink(action, type)).join('')}</ul>`
}
function renderActionCollection(collection: DocActionCollection | undefined, title: string, type: 'download' | 'open') {
const { actions, groups } = splitActionCollection(collection)
if (actions.length === 0 && groups.length === 0) return ''
const content = groups.length > 0
? groups.map((group) => `
<li>
<span class="static-action-group-title">${escapeHtml(group.title)}</span>
${renderActionList(group.actions, type)}
</li>`).join('')
: actions.map((action) => renderLink(action, type)).join('')
return `
<li>
<span class="static-action-title">${title}</span>
<ul class="static-action-list static-action-list-nested">${content}
</ul>
</li>`
}
function renderDocLinks(title: string, links: DocAction[]) {
if (links.length === 0) return ''
return `
<li>
<span class="static-action-title">AI</span>
${renderActionList(links, 'open')}
</li>`
}
function renderDoc(doc: typeof docs[number]) {
const title = escapeHtml(doc.title)
const heading = doc.href ? `<a href="${escapeHtml(doc.href)}">${title}</a>` : title
const actionGroups = doc.actionGroups
const actions = [
renderActionCollection(actionGroups?.open, 'Открыть', 'open'),
renderActionCollection(actionGroups?.download, 'Скачать', 'download'),
doc.actionGroups ? '' : renderDocLinks(doc.title, doc.links),
].filter(Boolean).join('')
const actionBlock = actions ? `
<ul class="static-actions-list">${actions}
</ul>` : ''
return `
<li class="static-card">
<article>
<div class="static-meta">${escapeHtml(doc.label)} · ${escapeHtml(doc.status)}</div>
<h2>${heading}</h2>
<p>${escapeHtml(doc.description)}</p>${actionBlock}
</article>
</li>`
}
function renderStaticDocs() {
return `<section aria-labelledby="static-docs-title">
<h2 id="static-docs-title">Список документаций</h2>
<ul class="static-docs">${docs.map(renderDoc).join('')}
</ul>
</section>`
}
const indexHtml = fs.readFileSync(indexPath, 'utf8')
const startIndex = indexHtml.indexOf(startMarker)
const endIndex = indexHtml.indexOf(endMarker)
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
throw new Error(`Не найдены маркеры ${startMarker} / ${endMarker} в index.html`)
}
const nextIndexHtml = [
indexHtml.slice(0, startIndex + startMarker.length),
'\n ',
renderStaticDocs().replaceAll('\n', '\n '),
'\n ',
indexHtml.slice(endIndex),
].join('')
fs.writeFileSync(indexPath, nextIndexHtml, 'utf8')
console.log('Подготовлен static fallback в index.html')

View File

@@ -359,6 +359,99 @@
gap: 8px;
}
.docMenu {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: space-between;
width: 100%;
pointer-events: auto;
}
.docMenuButton {
--button-bg: var(--page-bg);
--button-bd: 1px solid var(--border-soft);
--button-color: var(--text-primary);
--button-hover: var(--page-bg);
--button-hover-color: var(--doc-accent);
--button-padding-x: 10px;
min-height: 33px;
height: 33px;
padding: 6px 10px;
border: 1px solid var(--border-soft) !important;
border-radius: 8px;
color: var(--text-primary) !important;
background: var(--page-bg) !important;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
font-weight: 500;
line-height: normal;
transition: border-color 150ms ease, color 150ms ease;
}
.docMenuButton:hover {
border-color: var(--doc-accent) !important;
color: var(--doc-accent) !important;
background: var(--page-bg) !important;
}
.docMenuItem {
text-decoration: none;
}
.docMenuFallback {
display: grid;
gap: 14px;
width: 100%;
pointer-events: auto;
}
.docMenuFallbackSection,
.docMenuFallbackGroup {
display: grid;
gap: 8px;
}
.docMenuFallbackSectionTitle,
.docMenuFallbackTitle {
color: var(--doc-accent);
font-size: 11px;
font-weight: 760;
letter-spacing: 0.08em;
line-height: 1;
text-transform: uppercase;
}
.docMenuFallbackTitle {
color: var(--text-muted);
font-size: 10px;
}
.docMenuFallbackLinks {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.docMenuFallbackLink {
padding: 6px 10px;
border: 1px solid var(--border-soft);
border-radius: 8px;
color: var(--text-primary);
background: var(--page-bg);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
font-weight: 500;
text-decoration: none;
transition: border-color 150ms ease, color 150ms ease;
}
.docMenuFallbackLink:hover {
border-color: var(--doc-accent);
color: var(--doc-accent);
}
.docLink {
padding: 6px 10px;
border: 1px solid var(--border-soft);
@@ -477,4 +570,10 @@
.docLink {
flex: 1;
}
.docMenu,
.docMenu .mantine-Menu-root,
.docMenu .mantine-Button-root {
width: 100%;
}
}

View File

@@ -1,6 +1,12 @@
import { useEffect, useLayoutEffect, useState } from 'react'
import { Fragment, useEffect, useLayoutEffect, useState, type ReactNode } from 'react'
import { Button, createTheme, MantineProvider, Menu } from '@mantine/core'
import { ArrowSquareOutIcon } from '@phosphor-icons/react/ArrowSquareOut'
import { DownloadSimpleIcon } from '@phosphor-icons/react/DownloadSimple'
import { EyeIcon } from '@phosphor-icons/react/Eye'
import { FileTextIcon } from '@phosphor-icons/react/FileText'
import { FileZipIcon } from '@phosphor-icons/react/FileZip'
import { docs } from './config/docs.config'
import { docs, type DocAction, type DocActionCollection, type DocActionGroup, type DocActionGroups } from './config/docs.config'
import './App.css'
type ThemeMode = 'auto' | 'dark' | 'light'
@@ -11,6 +17,10 @@ const THEME_STORAGE_KEY = 'vitepress-theme-appearance'
const LEGACY_THEME_STORAGE_KEY = 'all-docs-theme'
const repositoryUrl = 'https://gromlab.ru/gromov/docs'
const authorUrl = 'https://gromlab.ru/gromov'
const mantineTheme = createTheme({
fontFamily: 'var(--sans)',
primaryColor: 'indigo',
})
const themeOptions: ReadonlyArray<{
value: Exclude<ThemeMode, 'auto'>
@@ -149,8 +159,11 @@ function useTheme() {
return { theme, resolvedTheme, setTheme }
}
function ThemeToggle() {
const { theme, resolvedTheme, setTheme } = useTheme()
function ThemeToggle({ theme, resolvedTheme, setTheme }: {
theme: ThemeMode
resolvedTheme: ResolvedTheme
setTheme: (theme: ThemeMode) => void
}) {
const toggleTheme = (value: Exclude<ThemeMode, 'auto'>) => {
setTheme(theme === value ? 'auto' : value)
}
@@ -183,6 +196,157 @@ function GithubIcon() {
)
}
function getActionIcon(type: 'download' | 'open'): ReactNode {
if (type === 'download') return <FileZipIcon size={16} />
return <FileTextIcon size={16} />
}
function isActionGroup(action: DocAction | DocActionGroup): action is DocActionGroup {
return 'actions' in action
}
function splitActionCollection(collection: DocActionCollection | undefined) {
if (!collection?.length) return { actions: [], groups: [] }
if (isActionGroup(collection[0])) {
const groups = collection as DocActionGroup[]
return {
actions: [],
groups: groups.filter((group) => group.actions.length > 0),
}
}
return {
actions: collection as DocAction[],
groups: [],
}
}
function DocActionsMenuButton({ actions = [], groups = [], label, type }: {
actions?: DocAction[]
groups?: DocActionGroup[]
label: string
type: 'download' | 'open'
}) {
const [opened, setOpened] = useState(false)
const buttonIcon = type === 'download' ? <DownloadSimpleIcon size={16} /> : <EyeIcon size={16} />
const renderAction = (action: DocAction, keyPrefix: string) => (
<Menu.Item
component="a"
href={action.href}
download={type === 'download' ? '' : undefined}
target={type === 'open' ? '_blank' : undefined}
rel={type === 'open' ? 'noopener noreferrer' : undefined}
leftSection={getActionIcon(type)}
rightSection={type === 'open' && action.href.endsWith('.md') ? <ArrowSquareOutIcon size={16} /> : undefined}
key={`${keyPrefix}-${action.href}`}
>
{action.label}
</Menu.Item>
)
return (
<Menu
opened={opened}
onChange={setOpened}
position="bottom-start"
shadow="md"
width={260}
withinPortal
classNames={{ item: 'docMenuItem' }}
>
<Menu.Target>
<Button className={`docMenuButton docMenuButton-${type}`} leftSection={buttonIcon} size="xs" variant="outline">
{label}
</Button>
</Menu.Target>
<Menu.Dropdown>
{groups.length > 0
? groups.map((group, groupIndex) => (
<Fragment key={group.title}>
{groupIndex > 0 && <Menu.Divider />}
<Menu.Label>{group.title}</Menu.Label>
{group.actions.map((action) => renderAction(action, group.title))}
</Fragment>
))
: actions.map((action) => renderAction(action, label))}
</Menu.Dropdown>
</Menu>
)
}
function DocActionsFallback({ groups }: { groups: DocActionGroups }) {
const open = splitActionCollection(groups.open)
const download = splitActionCollection(groups.download)
const renderActions = (actions: DocAction[], type: 'download' | 'open') => actions.map((action) => (
<a
className="docMenuFallbackLink"
href={action.href}
target={type === 'open' ? '_blank' : undefined}
rel={type === 'open' ? 'noopener noreferrer' : undefined}
download={type === 'download' ? '' : undefined}
key={`${type}-${action.href}`}
>
{action.label}
</a>
))
const renderGroups = (groups: DocActionGroup[], type: 'download' | 'open') => groups.map((group) => (
<div className="docMenuFallbackGroup" key={`${type}-${group.title}`}>
<div className="docMenuFallbackTitle">{group.title}</div>
<div className="docMenuFallbackLinks">{renderActions(group.actions, type)}</div>
</div>
))
return (
<div className="docMenuFallback" aria-label="Действия">
{(open.groups.length > 0 || open.actions.length > 0) && (
<div className="docMenuFallbackSection">
<div className="docMenuFallbackSectionTitle">Открыть</div>
{open.groups.length > 0
? renderGroups(open.groups, 'open')
: <div className="docMenuFallbackLinks">{renderActions(open.actions, 'open')}</div>}
</div>
)}
{(download.groups.length > 0 || download.actions.length > 0) && (
<div className="docMenuFallbackSection">
<div className="docMenuFallbackSectionTitle">Скачать</div>
{download.groups.length > 0
? renderGroups(download.groups, 'download')
: <div className="docMenuFallbackLinks">{renderActions(download.actions, 'download')}</div>}
</div>
)}
</div>
)
}
function DocActionsMenu({ groups }: { groups: DocActionGroups }) {
const [isHydrated, setIsHydrated] = useState(false)
const open = splitActionCollection(groups.open)
const download = splitActionCollection(groups.download)
useEffect(() => {
const timeoutId = window.setTimeout(() => setIsHydrated(true), 0)
return () => window.clearTimeout(timeoutId)
}, [])
if (!isHydrated) return <DocActionsFallback groups={groups} />
return (
<div className="docMenu">
{(open.groups.length > 0 || open.actions.length > 0) && (
<DocActionsMenuButton actions={open.actions} groups={open.groups} label="Открыть" type="open" />
)}
{(download.groups.length > 0 || download.actions.length > 0) && (
<DocActionsMenuButton actions={download.actions} groups={download.groups} label="Скачать" type="download" />
)}
</div>
)
}
function DocIcon({ mark }: { mark: string }) {
if (mark === 'SLM') {
return (
@@ -237,7 +401,10 @@ function DocIcon({ mark }: { mark: string }) {
}
function App() {
const theme = useTheme()
return (
<MantineProvider theme={mantineTheme} forceColorScheme={theme.resolvedTheme}>
<main className="page">
<section className="hero" aria-labelledby="page-title">
<h1 className="title" id="page-title">Документация</h1>
@@ -250,7 +417,7 @@ function App() {
<GithubIcon />
<span>Репозиторий</span>
</a>
<ThemeToggle />
<ThemeToggle {...theme} />
</div>
</section>
@@ -262,13 +429,14 @@ function App() {
{docs.map((doc) => {
const isAvailable = Boolean(doc.href)
const hasActionGroups = Boolean(doc.actionGroups?.open?.length || doc.actionGroups?.download?.length)
return (
<article
className="docItem"
data-accent={doc.accent}
data-state={isAvailable ? 'available' : 'planned'}
key={doc.href ?? doc.title}
key={doc.title}
>
{isAvailable && (
<a className="docCardLink" href={doc.href} aria-label={`Открыть ${doc.title}`} />
@@ -288,7 +456,9 @@ function App() {
</div>
<div className="docActions">
{isAvailable ? (
{hasActionGroups ? (
<DocActionsMenu groups={doc.actionGroups ?? {}} />
) : isAvailable ? (
<a className="docStatus docStatusLink" href={doc.href}>
Открыть -&gt;
</a>
@@ -317,6 +487,7 @@ function App() {
Автор документации: <a href={authorUrl}>Сергей Громов</a>
</footer>
</main>
</MantineProvider>
)
}

View File

@@ -3,6 +3,23 @@ export type DocLink = {
href: string
}
export type DocAction = {
label: string
href: string
}
export type DocActionGroup = {
title: string
actions: DocAction[]
}
export type DocActionCollection = DocAction[] | DocActionGroup[]
export type DocActionGroups = {
open?: DocActionCollection
download?: DocActionCollection
}
export type DocCard = {
title: string
label: string
@@ -12,6 +29,7 @@ export type DocCard = {
status: string
accent: string
links: DocLink[]
actionGroups?: DocActionGroups
}
export const docs: DocCard[] = [
@@ -23,11 +41,45 @@ export const docs: DocCard[] = [
href: '/slm-design/',
status: 'Доступно',
accent: 'violet',
links: [
links: [],
actionGroups: {
open: [
{
title: 'Читать',
actions: [
{ label: 'SLM Документация', href: '/slm-design/' },
],
},
{
title: 'Skill для CLI-агентов',
actions: [
{ label: 'slm-design/SKILL.md', href: '/slm-design/skill/.opencode/skills/slm-design/SKILL.md' },
],
},
{
title: 'AI агентам',
actions: [
{ label: 'llms.txt', href: '/slm-design/llms.txt' },
{ label: 'llms-full.txt', href: '/slm-design/llms-full.txt' },
],
},
],
download: [
{
title: 'Документация MD',
actions: [
{ label: 'slm-design.zip', href: '/slm-design/slm-design.zip' },
],
},
{
title: 'Skills (Claude code / OpenCode)',
actions: [
{ label: 'slm-design.skill.zip', href: '/slm-design/skill/slm-design.skill.zip' },
],
},
],
},
},
{
title: 'NextJS Style Guide',
label: 'Стайлгайд',

View File

@@ -1,5 +1,11 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import '@mantine/core/styles/baseline.css'
import '@mantine/core/styles/default-css-variables.css'
import '@mantine/core/styles/global.css'
import '@mantine/core/styles/Button.css'
import '@mantine/core/styles/Menu.css'
import '@mantine/core/styles/Popover.css'
import './index.css'
import App from './App.tsx'