chore: Новый черновик DRAFT, удалить старые docs-v

This commit is contained in:
2026-07-30 09:19:59 +03:00
parent 6f6e4896af
commit 590fb63ca7
122 changed files with 29145 additions and 3253 deletions

View File

@@ -1,165 +1,91 @@
import { defineConfig } from 'vitepress'
import type MarkdownIt from 'markdown-it'
import { fileURLToPath } from 'node:url'
import { splitSearchSections } from './search.mts'
import { RULE_SEARCH_OPTIONS } from '../../scripts/lib/specification.mjs'
import { defineConfig } from 'vitepress'
const repositoryUrl = 'https://github.com/gromlab-ru/slm-design'
const viteConfigPath = fileURLToPath(new URL('../vite.config.mts', import.meta.url))
const specificationSidebar = [
function slugifyHeading(value: string) {
const slug = value
.replace(/<[^>]+>/g, '')
.replace(/`/g, '')
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\p{L}\p{N}_-]/gu, '')
.replace(/-+/g, '-')
return ['app', 'compositions', 'infra', 'ui', 'shared'].includes(slug)
? `layer-${slug}`
: slug
}
const levelOneSidebar = [
{
text: 'Начало',
text: 'SLM Level 1',
items: [
{ text: 'Обзор спецификации', link: '/ru/specification/' },
{ text: 'Architecture modes', link: '/ru/specification/architecture-modes' },
{ text: 'Реестр правил', link: '/ru/specification/rules' },
{ text: 'Обзор', link: '/level-1/' },
{ text: 'Терминология', link: '/level-1/terminology' },
{ text: 'Слои', link: '/level-1/layers' },
{ text: 'Зависимости', link: '/level-1/dependencies' },
{ text: 'Модули', link: '/level-1/modules' },
{ text: 'Группы', link: '/level-1/groups' },
{ text: 'Сегменты', link: '/level-1/segments' },
{ text: 'Компоненты', link: '/level-1/components' },
{ text: 'Вложенные модули', link: '/level-1/nested-modules' },
{ text: 'Жизненный цикл', link: '/level-1/lifecycle' },
{ text: 'Проверка', link: '/level-1/validation' },
],
},
{
text: 'Base SLM',
collapsed: false,
text: 'Правила',
items: [
{
text: 'Основы',
items: [
{ text: 'Основные инварианты', link: '/ru/specification/foundations' },
{ text: 'Терминология', link: '/ru/specification/terminology' },
{ text: 'Архитектурная модель', link: '/ru/specification/architecture-model' },
],
},
{
text: 'Слои',
collapsed: true,
items: [
{ text: 'Обзор', link: '/ru/specification/layers/' },
{ text: 'App', link: '/ru/specification/layers/app' },
{ text: 'Compositions', link: '/ru/specification/layers/compositions' },
{ text: 'Infra', link: '/ru/specification/layers/infra' },
{ text: 'UI', link: '/ru/specification/layers/ui' },
{ text: 'Shared', link: '/ru/specification/layers/shared' },
],
},
{
text: 'Общие правила',
collapsed: true,
items: [
{ text: 'Модули и группы', link: '/ru/specification/modules-and-groups' },
{ text: 'Сегменты', link: '/ru/specification/segments' },
{ text: 'Public API и импорты', link: '/ru/specification/public-api-and-imports' },
{ text: 'State и data', link: '/ru/specification/state-and-data' },
{ text: 'Runtime и lifecycle', link: '/ru/specification/runtime-and-lifecycle' },
{ text: 'Тестирование', link: '/ru/specification/testing-and-conformance' },
{ text: 'Монорепозитории', link: '/ru/specification/monorepo' },
],
},
],
},
{
text: 'Overlays',
collapsed: false,
items: [
{
text: 'SLM Advanced',
collapsed: true,
items: [
{ text: 'Advanced overlay', link: '/ru/specification/modes/advanced/' },
{ text: 'Domains', link: '/ru/specification/modes/advanced/domains' },
],
},
{
text: 'SLM Pro',
collapsed: true,
items: [
{ text: 'Pro overlay', link: '/ru/specification/modes/pro/' },
{ text: 'Domains', link: '/ru/specification/modes/pro/domains/' },
{ text: 'Business', link: '/ru/specification/modes/pro/domains/business' },
{ text: 'Framework surface', link: '/ru/specification/modes/pro/domains/framework' },
{ text: 'Ports и adapters', link: '/ru/specification/modes/pro/domains/ports-and-adapters' },
{ text: 'Client и server', link: '/ru/specification/modes/pro/domains/client-and-server' },
{ text: 'Cross-domain boundary', link: '/ru/specification/modes/pro/domains/cross-domain-boundary' },
{ text: 'Тестирование domains', link: '/ru/specification/modes/pro/domains/testing' },
],
},
{ text: 'Как устроены правила', link: '/rules/' },
{ text: 'Реестр Level 1', link: '/rules/level-1' },
],
},
]
function addRuleAnchors(md: MarkdownIt) {
const rulePattern = /^\*\*(SLM-(?:BASE|ADV|PRO)-[A-Z][A-Z-]*-\d{3}) - (ОБЯЗАН|ЗАПРЕЩЕНО|СЛЕДУЕТ|МОЖЕТ)\.\*\*/
const kindByKeyword: Record<string, string> = {
ОБЯЗАН: 'required',
ЗАПРЕЩЕНО: 'prohibited',
СЛЕДУЕТ: 'recommended',
МОЖЕТ: 'optional',
}
md.core.ruler.after('inline', 'slm-rule-anchors', (state) => {
for (let index = 0; index < state.tokens.length - 1; index += 1) {
const paragraph = state.tokens[index]
const content = state.tokens[index + 1]
if (paragraph.type !== 'paragraph_open' || content.type !== 'inline') continue
const match = content.content.match(rulePattern)
if (!match) continue
paragraph.attrSet('id', match[1].toLowerCase())
paragraph.attrJoin('class', 'slm-rule')
paragraph.attrJoin('class', `slm-rule--${kindByKeyword[match[2]]}`)
const strongCloseIndex = content.children?.findIndex((token) => token.type === 'strong_close') ?? -1
if (strongCloseIndex < 0 || !content.children) continue
const permalinkOpen = new state.Token('link_open', 'a', 1)
permalinkOpen.attrSet('class', 'slm-rule__permalink')
permalinkOpen.attrSet('href', `#${match[1].toLowerCase()}`)
permalinkOpen.attrSet('aria-label', `Ссылка на правило ${match[1]}`)
permalinkOpen.attrSet('title', `Ссылка на ${match[1]}`)
const permalinkText = new state.Token('text', '', 0)
permalinkText.content = '#'
const permalinkClose = new state.Token('link_close', 'a', -1)
content.children.splice(
strongCloseIndex + 1,
0,
permalinkOpen,
permalinkText,
permalinkClose,
)
}
})
}
const rulesSidebar = [
{
text: 'Правила SLM',
items: [
{ text: 'Формат и классификация', link: '/rules/' },
{ text: 'Правила Level 1', link: '/rules/level-1' },
],
},
{
text: 'Документация',
items: [
{ text: 'Обзор Level 1', link: '/level-1/' },
{ text: 'Проверка', link: '/level-1/validation' },
],
},
]
export default defineConfig({
srcDir: '../docs',
srcDir: '../DRAFT',
srcExclude: ['README.md', 'domains/**'],
rewrites: {
'level-1/README.md': 'level-1/index.md',
'rules/README.md': 'rules/index.md',
},
title: 'SLM Design',
description: 'Specification for explicit architecture boundaries in product applications',
lang: 'en-US',
description: 'Базовая архитектура фронтенд-приложений SLM Level 1',
lang: 'ru-RU',
base: '/slm-design/',
cleanUrls: true,
lastUpdated: true,
sitemap: {
hostname: 'https://gromlab-ru.github.io/slm-design/',
transformItems: (items) => items.map((item) => {
if (!item.links?.some((link) => link.url === 'ru/')) return item
return {
...item,
links: [
{ lang: 'x-default', url: '' },
...item.links.filter((link) => link.url !== ''),
],
}
}),
},
head: [
['link', { rel: 'icon', type: 'image/svg+xml', href: '/slm-design/logo.svg' }],
['meta', { name: 'theme-color', content: '#d97706' }],
],
markdown: {
config: addRuleAnchors,
anchor: {
slugify: slugifyHeading,
},
},
vite: {
configFile: viteConfigPath,
@@ -167,103 +93,68 @@ export default defineConfig({
themeConfig: {
logo: '/logo.svg',
siteTitle: 'SLM Design',
i18nRouting: false,
nav: [
{ text: 'Русская спецификация', link: '/ru/' },
{ text: 'English', link: '/en/' },
{ text: 'Level 1', link: '/level-1/' },
{ text: 'Правила', link: '/rules/level-1' },
],
sidebar: {
'/level-1/': levelOneSidebar,
'/rules/': levelOneSidebar,
},
socialLinks: [{ icon: 'github', link: repositoryUrl }],
search: {
provider: 'local',
options: {
detailedView: false,
disableQueryPersistence: true,
miniSearch: {
searchOptions: RULE_SEARCH_OPTIONS,
_splitIntoSections: splitSearchSections,
},
locales: {
ru: {
translations: {
button: {
buttonText: 'Поиск или rule ID',
buttonAriaLabel: 'Поиск по документации или rule ID',
},
modal: {
displayDetails: 'Показать подробности',
resetButtonTitle: 'Сбросить поиск',
backButtonTitle: 'Закрыть поиск',
noResultsText: 'Ничего не найдено по запросу',
footer: {
selectText: 'выбрать',
navigateText: 'перейти',
closeText: 'закрыть',
},
},
translations: {
button: {
buttonText: 'Поиск по документации',
buttonAriaLabel: 'Поиск по документации или коду правила',
},
modal: {
displayDetails: 'Показать подробности',
resetButtonTitle: 'Сбросить поиск',
backButtonTitle: 'Закрыть поиск',
noResultsText: 'Ничего не найдено',
footer: {
selectText: 'выбрать',
navigateText: 'перейти',
closeText: 'закрыть',
},
},
},
},
},
},
locales: {
ru: {
label: 'Русский',
lang: 'ru-RU',
link: '/ru/',
title: 'SLM Design',
description: 'Спецификация архитектурных границ продуктовых приложений',
themeConfig: {
nav: [
{ text: 'Документация', link: '/ru/' },
{ text: 'Спецификация', link: '/ru/specification/' },
],
sidebar: {
'/ru/specification/': specificationSidebar,
},
outline: { level: [2, 3], label: 'На этой странице' },
editLink: {
pattern: `${repositoryUrl}/edit/master/docs/:path`,
text: 'Предложить изменение',
},
lastUpdated: {
text: 'Обновлено',
formatOptions: { dateStyle: 'medium' },
},
docFooter: {
prev: 'Предыдущая страница',
next: 'Следующая страница',
},
darkModeSwitchLabel: 'Оформление',
lightModeSwitchTitle: 'Светлая тема',
darkModeSwitchTitle: 'Тёмная тема',
sidebarMenuLabel: 'Содержание',
returnToTopLabel: 'Наверх',
langMenuLabel: 'Изменить язык',
skipToContentLabel: 'Перейти к содержанию',
footer: {
message: 'SLM Design 2.0 Draft',
copyright: 'Нормативная русская версия находится в статусе draft.',
},
},
outline: { level: [2, 3], label: 'На этой странице' },
notFound: {
code: '404',
title: 'Страница не найдена',
quote: 'Запрошенная страница отсутствует в документации Level 1.',
linkLabel: 'Перейти на главную',
linkText: 'Вернуться к документации',
},
en: {
label: 'English',
lang: 'en-US',
link: '/en/',
title: 'SLM Design',
description: 'English translation placeholder for the SLM Design specification',
themeConfig: {
nav: [
{ text: 'English status', link: '/en/' },
{ text: 'Russian specification', link: '/ru/specification/' },
],
outline: { level: [2, 3], label: 'On this page' },
footer: {
message: 'SLM Design 2.0 Draft',
copyright: 'The English edition is not normative yet.',
},
},
editLink: {
pattern: `${repositoryUrl}/edit/master/DRAFT/:path`,
text: 'Предложить изменение',
},
lastUpdated: {
text: 'Обновлено',
formatOptions: { dateStyle: 'medium' },
},
docFooter: {
prev: 'Предыдущая страница',
next: 'Следующая страница',
},
darkModeSwitchLabel: 'Оформление',
lightModeSwitchTitle: 'Светлая тема',
darkModeSwitchTitle: 'Тёмная тема',
sidebarMenuLabel: 'Содержание',
returnToTopLabel: 'Наверх',
skipToContentLabel: 'Перейти к содержанию',
footer: {
message: 'SLM Level 1',
copyright: 'Рабочий черновик архитектуры.',
},
},
})

View File

@@ -1,14 +0,0 @@
import { fileURLToPath } from 'node:url'
import { defineLoader } from 'vitepress'
import { collectRules } from '../../scripts/lib/specification.mjs'
const specificationRoot = fileURLToPath(
new URL('../../docs/ru/specification/', import.meta.url),
)
export default defineLoader({
watch: '../../docs/ru/specification/**/*.md',
async load() {
return collectRules(specificationRoot)
},
})

View File

@@ -1,61 +0,0 @@
const headingPattern = /<h(\d).*?>(.*?<a.*? href="#.*?".*?>.*?<\/a>)<\/h\1>/gi
const headingContentPattern = /(.*?)<a.*? href="#(.*?)".*?>.*?<\/a>/i
const ruleBlockPattern = /<p id="(slm-(?:base|adv|pro)-[a-z][a-z0-9]*-\d{3})" class="[^"]*\bslm-rule\b[^"]*"[^>]*>([\s\S]*?)<\/p>/gi
const rulePermalinkPattern = /<a\b[^>]*class="[^"]*\bslm-rule__permalink\b[^"]*"[^>]*>[\s\S]*?<\/a>/i
function clearHtml(value: string) {
return value.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim()
}
function makeRulesSearchable(html: string) {
return html.replace(ruleBlockPattern, (_block, anchor: string, innerHtml: string) => {
const strong = innerHtml.match(/<strong>([\s\S]*?)<\/strong>/i)
const label = clearHtml(strong?.[1] || anchor.toUpperCase())
const bodyHtml = innerHtml
.replace(/<strong>[\s\S]*?<\/strong>/i, '')
.replace(rulePermalinkPattern, '')
.trim()
return `<h6>${label}<a href="#${anchor}"></a></h6><p>${bodyHtml}</p>`
})
}
function* splitByHeadings(html: string) {
const parts = html.split(headingPattern)
parts.shift()
let parentTitles: string[] = []
for (let index = 0; index < parts.length; index += 3) {
const level = Number.parseInt(parts[index], 10) - 1
const heading = headingContentPattern.exec(parts[index + 1])
const title = clearHtml(heading?.[1] || '')
const anchor = heading?.[2] || ''
const text = clearHtml(parts[index + 2] || '')
if (!title || !text) continue
let titles = parentTitles.slice(0, level)
titles[level] = title
titles = titles.filter(Boolean)
yield { anchor, titles, text }
if (level === 0) parentTitles = [title]
else parentTitles[level] = title
}
}
export function* splitSearchSections(file: string, html: string) {
const normalizedFile = file.replaceAll('\\', '/')
const documentTitle = normalizedFile.includes('/ru/specification/')
? 'Спецификация'
: normalizedFile.includes('/ru/guide/')
? 'Архитектурный гайд'
: null
for (const section of splitByHeadings(makeRulesSearchable(html))) {
yield {
...section,
titles: documentTitle ? [documentTitle, ...section.titles] : section.titles,
}
}
}

View File

@@ -5,21 +5,21 @@ import { useData, withBase } from 'vitepress'
const { page } = useData()
const documentSet = computed(() => {
if (page.value.relativePath.startsWith('ru/specification/')) {
if (page.value.relativePath.startsWith('level-1/')) {
return {
eyebrow: 'Нормативный документ',
href: '/ru/specification/',
meta: 'DRAFT · v0.1.0',
title: 'SLM Design Specification',
eyebrow: 'Архитектурная документация',
href: '/level-1/',
meta: 'LEVEL 1 · DRAFT',
title: 'SLM Level 1',
}
}
if (page.value.relativePath.startsWith('ru/guide/')) {
if (page.value.relativePath.startsWith('rules/')) {
return {
eyebrow: 'Учебный материал',
href: '/ru/guide/',
meta: 'GUIDE',
title: 'SLM Architecture Guide',
eyebrow: 'Канонический реестр',
href: '/rules/level-1',
meta: '14 RULES · DRAFT',
title: 'Правила SLM',
}
}
@@ -27,15 +27,3 @@ const documentSet = computed(() => {
})
</script>
<template>
<div v-if="documentSet" class="doc-set-header">
<a class="doc-set-header__back" :href="withBase('/ru/')">Документация</a>
<a class="doc-set-header__title" :href="withBase(documentSet.href)">
{{ documentSet.title }}
</a>
<div class="doc-set-header__meta">
<span>{{ documentSet.eyebrow }}</span>
<span>{{ documentSet.meta }}</span>
</div>
</div>
</template>

View File

@@ -1,29 +1,12 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useData } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import DocSetHeader from './DocSetHeader.vue'
const { Layout } = DefaultTheme
const { lang } = useData()
const bannerText = computed(() =>
lang.value.startsWith('ru')
? 'SLM 2.0 DRAFT / Не заменяет действующую документацию'
: 'SLM 2.0 DRAFT / Not the current stable documentation',
)
</script>
<template>
<Layout>
<template #layout-top>
<div class="draft-banner">
<span class="draft-banner__mark" aria-hidden="true" />
{{ bannerText }}
</div>
</template>
<template #sidebar-nav-before>
<DocSetHeader />
</template>
</Layout>
</template>

View File

@@ -1,271 +0,0 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { withBase } from 'vitepress'
import { data as rules } from '../rules.data.mts'
type Rule = (typeof rules)[number]
const query = ref('')
const ruleset = ref('ALL')
const area = ref('ALL')
const level = ref('ALL')
const copiedId = ref('')
const areas = [...new Set(rules.map((rule) => rule.area))].sort()
const levels = ['ОБЯЗАН', 'ЗАПРЕЩЕНО', 'СЛЕДУЕТ', 'МОЖЕТ']
function relevance(rule: Rule, normalizedQuery: string) {
const id = rule.id.toLowerCase()
if (id === normalizedQuery) return 0
if (id.startsWith(normalizedQuery)) return 1
if (id.includes(normalizedQuery)) return 2
return 3
}
const filteredRules = computed(() => {
const normalizedQuery = query.value.trim().toLowerCase()
return rules
.filter((rule) => ruleset.value === 'ALL' || rule.ruleset === ruleset.value)
.filter((rule) => area.value === 'ALL' || rule.area === area.value)
.filter((rule) => level.value === 'ALL' || rule.level === level.value)
.filter((rule) => {
if (!normalizedQuery) return true
return `${rule.id} ${rule.text} ${rule.pageTitle} ${rule.sectionTitle}`
.toLowerCase()
.includes(normalizedQuery)
})
.sort((left, right) => relevance(left, normalizedQuery) - relevance(right, normalizedQuery))
})
async function copyRuleLink(rule: Rule) {
const url = new URL(withBase(rule.href), window.location.origin).href
await navigator.clipboard.writeText(url)
copiedId.value = rule.id
window.setTimeout(() => {
if (copiedId.value === rule.id) copiedId.value = ''
}, 1600)
}
</script>
<template>
<div class="rule-catalog">
<div class="rule-catalog__controls">
<label class="rule-catalog__search">
<span>Правило или текст</span>
<input v-model="query" type="search" placeholder="SLM-BASE-FND-003" />
</label>
<label>
<span>Rule set</span>
<select v-model="ruleset">
<option value="ALL">Все</option>
<option value="BASE">Base</option>
<option value="ADV">Advanced</option>
<option value="PRO">Pro</option>
</select>
</label>
<label>
<span>Area</span>
<select v-model="area">
<option value="ALL">Все</option>
<option v-for="item in areas" :key="item" :value="item">{{ item }}</option>
</select>
</label>
<label>
<span>Уровень</span>
<select v-model="level">
<option value="ALL">Все</option>
<option v-for="item in levels" :key="item" :value="item">{{ item }}</option>
</select>
</label>
</div>
<div class="rule-catalog__summary" aria-live="polite">
Найдено: <strong>{{ filteredRules.length }}</strong> из {{ rules.length }}
</div>
<div class="rule-catalog__list">
<article v-for="rule in filteredRules" :key="rule.id" class="rule-catalog__item">
<div class="rule-catalog__item-head">
<a :href="withBase(rule.href)" class="rule-catalog__id">{{ rule.id }}</a>
<div class="rule-catalog__badges">
<span :class="`rule-catalog__badge rule-catalog__badge--${rule.ruleset.toLowerCase()}`">
{{ rule.ruleset }}
</span>
<span class="rule-catalog__badge">{{ rule.area }}</span>
<span class="rule-catalog__badge">{{ rule.level }}</span>
</div>
</div>
<p>{{ rule.text }}</p>
<div class="rule-catalog__source">
<a :href="withBase(rule.href)">{{ rule.pageTitle }} · {{ rule.sectionTitle }}</a>
<button type="button" @click="copyRuleLink(rule)">
{{ copiedId === rule.id ? 'Скопировано' : 'Копировать ссылку' }}
</button>
</div>
</article>
</div>
</div>
</template>
<style scoped>
.rule-catalog {
margin-top: 28px;
}
.rule-catalog__controls {
display: grid;
grid-template-columns: minmax(220px, 1fr) repeat(3, minmax(120px, 0.35fr));
gap: 12px;
padding: 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 10px;
background: var(--vp-c-bg-soft);
}
.rule-catalog__controls label {
display: grid;
gap: 6px;
color: var(--vp-c-text-2);
font-size: 12px;
font-weight: 650;
}
.rule-catalog__controls input,
.rule-catalog__controls select {
width: 100%;
min-height: 40px;
padding: 8px 10px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
outline: none;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font: inherit;
font-size: 14px;
}
.rule-catalog__controls input:focus,
.rule-catalog__controls select:focus {
border-color: var(--vp-c-brand-1);
box-shadow: 0 0 0 3px var(--vp-c-brand-soft);
}
.rule-catalog__summary {
margin: 14px 2px;
color: var(--vp-c-text-2);
font-size: 13px;
}
.rule-catalog__list {
display: grid;
gap: 10px;
}
.rule-catalog__item {
padding: 16px 18px;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
}
.rule-catalog__item p {
margin: 12px 0;
color: var(--vp-c-text-1);
line-height: 1.6;
}
.rule-catalog__item-head,
.rule-catalog__source,
.rule-catalog__badges {
display: flex;
align-items: center;
}
.rule-catalog__item-head,
.rule-catalog__source {
justify-content: space-between;
gap: 12px;
}
.rule-catalog__badges {
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
}
.rule-catalog__id {
color: var(--vp-c-brand-1);
font-family: var(--vp-font-family-mono);
font-size: 14px;
font-weight: 700;
}
.rule-catalog__badge {
padding: 3px 7px;
border-radius: 4px;
background: var(--vp-c-default-soft);
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
font-size: 10px;
font-weight: 650;
}
.rule-catalog__badge--base {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.rule-catalog__source {
color: var(--vp-c-text-3);
font-size: 12px;
}
.rule-catalog__source a {
color: inherit;
}
.rule-catalog__source button {
flex: 0 0 auto;
border: 0;
background: transparent;
color: var(--vp-c-brand-1);
cursor: pointer;
font: inherit;
}
@media (max-width: 820px) {
.rule-catalog__controls {
grid-template-columns: 1fr 1fr;
}
.rule-catalog__search {
grid-column: 1 / -1;
}
}
@media (max-width: 560px) {
.rule-catalog__controls {
grid-template-columns: 1fr;
}
.rule-catalog__search {
grid-column: auto;
}
.rule-catalog__item-head,
.rule-catalog__source {
align-items: flex-start;
flex-direction: column;
}
.rule-catalog__badges {
justify-content: flex-start;
}
}
</style>

View File

@@ -1,12 +1,8 @@
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'
import RuleCatalog from './RuleCatalog.vue'
import './style.css'
export default {
extends: DefaultTheme,
Layout,
enhanceApp({ app }) {
app.component('RuleCatalog', RuleCatalog)
},
}

View File

@@ -155,74 +155,60 @@ html {
font-size: clamp(2rem, 4vw, 2.65rem);
}
.vp-doc table {
display: table;
width: 100%;
.vp-doc h3[id^='slm-l1-'] {
margin-top: 34px;
scroll-margin-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height) + 24px);
color: var(--vp-c-brand-1);
font-family: var(--vp-font-family-mono);
font-size: 0.9rem;
font-weight: 750;
letter-spacing: -0.02em;
}
.vp-doc .slm-rule {
.vp-doc h3[id^='slm-l1-'] + blockquote {
position: relative;
margin: 18px 0;
padding: 15px 18px 15px 20px;
margin: 10px 0 24px;
padding: 42px 20px 18px;
scroll-margin-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height) + 24px);
border: 1px solid var(--vp-c-divider);
border-left: 3px solid var(--vp-c-brand-2);
border-radius: 0 8px 8px 0;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
line-height: 1.7;
}
.vp-doc .slm-rule strong:first-child {
.vp-doc h3[id^='slm-l1-'] + blockquote::before {
position: absolute;
top: 14px;
left: 20px;
color: var(--vp-c-text-3);
font-family: var(--vp-font-family-mono);
font-size: 9px;
font-weight: 750;
letter-spacing: 0.075em;
text-transform: uppercase;
}
.vp-doc h3[id^='slm-l1-'][id*='-a'] + blockquote::before {
content: 'Автоматическая проверка';
}
.vp-doc h3[id^='slm-l1-'][id*='-r'] + blockquote::before {
content: 'Проверка на ревью';
}
.vp-doc h3[id^='slm-l1-'] + blockquote > p {
margin: 0;
}
.vp-doc h3[id^='slm-l1-'] + blockquote > p + p {
margin-top: 8px;
}
.vp-doc h3[id^='slm-l1-'] + blockquote strong:first-child {
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
font-size: 0.89em;
letter-spacing: -0.015em;
}
.vp-doc .slm-rule__permalink {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
margin: 0 2px 0 7px;
border-radius: 4px;
color: var(--vp-c-brand-1);
font-family: var(--vp-font-family-mono);
font-size: 13px;
font-weight: 700;
opacity: 0;
transition: background-color 0.15s, opacity 0.15s;
vertical-align: -1px;
}
.vp-doc .slm-rule:hover .slm-rule__permalink,
.vp-doc .slm-rule__permalink:focus-visible {
opacity: 1;
}
.vp-doc .slm-rule__permalink:hover,
.vp-doc .slm-rule__permalink:focus-visible {
background: var(--vp-c-brand-soft);
}
.vp-doc .slm-rule--prohibited {
border-left-color: #dc2626;
background: rgba(220, 38, 38, 0.06);
}
.vp-doc .slm-rule--recommended {
border-left-color: #2563eb;
background: rgba(37, 99, 235, 0.055);
}
.vp-doc .slm-rule--optional {
border-left-color: #64748b;
}
.vp-doc .slm-rule:target {
outline: 3px solid var(--vp-c-brand-soft);
outline-offset: 3px;
}
@media (max-width: 640px) {
@@ -237,12 +223,8 @@ html {
font-size: 16px;
}
.vp-doc .slm-rule {
.vp-doc h3[id^='slm-l1-'] + blockquote {
margin-inline: -8px;
padding: 13px 14px;
}
.vp-doc .slm-rule__permalink {
opacity: 1;
padding: 42px 14px 14px;
}
}

View File

@@ -1,18 +1,15 @@
# SLM Design 2.0 Draft
# Сайт SLM Design
`site/` содержит VitePress-конфигурацию, тему и статические ресурсы сайта SLM Design.
`site/` содержит конфигурацию, тему и статические ресурсы VitePress.
Новый publishable corpus находится в `docs/`. Действующая legacy-документация и reference текущего skill находятся в `old-docs/` до отдельного решения о принятии новой спецификации.
Источником опубликованной документации служит [`DRAFT`](../DRAFT/index.md). Сайт включает только Level 1 и его правила; каталог `DRAFT/domains` исключён из маршрутов и поиска.
## Точка входа
## Маршруты
[SLM Design Specification](../docs/ru/specification/index.md)
Specification определяет base SLM и два независимых [architecture modes](../docs/ru/specification/architecture-modes.md): `SLM Advanced` и `SLM Pro`. Каждый mode является отдельным overlay непосредственно над base SLM.
## Границы текущего этапа
На этом этапе в `docs/ru/specification/` размещается только нормативная русская спецификация. Английский раздел зарезервирован под будущий перевод. Учебные материалы, руководства, примеры, справочники и agent skill будут проектироваться после стабилизации правил.
- `/` - главная страница;
- `/level-1/` - документация Level 1;
- `/rules/` - устройство правил;
- `/rules/level-1` - канонический реестр Level 1.
## Локальный запуск
@@ -20,4 +17,10 @@ Specification определяет base SLM и два независимых [ar
npm run docs:dev
```
Production build создаётся командой `npm run docs:build`.
## Проверка
```bash
npm run check:site
```
Команда проверяет реестр правил, собирает VitePress и проверяет опубликованные маршруты и якоря правил.