mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
feat: Добавить VitePress
This commit is contained in:
269
site/.vitepress/config.mts
Normal file
269
site/.vitepress/config.mts
Normal file
@@ -0,0 +1,269 @@
|
||||
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'
|
||||
|
||||
const repositoryUrl = 'https://github.com/gromlab-ru/slm-design'
|
||||
const viteConfigPath = fileURLToPath(new URL('../vite.config.mts', import.meta.url))
|
||||
|
||||
const specificationSidebar = [
|
||||
{
|
||||
text: 'Начало',
|
||||
items: [
|
||||
{ text: 'Обзор спецификации', link: '/ru/specification/' },
|
||||
{ text: 'Architecture modes', link: '/ru/specification/architecture-modes' },
|
||||
{ text: 'Реестр правил', link: '/ru/specification/rules' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Base SLM',
|
||||
collapsed: false,
|
||||
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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: '../docs',
|
||||
title: 'SLM Design',
|
||||
description: 'Specification for explicit architecture boundaries in product applications',
|
||||
lang: 'en-US',
|
||||
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,
|
||||
},
|
||||
vite: {
|
||||
configFile: viteConfigPath,
|
||||
},
|
||||
themeConfig: {
|
||||
logo: '/logo.svg',
|
||||
siteTitle: 'SLM Design',
|
||||
i18nRouting: false,
|
||||
nav: [
|
||||
{ text: 'Русская спецификация', link: '/ru/' },
|
||||
{ text: 'English', link: '/en/' },
|
||||
],
|
||||
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: 'закрыть',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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.',
|
||||
},
|
||||
},
|
||||
},
|
||||
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.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
14
site/.vitepress/rules.data.mts
Normal file
14
site/.vitepress/rules.data.mts
Normal file
@@ -0,0 +1,14 @@
|
||||
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)
|
||||
},
|
||||
})
|
||||
61
site/.vitepress/search.mts
Normal file
61
site/.vitepress/search.mts
Normal file
@@ -0,0 +1,61 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
41
site/.vitepress/theme/DocSetHeader.vue
Normal file
41
site/.vitepress/theme/DocSetHeader.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useData, withBase } from 'vitepress'
|
||||
|
||||
const { page } = useData()
|
||||
|
||||
const documentSet = computed(() => {
|
||||
if (page.value.relativePath.startsWith('ru/specification/')) {
|
||||
return {
|
||||
eyebrow: 'Нормативный документ',
|
||||
href: '/ru/specification/',
|
||||
meta: 'DRAFT · v0.1.0',
|
||||
title: 'SLM Design Specification',
|
||||
}
|
||||
}
|
||||
|
||||
if (page.value.relativePath.startsWith('ru/guide/')) {
|
||||
return {
|
||||
eyebrow: 'Учебный материал',
|
||||
href: '/ru/guide/',
|
||||
meta: 'GUIDE',
|
||||
title: 'SLM Architecture Guide',
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
</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>
|
||||
29
site/.vitepress/theme/Layout.vue
Normal file
29
site/.vitepress/theme/Layout.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<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>
|
||||
271
site/.vitepress/theme/RuleCatalog.vue
Normal file
271
site/.vitepress/theme/RuleCatalog.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<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>
|
||||
12
site/.vitepress/theme/index.ts
Normal file
12
site/.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
248
site/.vitepress/theme/style.css
Normal file
248
site/.vitepress/theme/style.css
Normal file
@@ -0,0 +1,248 @@
|
||||
:root {
|
||||
--vp-layout-top-height: 34px;
|
||||
--vp-c-brand-1: #b45309;
|
||||
--vp-c-brand-2: #d97706;
|
||||
--vp-c-brand-3: #f59e0b;
|
||||
--vp-c-brand-soft: rgba(217, 119, 6, 0.14);
|
||||
--vp-c-bg: #f8f7f4;
|
||||
--vp-c-bg-alt: #efede8;
|
||||
--vp-c-bg-elv: #ffffff;
|
||||
--vp-c-bg-soft: #f1efe9;
|
||||
--vp-font-family-base: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--vp-font-family-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: linear-gradient(120deg, #92400e 5%, #d97706 50%, #f59e0b 95%);
|
||||
--vp-home-hero-image-background-image: radial-gradient(circle, rgba(245, 158, 11, 0.32), transparent 68%);
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-brand-1: #fbbf24;
|
||||
--vp-c-brand-2: #f59e0b;
|
||||
--vp-c-brand-3: #d97706;
|
||||
--vp-c-brand-soft: rgba(245, 158, 11, 0.16);
|
||||
--vp-c-bg: #111315;
|
||||
--vp-c-bg-alt: #191c1f;
|
||||
--vp-c-bg-elv: #202428;
|
||||
--vp-c-bg-soft: #1c2023;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-padding-top: calc(var(--vp-nav-height) + var(--vp-layout-top-height) + 24px);
|
||||
}
|
||||
|
||||
.draft-banner {
|
||||
position: fixed;
|
||||
inset: 0 0 auto;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
height: var(--vp-layout-top-height);
|
||||
padding: 0 16px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid rgba(120, 53, 15, 0.2);
|
||||
background: #fef3c7;
|
||||
color: #78350f;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.055em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dark .draft-banner {
|
||||
border-bottom-color: rgba(251, 191, 36, 0.22);
|
||||
background: #2b2114;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
.draft-banner__mark {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: #d97706;
|
||||
box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.13);
|
||||
}
|
||||
|
||||
.VPNavBarTitle .logo {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.doc-set-header {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin: 0 0 22px;
|
||||
padding: 0 0 18px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.doc-set-header__back {
|
||||
width: fit-content;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.doc-set-header__back::before {
|
||||
content: '←';
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.doc-set-header__title {
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.doc-set-header__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.doc-set-header__meta span {
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
font-family: var(--vp-font-family-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.045em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.VPHome {
|
||||
background-image:
|
||||
linear-gradient(rgba(120, 113, 108, 0.07) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(120, 113, 108, 0.07) 1px, transparent 1px);
|
||||
background-position: center top;
|
||||
background-size: 40px 40px;
|
||||
}
|
||||
|
||||
.VPHero .name,
|
||||
.VPHero .text {
|
||||
letter-spacing: -0.045em;
|
||||
}
|
||||
|
||||
.VPHero .tagline {
|
||||
max-width: 660px;
|
||||
font-size: 19px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.VPFeature {
|
||||
border-color: rgba(120, 113, 108, 0.2) !important;
|
||||
background: color-mix(in srgb, var(--vp-c-bg-soft) 84%, transparent) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.vp-doc h1,
|
||||
.vp-doc h2,
|
||||
.vp-doc h3 {
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.vp-doc h1 {
|
||||
font-size: clamp(2rem, 4vw, 2.65rem);
|
||||
}
|
||||
|
||||
.vp-doc table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vp-doc .slm-rule {
|
||||
position: relative;
|
||||
margin: 18px 0;
|
||||
padding: 15px 18px 15px 20px;
|
||||
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);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.vp-doc .slm-rule 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) {
|
||||
.draft-banner {
|
||||
justify-content: flex-start;
|
||||
padding-inline: 12px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.VPHero .tagline {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.vp-doc .slm-rule {
|
||||
margin-inline: -8px;
|
||||
padding: 13px 14px;
|
||||
}
|
||||
|
||||
.vp-doc .slm-rule__permalink {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
23
site/README.md
Normal file
23
site/README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# SLM Design 2.0 Draft
|
||||
|
||||
`site/` содержит VitePress-конфигурацию, тему и статические ресурсы сайта SLM Design.
|
||||
|
||||
Новый publishable corpus находится в `docs/`. Действующая legacy-документация и reference текущего skill находятся в `old-docs/` до отдельного решения о принятии новой спецификации.
|
||||
|
||||
## Точка входа
|
||||
|
||||
[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 будут проектироваться после стабилизации правил.
|
||||
|
||||
## Локальный запуск
|
||||
|
||||
```bash
|
||||
npm run docs:dev
|
||||
```
|
||||
|
||||
Production build создаётся командой `npm run docs:build`.
|
||||
4
site/public/logo.svg
Normal file
4
site/public/logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" role="img" aria-label="SLM Design">
|
||||
<rect width="40" height="40" rx="10" fill="#111315"/>
|
||||
<path d="M10 11h20v5H16v3h11v5H16v5h14" fill="none" stroke="#f59e0b" stroke-width="4" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 273 B |
5
site/vite.config.mts
Normal file
5
site/vite.config.mts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export default {
|
||||
publicDir: fileURLToPath(new URL('./public', import.meta.url)),
|
||||
}
|
||||
Reference in New Issue
Block a user