feat: добавить skill для SLM Design
- добавлена сборка 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:
99
src/App.css
99
src/App.css
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
323
src/App.tsx
323
src/App.tsx
@@ -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,86 +401,93 @@ function DocIcon({ mark }: { mark: string }) {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<main className="page">
|
||||
<section className="hero" aria-labelledby="page-title">
|
||||
<h1 className="title" id="page-title">Документация</h1>
|
||||
<p className="lead">
|
||||
Единое пространство для идей, черновиков и первых версий документаций,
|
||||
которые ещё формируются и постепенно становятся самостоятельными материалами.
|
||||
</p>
|
||||
<div className="controls">
|
||||
<a className="repoLink" href={repositoryUrl} target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon />
|
||||
<span>Репозиторий</span>
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</section>
|
||||
<MantineProvider theme={mantineTheme} forceColorScheme={theme.resolvedTheme}>
|
||||
<main className="page">
|
||||
<section className="hero" aria-labelledby="page-title">
|
||||
<h1 className="title" id="page-title">Документация</h1>
|
||||
<p className="lead">
|
||||
Единое пространство для идей, черновиков и первых версий документаций,
|
||||
которые ещё формируются и постепенно становятся самостоятельными материалами.
|
||||
</p>
|
||||
<div className="controls">
|
||||
<a className="repoLink" href={repositoryUrl} target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon />
|
||||
<span>Репозиторий</span>
|
||||
</a>
|
||||
<ThemeToggle {...theme} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="docsPanel" aria-label="Быстрые переходы">
|
||||
<div className="docsPanelHeader">
|
||||
<span>Документация</span>
|
||||
<span>{docs.length} направления</span>
|
||||
</div>
|
||||
<section className="docsPanel" aria-label="Быстрые переходы">
|
||||
<div className="docsPanelHeader">
|
||||
<span>Документация</span>
|
||||
<span>{docs.length} направления</span>
|
||||
</div>
|
||||
|
||||
{docs.map((doc) => {
|
||||
const isAvailable = Boolean(doc.href)
|
||||
{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}
|
||||
>
|
||||
{isAvailable && (
|
||||
<a className="docCardLink" href={doc.href} aria-label={`Открыть ${doc.title}`} />
|
||||
)}
|
||||
return (
|
||||
<article
|
||||
className="docItem"
|
||||
data-accent={doc.accent}
|
||||
data-state={isAvailable ? 'available' : 'planned'}
|
||||
key={doc.title}
|
||||
>
|
||||
{isAvailable && (
|
||||
<a className="docCardLink" href={doc.href} aria-label={`Открыть ${doc.title}`} />
|
||||
)}
|
||||
|
||||
<div className="docMain">
|
||||
<span className="docMark" aria-hidden="true">
|
||||
<DocIcon mark={doc.mark} />
|
||||
</span>
|
||||
<div>
|
||||
<div className="docMeta">
|
||||
{doc.label}
|
||||
</div>
|
||||
<h2>{doc.title}</h2>
|
||||
<p>{doc.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="docActions">
|
||||
{isAvailable ? (
|
||||
<a className="docStatus docStatusLink" href={doc.href}>
|
||||
Открыть ->
|
||||
</a>
|
||||
) : (
|
||||
<span className="docStatus" aria-disabled="true">
|
||||
{doc.status}
|
||||
<div className="docMain">
|
||||
<span className="docMark" aria-hidden="true">
|
||||
<DocIcon mark={doc.mark} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{doc.links.length > 0 && (
|
||||
<div className="docLinks" aria-label="LLM-артефакты">
|
||||
{doc.links.map((link) => (
|
||||
<a className="docLink" href={link.href} key={link.href}>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
<div>
|
||||
<div className="docMeta">
|
||||
{doc.label}
|
||||
</div>
|
||||
<h2>{doc.title}</h2>
|
||||
<p>{doc.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer className="footer">
|
||||
Автор документации: <a href={authorUrl}>Сергей Громов</a>
|
||||
</footer>
|
||||
</main>
|
||||
<div className="docActions">
|
||||
{hasActionGroups ? (
|
||||
<DocActionsMenu groups={doc.actionGroups ?? {}} />
|
||||
) : isAvailable ? (
|
||||
<a className="docStatus docStatusLink" href={doc.href}>
|
||||
Открыть ->
|
||||
</a>
|
||||
) : (
|
||||
<span className="docStatus" aria-disabled="true">
|
||||
{doc.status}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{doc.links.length > 0 && (
|
||||
<div className="docLinks" aria-label="LLM-артефакты">
|
||||
{doc.links.map((link) => (
|
||||
<a className="docLink" href={link.href} key={link.href}>
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
|
||||
<footer className="footer">
|
||||
Автор документации: <a href={authorUrl}>Сергей Громов</a>
|
||||
</footer>
|
||||
</main>
|
||||
</MantineProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,10 +41,44 @@ export const docs: DocCard[] = [
|
||||
href: '/slm-design/',
|
||||
status: 'Доступно',
|
||||
accent: 'violet',
|
||||
links: [
|
||||
{ label: 'llms.txt', href: '/slm-design/llms.txt' },
|
||||
{ label: 'llms-full.txt', href: '/slm-design/llms-full.txt' },
|
||||
],
|
||||
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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user