No files in this category
No files in this category
# Текущее задание (Выполнить)
## Общее задание:
Разработать веб-приложение [название], используя платформу **s5l.ru**
### Дизайн:
- Темы: темная по умолчанию, переключаемая на светлую через `[data-theme="light"]`
- Цветовая палитра из `DesignTokens.styl`
- Atomic Design + User-Centered подход
- Полная поддержка WCAG 2.2 (контраст ≥ 4.5:1)
### Функциональность:
- Автоматическое определение языка (URL → браузер)
- Динамическая подгрузка контента из CouchDB с локальной репликацией через PouchDB
- Offline-first: работа без интернета после первого визита
- Единая система стилей через CSS-переменные и Tailwind
## Текущее действие:
Сделай первый этап разработки, включающий файлы app/pages/home* и `app/utils/AppDB.coffee` — полная реализация с методами `getDocumentByPath`
с проверкой наличия там документа, с начтройками саййта, если его нет создать документ по умолчанию при инициализации AppDB
Опиши следущие для разработки файлы.
---
# Промт для разработки на платформе s5l.ru
Ты — Senior Fullstack-архитектор и UI/UX-прагматик. Твоя задача — разрабатывать новые проекты, строго следуя логике и структуре платформы `s5l.ru`.
## Стек (НЕИЗМЕНЕН)
- **Frontend**: Vue 3 (Composition API через `render` функции), CoffeeScript, Pug, Stylus, Tailwind CSS, svg, webp, webm, peerjs, websocket, webtorrent
- **Backend**: CouchDB + PouchDB (репликация, дизайн-документы, админка)
- **Сборка**: все шаблоны → `pug.json`, стили → `styl.json`
- **Именование классов**: используй методику BEM
- **Глобальный контекст**: `globalThis._` — ссылка на корневой Vue-экземпляр из `app/app.coffee`
# Проект: s5l.ru
## Общее описание
`s5l.ru` — это **мультиязычная платформа для быстрого старта проектов** с поддержкой:
- offline-first через **PouchDB/CouchDB**
- автоматического определения языка (из URL → браузер)
- динамической подгрузки контента
- темной/светлой темы (`[data-theme="dark/light"]`)
- WCAG 2.2 (контраст ≥ 4.5:1)
## Стек
- **Frontend**: Vue 3 (render-функции), CoffeeScript, Pug, Stylus, Tailwind CSS
- **Backend**: CouchDB + PouchDB (репликация, дизайн-документы)
- **Сборка**: `pug.json`, `styl.json`
## Запуск нового проекта
1. Создать дизайн-документы в CouchDB
2. Положить стартовые документы с `type: 'page'`, `path: '/'`, и `translations`
3. Использовать `AppDB.getDocumentByPath` в `beforeMount`
4. Стили — только через CSS-переменные из `DesignTokens.styl`
## Доступность
- Поддержка тем: `:root` → `[data-theme="light"]`
- Контраст ≥ 4.5:1
- Анимации ≤ 500 мс
- Mobile-first верстка
## Архитектура
- Все тексты хранятся в **CouchDB** с поддержкой мультиязычности и мультидоменности
- Вся логика работы с БД — через `AppDB`
- Все компоненты — по **Atomic Design**, стили — через `DesignTokens.styl`
## Структура проекта
app/ ├── assets/ # Системные изображения, пиктограммы ├── app.coffee # инициализация Vue, AppDB, глобальный _ ├── app.pug # Основной шаблон с Хедером, , Футером ├── app.styl # Глобальные стили ├── DesignTokens.styl # дизайн-система ├── utils/ | └── AppDB.coffee # доступ к данным ├── pages/ # Страницы проекта | ├── Home.coffee | ├── Home.pug | └── Home.styl └── shared/
├── AppLink.* # компонент ссылок
└── ...
## Правила
### 1. Vue-компоненты
- **Имя файла**: PascalCase (`NewsList.coffee`, `NewsList.pug`, `NewsList.styl`)
- **Экспорт**: `module.exports = { name: '...', render: ..., data: -> {}, ... }`
- **Стили**: подключать через
coffee document.head.insertAdjacentHTML 'beforeend', ''+stylFns['app/shared/NewsList.styl']+'
'- **Шаблон**: рендерить через
coffee render: (new Function '_ctx', '_cache', renderFns['app/shared/NewsList.pug'])()
- **Жизненный цикл**:
❌ НЕПРАВИЛЬНО:
coffeescript async beforeMount: ->
✅ ПРАВИЛЬНО:
coffeescript beforeMount: ->
*(асинхронность обрабатывается внутри метода через `await`, но сигнатура — без `async`)*
### 2. Роутинг
- Все маршруты — в `temp.coffee` → `VueRouter.createRouter({ routes: [...] })`
- Компонент страницы должен быть `require`'нут без `.default` только если не экспортирует как `default`
### 3. Ссылки
- **ЗАПРЕЩЕНО**: `a(href="...")`, `router-link(to="...")`
- **ТОЛЬКО**: `app-link(to="...")` с подключением компонента `'app-link': require 'app/shared/AppLink'`
### 4. Стили
- **Цвета, отступы, шрифты** — ТОЛЬКО из `DesignTokens.styl` через CSS-переменные (`var(--primary-color)`, `var(--space-4)`)
- **Tailwind**: не использовать `@apply`
- **Stylus**: не использовать `@import '../DesignTokens.styl'` — он уже подключен глобально
### 5. Pug
- Атрибуты в одной строке, без многострочных выражений
- Внешние данные — только через `data` или `computed`
- **Tailwind-классы** — только внутри `class=""`, **не через точечную нотацию**
❌ НЕПРАВИЛЬНО:
pug .max-w-4xl.mx-auto.px-4
✅ ПРАВИЛЬНО:
pug div(class="max-w-4xl mx-auto px-4")
- Пример правильно:
pug div(v-for="item in items" :key="item.id") div(:class="isActive ? 'active' : 'inactive'" class="w-full")
### 6. CoffeeScript
- Отступы: 4 пробела
- `->` для методов, `=>` — только при необходимости сохранения `this`
- `debug.log "сообщение"` вместо `console.log`
- Строки: `"строка="+переменная`, без интерполяции
### 7. CouchDB
- Все запросы — через `AppDB`, который надо разработать под проект и подключить глобально в `app/app.coffee`
- Дизайн-документы: создавать функции `.toString()`
### 8. Доступность и дизайн
- WCAG 2.2 (контраст ≥ 4.5:1)
- Mobile-first
- Atomic Design + User-Centered подход
- Анимации: 200–500 мс, плавные переходы
## Обязательное требование
> **ВСЕГДА прикладывай полные листинги всех файлов** — даже если изменения минимальны. Частичные или сокращённые фрагменты недопустимы. Каждый файл должен быть представлен целиком, как он будет сохранён на диске.
## Шаблон компонента (полный пример)
**Файл**: `app/shared/NewsList.coffee`
coffeescript document.head.insertAdjacentHTML 'beforeend',''+stylFns['app/shared/NewsList.styl']+'
' module.exports =name: 'NewsList'
render: (new Function '_ctx', '_cache', renderFns['app/shared/NewsList.pug'])()
data: ->
return {
_: _
posts: []
}
beforeMount: ->
@posts = await AppDB.getPublishedPosts(limit: 10)
components:
'app-link': require 'app/shared/AppLink'
**Файл**: `app/shared/NewsList.pug`
pug div(class="space-y-4")
app-link(v-for="post in posts" :key="post.id" :to="'/pages/'+post.id")
h3(class="text-xl") {{ post.doc.h }}
**Файл**: `app/shared/NewsList.styl`
stylus // Только стили компонента. DesignTokens — через var() .news-item
padding: var(--space-4)
border-bottom: var(--border-1) solid var(--neutral-300)
## Запрещено
- React, TypeScript, MongoDB, SASS, Webpack, Vite
- Любые отклонения от стиля кода в `DEVELOPMENT.md`
- Использование `Vue = require 'vue'` — всё глобально
Следуй этому промту для всех новых проектов.
---
:root
// Цветовая система для темной темы по умолчанию
--primary-color: #f87171
--primary-dark: #ef4444
--primary-light: #fca5a5
--secondary-color: #1e293b
--secondary-dark: #0f172a
--secondary-light: #334155
--accent-color: #22d3ee
--accent-dark: #06b6d4
--accent-light: #67e8f9
// Нейтральные цвета для темной темы
--neutral-50: #0f172a
--neutral-100: #1e293b
--neutral-200: #334155
--neutral-300: #475569
--neutral-400: #64748b
--neutral-500: #94a3b8
--neutral-600: #cbd5e1
--neutral-700: #e2e8f0
--neutral-800: #f1f5f9
--neutral-900: #f8fafc
// Цвета текста для темной темы
--text-primary: #f8fafc
--text-secondary: #e2e8f0
--text-muted: #94a3b8
// Цвета фона для темной темы
--bg-primary: #0f172a
--bg-secondary: #1e293b
--bg-card: #1e293b
--bg-overlay: rgba(15, 23, 42, 0.8)
// Градиенты для темной темы
--gradient-primary: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 50%, var(--accent-color) 100%)
--gradient-dark: linear-gradient(135deg, var(--secondary-dark) 0%, var(--secondary-color) 100%)
// Типографическая система
--font-family-sans: 'Inter', 'Segoe UI', system-ui, sans-serif
--font-family-serif: 'Georgia', 'Times New Roman', serif
--text-xs: 0.75rem
--text-sm: 0.875rem
--text-base: 1rem
--text-lg: 1.125rem
--text-xl: 1.25rem
--text-2xl: 1.5rem
--text-3xl: 1.875rem
--text-4xl: 2.25rem
--text-5xl: 3rem
--font-light: 300
--font-normal: 400
--font-medium: 500
--font-semibold: 600
--font-bold: 700
// Spacing system
--space-1: 0.25rem
--space-2: 0.5rem
--space-3: 0.75rem
--space-4: 1rem
--space-5: 1.25rem
--space-6: 1.5rem
--space-8: 2rem
--space-10: 2.5rem
--space-12: 3rem
--space-16: 4rem
--space-20: 5rem
// Тени и эффекты для темной темы
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.5)
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.6), 0 2px 4px -1px rgba(0, 0, 0, 0.4)
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.7), 0 4px 6px -2px rgba(0, 0, 0, 0.5)
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.8), 0 10px 10px -5px rgba(0, 0, 0, 0.6)
// Анимации
--transition-fast: 0.15s ease-in-out
--transition-normal: 0.3s ease-in-out
--transition-slow: 0.5s ease-in-out
// Breakpoints
--breakpoint-sm: 640px
--breakpoint-md: 768px
--breakpoint-lg: 1024px
--breakpoint-xl: 1280px
--breakpoint-2xl: 1536px
// Светлая тема
[data-theme="light"]
--primary-color: #e11d48
--primary-dark: #be123c
--primary-light: #fb7185
--secondary-color: #f8fafc
--secondary-dark: #f1f5f9
--secondary-light: #ffffff
--accent-color: #06b6d4
--accent-dark: #0891b2
--accent-light: #22d3ee
--neutral-50: #f8fafc
--neutral-100: #f1f5f9
--neutral-200: #e2e8f0
--neutral-300: #cbd5e1
--neutral-400: #94a3b8
--neutral-500: #64748b
--neutral-600: #475569
--neutral-700: #334155
--neutral-800: #1e293b
--neutral-900: #0f172a
--text-primary: #0f172a
--text-secondary: #334155
--text-muted: #64748b
--bg-primary: #ffffff
--bg-secondary: #f8fafc
--bg-card: #ffffff
--bg-overlay: rgba(255, 255, 255, 0.8)
--gradient-primary: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 50%, var(--accent-color) 100%)
--gradient-dark: linear-gradient(135deg, var(--neutral-100) 0%, var(--neutral-50) 100%)
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05)
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)
// Утилитарные классы для кастомных свойств
.bg-primary
background-color: var(--primary-color)
.text-primary
color: var(--primary-color)
.border-primary
border-color: var(--primary-color)
.bg-accent
background-color: var(--accent-color)
.text-accent
color: var(--accent-color)
.bg-neutral
background-color: var(--neutral-500)
.text-neutral
color: var(--neutral-500)
.bg-dark
background-color: var(--bg-primary)
.text-dark
color: var(--text-primary)
.bg-card
background-color: var(--bg-card)
.text-card
color: var(--text-primary)
// Анимации
.transition-fast
transition: var(--transition-fast)
.transition-normal
transition: var(--transition-normal)
.transition-slow
transition: var(--transition-slow)
// Тени
.shadow-custom-sm
box-shadow: var(--shadow-sm)
.shadow-custom-md
box-shadow: var(--shadow-md)
.shadow-custom-lg
box-shadow: var(--shadow-lg)
.shadow-custom-xl
box-shadow: var(--shadow-xl)
// Добавляем дополнительные утилитарные классы для лучшей поддержки компонентов
.card-primary
background-color: var(--bg-card)
border: 1px solid var(--neutral-300)
border-radius: 8px
box-shadow: var(--shadow-sm)
transition: all var(--transition-normal)
.card-primary:hover
border-color: var(--primary-color)
box-shadow: var(--shadow-md)
.text-icon
filter: brightness(0) invert(0)
transition: filter var(--transition-fast)
[data-theme="dark"] .text-icon
filter: brightness(0) invert(1)
.icon-primary
filter: brightness(0) invert(0)
transition: filter var(--transition-fast)
[data-theme="dark"] .icon-primary
filter: brightness(0) invert(1)
// Утилиты для контрастного текста
.text-contrast-high
color: var(--text-primary)
.text-contrast-medium
color: var(--text-secondary)
.text-contrast-low
color: var(--text-muted)
// Утилиты для фонов
.bg-surface
background-color: var(--bg-card)
.bg-surface-alt
background-color: var(--bg-secondary)
html.json
pug.json
styl.json
# Подключение мета информации
document.head.insertAdjacentHTML 'beforeend', '<meta charset="UTF-8">'
document.head.insertAdjacentHTML 'beforeend', '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
# Настройка tailwind
#tailwind.config = require 'tailwind.config.js'
# Подключение основных стилей
document.head.insertAdjacentHTML('beforeend', '<style type="text/tailwindcss" file="main.css">' + stylFns['main.css'] + '</style>')
document.head.insertAdjacentHTML('beforeend', '<style type="text/tailwindcss" file="app/DesignTokens.styl">' + stylFns['app/DesignTokens.styl'] + '</style>')
document.head.insertAdjacentHTML('beforeend', '<style type="text/tailwindcss" file="app/app.styl">' + stylFns['app/app.styl'] + '</style>')
# Маршруты
routes = [
{ path: '/', component: require 'app/pages/Home' }
{ path: '/:path*', component: require 'app/pages/DocumentPage' }
]
globalThis._ = {}
# Глобальное определение vuejs приложения
globalThis.app = Vue.createApp
name: 'app'
data: () ->
return {
appState:
currentDocument: null
currentLanguage: 'ru'
availableLanguages: ['ru', 'en', 'tj']
loading: true
error: null
dbService: new (require('app/core/DB'))()
}
beforeMount: ->
globalThis.AppDB = new (require 'app/utils/AppDB')()
await globalThis.AppDB.init()
globalThis._ = @
computed:
currentLanguage: ->
@appState.currentLanguage
watch:
currentLanguage:
handler: (newDoc) ->
debug.dir newDoc
@loadDocumentForPath(window.location.pathname)
immediate: true
methods:
initializeApp: ->
# Определяем язык из URL или браузера
@detectLanguage()
# Загружаем документ для текущего пути
@loadDocumentForPath(window.location.pathname)
detectLanguage: ->
# Простая логика определения языка
pathLang = window.location.pathname.split('/')[1]
if pathLang in @appState.availableLanguages
@appState.currentLanguage = pathLang
else
browserLang = navigator.language.split('-')[0]
if browserLang in @appState.availableLanguages
@appState.currentLanguage = browserLang
loadDocumentForPath: (path) ->
try
@appState.loading = true
doc = await AppDB.getDocumentByPath(path, AppDB.currentLanguage)
@appState.currentDocument = doc
@appState.loading = false
# Устанавливаем заголовок страницы
if doc?.title
document.head.insertAdjacentHTML('beforeend', '<title>' + doc.title + '</title>')
catch error
@appState.error = "Ошибка загрузки документа: " + error
@appState.loading = false
render: (new Function '_ctx', '_cache', renderFns['app/app.pug'])()
components: {
'hero-section': require('shared/HeroSection')
'image-gallery': require('shared/ImageGallery')
}
# Создаем и настраиваем роутер
router = VueRouter.createRouter({
routes: routes
history: VueRouter.createWebHistory()
scrollBehavior: (to, from, savedPosition) ->
if savedPosition
return savedPosition
else
return { x: 0, y: 0 }
})
app.use(router)
app.mount('body')
include ../pug/base.pug
include ../pug/bem.pug
div(class="min-h-screen bg-white dark:bg-gray-900 transition-colors duration-300")
div(v-if="appState.loading" class="flex items-center justify-center min-h-screen")
div(class="text-center")
div(class="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600")
p(class="mt-4 text-gray-600 dark:text-gray-400") Загрузка...
div(v-else-if="appState.error" class="flex items-center justify-center min-h-screen")
div(class="text-center")
div(class="text-red-600 text-xl") Ошибка
p(class="text-gray-600 dark:text-gray-400") {{ appState.error }}
router-view(v-else)
// Переменные темы
:root
--primary-color: #3b82f6
--secondary-color: #1e40af
--text-primary: #1f2937
--text-secondary: #6b7280
--bg-primary: #ffffff
--bg-secondary: #f9fafb
[data-theme="dark"]
--primary-color: #60a5fa
--secondary-color: #3b82f6
--text-primary: #f9fafb
--text-secondary: #d1d5db
--bg-primary: #111827
--bg-secondary: #1f2937
// Базовые стили
body
font-family: 'Inter', system-ui, -apple-system, sans-serif
color: var(--text-primary)
background-color: var(--bg-primary)
transition: all 0.3s ease
// Стили для Markdown контента
.prose
h1, h2, h3, h4, h5, h6
color: var(--text-primary)
font-weight: 600
p
color: var(--text-secondary)
line-height: 1.7
a
color: var(--primary-color)
text-decoration: none
&:hover
text-decoration: underline
img
border-radius: 0.5rem
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1)
include ../pug/base.pug
include ../pug/bem.pug
+mbh
header(class="sticky top-0 z-50 glass border-b border-neutral-300/60 transition-all duration-300")
div(class="navbar max-w-7xl mx-auto px-4 sm:px-6 lg:px-8")
block top-content
+mbl(class="py-8 md:py-12 lg:py-16")
.container
block content
document.head.insertAdjacentHTML 'beforeend', '<style type="text/css" file="app/pages/Home.styl">'+stylFns['app/pages/Home.styl']+'</style>'
module.exports =
name: 'Home'
render: (new Function '_ctx', '_cache', renderFns['app/pages/Home.pug'])()
data: ->
return {
_: _
document: null
}
beforeMount: ->
try
@document = await AppDB.getDocumentByPath('/', AppDB.currentLanguage)
catch e
debug.log "Document load error:", e
components:
'app-link': require 'app/shared/AppLink'
'hero-section': require 'app/shared/HeroSection'
'image-gallery': require 'app/shared/ImageGallery'
include ../../pug/base.pug
include ../../pug/bem.pug
extends ../layout.pug
block top-content
hero-section(:document="document")
block content
div(class="max-w-4xl mx-auto px-4" v-if="document")
h1(class="text-3xl font-bold text-contrast-high animate-fade-in-up") {{ document.translations[_.currentLanguage]?.title || document.translations.en.title }}
p(class="text-xl text-contrast-medium mb-8 animate-fade-in-up") {{ document.translations[_.currentLanguage]?.subtitle || document.translations.en.subtitle }}
div(class="prose mt-6 animate-fade-in-up" v-html="marked.parse(document.translations[_.currentLanguage]?.content || document.translations.en.content)")
div(class="mt-12")
image-gallery(:images="document.translations[_.currentLanguage]?.gallery || document.translations.en.gallery")
// Используем только утилиты из DesignTokens
.animate-fade-in-up
animation: fadeInUp var(--transition-slow) ease-out
@keyframes fadeInUp
from
opacity: 0
transform: translateY(var(--transition-px))
to
opacity: 1
transform: translateY(0)
document.head.insertAdjacentHTML('beforeend','<style type="text/css" file="app/shared/AppLink.styl">'+stylFns['app/shared/AppLink.styl']+'</style>')
module.exports =
default:
render: (new Function '_ctx', '_cache', renderFns['app/shared/AppLink.pug'])()
name: 'AppLink'
props:
to:
type: [String, Object]
required: true
computed:
isExternal: ->
if typeof @.to == 'string'
return @.to.startsWith('http')
return false
data: ->
return {
_: _
}
include ../../pug/base.pug
include ../../pug/bem.pug
a.app-link(v-if="isExternal" v-bind="$attrs" :href="to" target="_blank" rel="noopener")
slot
router-link.app-link(v-else v-bind="$attrs" :to="to")
slot
@import '../DesignTokens.styl'
// Базовые стили для AppLink компонента
.app-link
color: var(--primary-color)
text-decoration: none
transition: color var(--transition-fast)
.app-link:hover
color: var(--primary-dark)
text-decoration: underline
.app-link.router-link-active
font-weight: var(--font-semibold)
color: var(--primary-dark)
// Стили для внешних ссылок
.app-link-external::after
content: " ↗"
font-size: 0.875em
opacity: 0.7
// Стили для кнопко-подобных ссылок
.app-link-button
display: inline-flex
align-items: center
gap: var(--space-2)
padding: var(--space-2) var(--space-4)
background: var(--primary-color)
color: white
border-radius: 6px
font-weight: var(--font-medium)
transition: all var(--transition-fast)
.app-link-button:hover
background: var(--primary-dark)
transform: translateY(-1px)
text-decoration: none
color: white
box-shadow: var(--shadow-md)
// Стили для иконок в ссылках
.app-link-with-icon
display: inline-flex
align-items: center
gap: var(--space-2)
.app-link-icon
width: 16px
height: 16px
transition: transform var(--transition-fast)
.app-link:hover .app-link-icon
transform: translateX(2px)
document.head.insertAdjacentHTML 'beforeend', '<style type="text/css" file="app/shared/HeroSection.styl">'+stylFns['app/shared/HeroSection.styl']+'</style>'
module.exports =
name: 'HeroSection'
props: [ 'document' ]
render: (new Function '_ctx', '_cache', renderFns['app/shared/HeroSection.pug'])()
include ../../pug/base.pug
include ../../pug/bem.pug
section(v-if="document" class="hero-section bg-gradient")
div(class="max-w-7xl mx-auto px-4 py-24 text-center text-white")
h1(v-if="document.translations[_.appState.currentLanguage]" class="text-5xl font-bold mb-6") {{ document.translations[_.appState.currentLanguage].title || document.translations.en.title }}
p(v-if="document.translations[_.appState.currentLanguage]" class="text-xl opacity-90") {{ document.translations[_.appState.currentLanguage].subtitle || document.translations.en.subtitle }}
.hero-section
background: var(--gradient-primary)
color: white
padding: var(--space-16) 0
text-align: center
document.head.insertAdjacentHTML 'beforeend', '<style type="text/css" file="app/shared/ImageGallery.styl">'+stylFns['app/shared/ImageGallery.styl']+'</style>'
module.exports =
name: 'ImageGallery'
props:
images:
type: Array
default: -> []
render: (new Function '_ctx', '_cache', renderFns['app/shared/ImageGallery.pug'])()
include ../../pug/base.pug
include ../../pug/bem.pug
div(v-if="images && images.length > 0" class="image-gallery grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6")
div(v-for="(img, idx) in images" :key="idx" class="overflow-hidden rounded-lg shadow-custom-md bg-surface")
img(:src="img.src" :alt="img.alt || ''" class="w-full h-auto object-cover transition-transform duration-300 hover:scale-105")
.image-gallery
display: grid
gap: var(--space-6)
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))
.image-gallery img
width: 100%
height: auto
object-fit: cover
border-radius: 8px
transition: transform var(--transition-normal)
.image-gallery img:hover
transform: scale(1.05)
# FILE: app/utils/AppDB.coffee
class AppDB
currentLanguage: 'ru'
currentProject: 's5l.ru'
designDocVersion: '1.1' # управление версией дизайна
constructor: ->
@localDB = new PouchDB('s5l_local')
@remoteDB = new PouchDB('https://oleg:631074@couchdb.favt.ru.net/s5lru/')
@syncHandler = null
init: ->
try
await @remoteDB.info()
debug.log "Remote DB connected"
await @ensureDesignDocs()
await @ensureDefaultContent()
@startSync()
AppDB.currentLanguage = globalThis._?.appState?.currentLanguage or 'ru'
catch e
debug.log "DB init failed:", e
startSync: ->
@syncHandler = @localDB.sync(@remoteDB, { live: true, retry: true })
.on 'error', (err) -> debug.log "Sync error:", err
ensureDesignDocs: ->
adminDoc =
_id: '_design/admin'
version: @designDocVersion
views:
byPath:
map: (doc) ->
if doc.type == 'page'
emit [doc.domain, doc.path], doc
.toString()
byType:
map: (doc) ->
emit doc.type, doc
.toString()
try
existing = await @remoteDB.get('_design/admin')
if existing.version != @designDocVersion
adminDoc._rev = existing._rev
await @remoteDB.put(adminDoc)
debug.log "Design doc updated to v#{@designDocVersion}"
catch
await @remoteDB.put(adminDoc)
debug.log "Design doc created v#{@designDocVersion}"
ensureDefaultContent: ->
defaultHome =
_id: 'page::s5l.ru::/'
type: 'page'
domain: 's5l.ru'
path: '/'
translations:
ru:
title: "s5l.ru — мультиязычная offline-first платформа"
subtitle: "Разрабатывайте быстро, работайте везде"
content: '''
# Добро пожаловать на s5l.ru
**s5l.ru** — это платформа для быстрого запуска веб-проектов с поддержкой:
- offline-first через PouchDB/CouchDB
- автоматического переключения языка
- динамической подгрузки контента
- полной WCAG 2.2-совместимости
Все тексты хранятся в базе и легко редактируются через админку.
'''
gallery: [
{ src: "/assets/hero-s5l.svg", alt: "Hero illustration" }
]
en:
title: "s5l.ru — multilingual offline-first platform"
subtitle: "Build fast, work anywhere"
content: '''
# Welcome to s5l.ru
**s5l.ru** is a platform for rapid web project launches with:
- offline-first via PouchDB/CouchDB
- automatic language switching
- dynamic content loading
- full WCAG 2.2 compliance
All text is stored in the database and editable via admin panel.
'''
gallery: [
{ src: "/assets/hero-s5l.svg", alt: "Hero illustration" }
]
tj:
title: "s5l.ru — платформаи бисёрзабон ва аввал офлайн"
subtitle: "Бисёр тез бунёд кунед, дар ҳама ҷо кор кунед"
content: '''
# Ба s5l.ru хуш омадед
**s5l.ru** — ин платформа барои оғози тези лоиҳаҳои веб аст бо:
- офлайн-аввал аз рӯи PouchDB/CouchDB
- ивази худкори забон
- боркунии динамикӣ
- мутобиқати пурраи WCAG 2.2
Ҳамаи матнҳо дар база нигоҳ дошта мешаванд ва аз ҷониби панели маъмури озодона таҳрир карда мешаванд.
'''
gallery: [
{ src: "/assets/hero-s5l.svg", alt: "Hero illustration" }
]
try
await @remoteDB.get('page::s5l.ru::/')
catch
await @remoteDB.put(defaultHome)
debug.log "Default home page created"
getDocumentByPath: (path, lang = @currentLanguage) ->
path = path or '/'
try
result = await @localDB.query('admin/byPath', { key: [@currentProject, path], include_docs: true })
if result.rows.length > 0
doc = result.rows[0].doc
return doc
else
throw new Error "Document not found"
catch e
debug.log "Fallback for path:", path, "lang:", lang
# fallback to English if not found
if lang != 'en'
return await @getDocumentByPath(path, 'en')
else
throw new Error "Document not available even in English"
module.exports = AppDB
module.exports =
version: "0.0.1"
adres: [
{
"adr": "https://cdn.tailwindcss.com/3.4.17"
"eventName": "tailwindReady"
"obj": "tailwind"
}
{
"adr": "https://unpkg.com/pouchdb/dist/pouchdb.min.js"
"eventName": "puochReady"
"obj": "PouchDB"
}
{
"adr": "https://unpkg.com/vue@3/dist/vue.runtime.global.prod.js"
"eventName": "vueReady"
"obj": "Vue"
}
{
"adr": "https://unpkg.com/vue-router@4/dist/vue-router.global.js"
"eventName": "VueRouterReady"
"obj": "VueRouter"
}
{
"adr": "https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@3.5.22/dist/compiler-sfc.esm-browser.min.js"
"eventName": "compileTemplateReady"
"obj": "compileTemplate"
}
{
"adr": "https://cdn.jsdelivr.net/npm/marked@16.4.1/lib/marked.umd.min.js"
"eventName": "markedReady"
"obj": "marked"
}
{
"adr": "https://cdn.jsdelivr.net/npm/openlayers@4.6.5/dist/ol.min.js"
"css": "https://cdn.jsdelivr.net/npm/openlayers@4.6.5/dist/ol.min.css"
"eventName": "leafletReady"
"obj": "ol"
}
]
globalThis.debug = require('debug.coffee').default
require('headVue.coffee')
document.documentElement.classList.add('dark')
globalThis.initCount = 0
ic = ()->
if initCount > 5
window.location.reload()
else if not globalThis['appReady']
initCount++
#setTimeout ic, 200
ic()
# обязательно подключение глобальных массивов
globalThis.renderFns = require 'pug.json'
globalThis.stylFns = require 'styl.json'
try
init = (event={})->
debug.dir globalThis['vueReady']
debug.log "Init Start"
if not globalThis['appReady'] and globalThis['vueReady'] and globalThis['puochReady']
debug.log 'init start ok'
try
require('app/app.coffee')
globalThis['appReady'] = true
debug.log "init is ok"
catch err
debug.dir err
else if not globalThis['appReady']
debug.log 'pausedEvent appReady'
setTimeout init, 200
init()
include ./bem.pug
mixin mbh
- var otherClasses = attributes.class || ''
+b('hfn')(class="h-max overflow-hidden relative")
+e('hfn','fon')
mixin mbl
- var otherClasses = attributes.class || ''
+b('hfn')(class="h-max overflow-hidden relative")
+e('hfn','fon')(class="[background:url('https://jahonnamo.s5l.ru/assets/jahonnamo.s5l.ru/bkg00.webp')_0_0_/_cover_no-repeat] bg-[#ffffff] absolute h-full w-full z-[-1]")
if block
block
//- bem.pug
//- Базовый миксин для блоков и элементов
mixin b(blockName, elemName)
- var className = blockName
- var twClasses = attributes.tw || ''
- var otherClasses = attributes.class || ''
if elemName
- className += '__' + elemName
//- Обработка модификаторов
if attributes.mod
- className += ' ' + blockName + '--' + attributes.mod
if attributes.mods
each mod in attributes.mods.split(',')
- className += ' ' + blockName + '--' + mod.trim()
//- Собираем финальный класс
- var finalClass = [className, twClasses, otherClasses].filter(Boolean).join(' ')
//- Удаляем обработанные атрибуты
- attributes.tw = null
- attributes.mod = null
- attributes.mods = null
- attributes.class = null
//- Генерация элемента
if block
div(class=finalClass)&attributes(attributes)
block
else
div(class=finalClass)&attributes(attributes)
//- Миксин для элементов (альтернативный синтаксис)
mixin e(blockName, elemName)
+b(blockName, elemName)&attributes(attributes)
block
// FILE: README.md // TYPE: Markdown // SIZE: 7142 characters // CATEGORY: PRIORITY DOCUMENTATION
# Текущее задание (Выполнить)
## Общее задание:
Разработать веб-приложение [название], используя платформу **s5l.ru**
### Дизайн:
- Темы: темная по умолчанию, переключаемая на светлую через `[data-theme="light"]`
- Цветовая палитра из `DesignTokens.styl`
- Atomic Design + User-Centered подход
- Полная поддержка WCAG 2.2 (контраст ≥ 4.5:1)
### Функциональность:
- Автоматическое определение языка (URL → браузер)
- Динамическая подгрузка контента из CouchDB с локальной репликацией через PouchDB
- Offline-first: работа без интернета после первого визита
- Единая система стилей через CSS-переменные и Tailwind
## Текущее действие:
Сделай первый этап разработки, включающий файлы app/pages/home* и `app/utils/AppDB.coffee` — полная реализация с методами `getDocumentByPath`
с проверкой наличия там документа, с начтройками саййта, если его нет создать документ по умолчанию при инициализации AppDB
Опиши следущие для разработки файлы.
---
# Промт для разработки на платформе s5l.ru
Ты — Senior Fullstack-архитектор и UI/UX-прагматик. Твоя задача — разрабатывать новые проекты, строго следуя логике и структуре платформы `s5l.ru`.
## Стек (НЕИЗМЕНЕН)
- **Frontend**: Vue 3 (Composition API через `render` функции), CoffeeScript, Pug, Stylus, Tailwind CSS, svg, webp, webm, peerjs, websocket, webtorrent
- **Backend**: CouchDB + PouchDB (репликация, дизайн-документы, админка)
- **Сборка**: все шаблоны → `pug.json`, стили → `styl.json`
- **Именование классов**: используй методику BEM
- **Глобальный контекст**: `globalThis._` — ссылка на корневой Vue-экземпляр из `app/app.coffee`
# Проект: s5l.ru
## Общее описание
`s5l.ru` — это **мультиязычная платформа для быстрого старта проектов** с поддержкой:
- offline-first через **PouchDB/CouchDB**
- автоматического определения языка (из URL → браузер)
- динамической подгрузки контента
- темной/светлой темы (`[data-theme="dark/light"]`)
- WCAG 2.2 (контраст ≥ 4.5:1)
## Стек
- **Frontend**: Vue 3 (render-функции), CoffeeScript, Pug, Stylus, Tailwind CSS
- **Backend**: CouchDB + PouchDB (репликация, дизайн-документы)
- **Сборка**: `pug.json`, `styl.json`
## Запуск нового проекта
1. Создать дизайн-документы в CouchDB
2. Положить стартовые документы с `type: 'page'`, `path: '/'`, и `translations`
3. Использовать `AppDB.getDocumentByPath` в `beforeMount`
4. Стили — только через CSS-переменные из `DesignTokens.styl`
## Доступность
- Поддержка тем: `:root` → `[data-theme="light"]`
- Контраст ≥ 4.5:1
- Анимации ≤ 500 мс
- Mobile-first верстка
## Архитектура
- Все тексты хранятся в **CouchDB** с поддержкой мультиязычности и мультидоменности
- Вся логика работы с БД — через `AppDB`
- Все компоненты — по **Atomic Design**, стили — через `DesignTokens.styl`
## Структура проекта
app/ ├── assets/ # Системные изображения, пиктограммы ├── app.coffee # инициализация Vue, AppDB, глобальный _ ├── app.pug # Основной шаблон с Хедером, , Футером ├── app.styl # Глобальные стили ├── DesignTokens.styl # дизайн-система ├── utils/ | └── AppDB.coffee # доступ к данным ├── pages/ # Страницы проекта | ├── Home.coffee | ├── Home.pug | └── Home.styl └── shared/
├── AppLink.* # компонент ссылок
└── ...
## Правила
### 1. Vue-компоненты
- **Имя файла**: PascalCase (`NewsList.coffee`, `NewsList.pug`, `NewsList.styl`)
- **Экспорт**: `module.exports = { name: '...', render: ..., data: -> {}, ... }`
- **Стили**: подключать через
coffee document.head.insertAdjacentHTML 'beforeend', ''+stylFns['app/shared/NewsList.styl']+'
'- **Шаблон**: рендерить через
coffee render: (new Function '_ctx', '_cache', renderFns['app/shared/NewsList.pug'])()
- **Жизненный цикл**:
❌ НЕПРАВИЛЬНО:
coffeescript async beforeMount: ->
✅ ПРАВИЛЬНО:
coffeescript beforeMount: ->
*(асинхронность обрабатывается внутри метода через `await`, но сигнатура — без `async`)*
### 2. Роутинг
- Все маршруты — в `temp.coffee` → `VueRouter.createRouter({ routes: [...] })`
- Компонент страницы должен быть `require`'нут без `.default` только если не экспортирует как `default`
### 3. Ссылки
- **ЗАПРЕЩЕНО**: `a(href="...")`, `router-link(to="...")`
- **ТОЛЬКО**: `app-link(to="...")` с подключением компонента `'app-link': require 'app/shared/AppLink'`
### 4. Стили
- **Цвета, отступы, шрифты** — ТОЛЬКО из `DesignTokens.styl` через CSS-переменные (`var(--primary-color)`, `var(--space-4)`)
- **Tailwind**: не использовать `@apply`
- **Stylus**: не использовать `@import '../DesignTokens.styl'` — он уже подключен глобально
### 5. Pug
- Атрибуты в одной строке, без многострочных выражений
- Внешние данные — только через `data` или `computed`
- **Tailwind-классы** — только внутри `class=""`, **не через точечную нотацию**
❌ НЕПРАВИЛЬНО:
pug .max-w-4xl.mx-auto.px-4
✅ ПРАВИЛЬНО:
pug div(class="max-w-4xl mx-auto px-4")
- Пример правильно:
pug div(v-for="item in items" :key="item.id") div(:class="isActive ? 'active' : 'inactive'" class="w-full")
### 6. CoffeeScript
- Отступы: 4 пробела
- `->` для методов, `=>` — только при необходимости сохранения `this`
- `debug.log "сообщение"` вместо `console.log`
- Строки: `"строка="+переменная`, без интерполяции
### 7. CouchDB
- Все запросы — через `AppDB`, который надо разработать под проект и подключить глобально в `app/app.coffee`
- Дизайн-документы: создавать функции `.toString()`
### 8. Доступность и дизайн
- WCAG 2.2 (контраст ≥ 4.5:1)
- Mobile-first
- Atomic Design + User-Centered подход
- Анимации: 200–500 мс, плавные переходы
## Обязательное требование
> **ВСЕГДА прикладывай полные листинги всех файлов** — даже если изменения минимальны. Частичные или сокращённые фрагменты недопустимы. Каждый файл должен быть представлен целиком, как он будет сохранён на диске.
## Шаблон компонента (полный пример)
**Файл**: `app/shared/NewsList.coffee`
coffeescript document.head.insertAdjacentHTML 'beforeend',''+stylFns['app/shared/NewsList.styl']+'
' module.exports =name: 'NewsList'
render: (new Function '_ctx', '_cache', renderFns['app/shared/NewsList.pug'])()
data: ->
return {
_: _
posts: []
}
beforeMount: ->
@posts = await AppDB.getPublishedPosts(limit: 10)
components:
'app-link': require 'app/shared/AppLink'
**Файл**: `app/shared/NewsList.pug`
pug div(class="space-y-4")
app-link(v-for="post in posts" :key="post.id" :to="'/pages/'+post.id")
h3(class="text-xl") {{ post.doc.h }}
**Файл**: `app/shared/NewsList.styl`
stylus // Только стили компонента. DesignTokens — через var() .news-item
padding: var(--space-4)
border-bottom: var(--border-1) solid var(--neutral-300)
## Запрещено
- React, TypeScript, MongoDB, SASS, Webpack, Vite
- Любые отклонения от стиля кода в `DEVELOPMENT.md`
- Использование `Vue = require 'vue'` — всё глобально
Следуй этому промту для всех новых проектов.
---
// FILE: app/DesignTokens.styl // TYPE: Stylus // SIZE: 5908 characters // CATEGORY: PRIORITY DOCUMENTATION
:root
// Цветовая система для темной темы по умолчанию
--primary-color: #f87171
--primary-dark: #ef4444
--primary-light: #fca5a5
--secondary-color: #1e293b
--secondary-dark: #0f172a
--secondary-light: #334155
--accent-color: #22d3ee
--accent-dark: #06b6d4
--accent-light: #67e8f9
// Нейтральные цвета для темной темы
--neutral-50: #0f172a
--neutral-100: #1e293b
--neutral-200: #334155
--neutral-300: #475569
--neutral-400: #64748b
--neutral-500: #94a3b8
--neutral-600: #cbd5e1
--neutral-700: #e2e8f0
--neutral-800: #f1f5f9
--neutral-900: #f8fafc
// Цвета текста для темной темы
--text-primary: #f8fafc
--text-secondary: #e2e8f0
--text-muted: #94a3b8
// Цвета фона для темной темы
--bg-primary: #0f172a
--bg-secondary: #1e293b
--bg-card: #1e293b
--bg-overlay: rgba(15, 23, 42, 0.8)
// Градиенты для темной темы
--gradient-primary: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 50%, var(--accent-color) 100%)
--gradient-dark: linear-gradient(135deg, var(--secondary-dark) 0%, var(--secondary-color) 100%)
// Типографическая система
--font-family-sans: 'Inter', 'Segoe UI', system-ui, sans-serif
--font-family-serif: 'Georgia', 'Times New Roman', serif
--text-xs: 0.75rem
--text-sm: 0.875rem
--text-base: 1rem
--text-lg: 1.125rem
--text-xl: 1.25rem
--text-2xl: 1.5rem
--text-3xl: 1.875rem
--text-4xl: 2.25rem
--text-5xl: 3rem
--font-light: 300
--font-normal: 400
--font-medium: 500
--font-semibold: 600
--font-bold: 700
// Spacing system
--space-1: 0.25rem
--space-2: 0.5rem
--space-3: 0.75rem
--space-4: 1rem
--space-5: 1.25rem
--space-6: 1.5rem
--space-8: 2rem
--space-10: 2.5rem
--space-12: 3rem
--space-16: 4rem
--space-20: 5rem
// Тени и эффекты для темной темы
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.5)
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.6), 0 2px 4px -1px rgba(0, 0, 0, 0.4)
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.7), 0 4px 6px -2px rgba(0, 0, 0, 0.5)
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.8), 0 10px 10px -5px rgba(0, 0, 0, 0.6)
// Анимации
--transition-fast: 0.15s ease-in-out
--transition-normal: 0.3s ease-in-out
--transition-slow: 0.5s ease-in-out
// Breakpoints
--breakpoint-sm: 640px
--breakpoint-md: 768px
--breakpoint-lg: 1024px
--breakpoint-xl: 1280px
--breakpoint-2xl: 1536px
// Светлая тема
[data-theme="light"]
--primary-color: #e11d48
--primary-dark: #be123c
--primary-light: #fb7185
--secondary-color: #f8fafc
--secondary-dark: #f1f5f9
--secondary-light: #ffffff
--accent-color: #06b6d4
--accent-dark: #0891b2
--accent-light: #22d3ee
--neutral-50: #f8fafc
--neutral-100: #f1f5f9
--neutral-200: #e2e8f0
--neutral-300: #cbd5e1
--neutral-400: #94a3b8
--neutral-500: #64748b
--neutral-600: #475569
--neutral-700: #334155
--neutral-800: #1e293b
--neutral-900: #0f172a
--text-primary: #0f172a
--text-secondary: #334155
--text-muted: #64748b
--bg-primary: #ffffff
--bg-secondary: #f8fafc
--bg-card: #ffffff
--bg-overlay: rgba(255, 255, 255, 0.8)
--gradient-primary: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary-color) 50%, var(--accent-color) 100%)
--gradient-dark: linear-gradient(135deg, var(--neutral-100) 0%, var(--neutral-50) 100%)
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05)
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)
// Утилитарные классы для кастомных свойств
.bg-primary
background-color: var(--primary-color)
.text-primary
color: var(--primary-color)
.border-primary
border-color: var(--primary-color)
.bg-accent
background-color: var(--accent-color)
.text-accent
color: var(--accent-color)
.bg-neutral
background-color: var(--neutral-500)
.text-neutral
color: var(--neutral-500)
.bg-dark
background-color: var(--bg-primary)
.text-dark
color: var(--text-primary)
.bg-card
background-color: var(--bg-card)
.text-card
color: var(--text-primary)
// Анимации
.transition-fast
transition: var(--transition-fast)
.transition-normal
transition: var(--transition-normal)
.transition-slow
transition: var(--transition-slow)
// Тени
.shadow-custom-sm
box-shadow: var(--shadow-sm)
.shadow-custom-md
box-shadow: var(--shadow-md)
.shadow-custom-lg
box-shadow: var(--shadow-lg)
.shadow-custom-xl
box-shadow: var(--shadow-xl)
// Добавляем дополнительные утилитарные классы для лучшей поддержки компонентов
.card-primary
background-color: var(--bg-card)
border: 1px solid var(--neutral-300)
border-radius: 8px
box-shadow: var(--shadow-sm)
transition: all var(--transition-normal)
.card-primary:hover
border-color: var(--primary-color)
box-shadow: var(--shadow-md)
.text-icon
filter: brightness(0) invert(0)
transition: filter var(--transition-fast)
[data-theme="dark"] .text-icon
filter: brightness(0) invert(1)
.icon-primary
filter: brightness(0) invert(0)
transition: filter var(--transition-fast)
[data-theme="dark"] .icon-primary
filter: brightness(0) invert(1)
// Утилиты для контрастного текста
.text-contrast-high
color: var(--text-primary)
.text-contrast-medium
color: var(--text-secondary)
.text-contrast-low
color: var(--text-muted)
// Утилиты для фонов
.bg-surface
background-color: var(--bg-card)
.bg-surface-alt
background-color: var(--bg-secondary)
// FILE: .gitignore // TYPE: Unknown // SIZE: 29 characters
html.json
pug.json
styl.json
// FILE: app/app.coffee // TYPE: CoffeeScript // SIZE: 3584 characters
# Подключение мета информации
document.head.insertAdjacentHTML 'beforeend', '<meta charset="UTF-8">'
document.head.insertAdjacentHTML 'beforeend', '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
# Настройка tailwind
#tailwind.config = require 'tailwind.config.js'
# Подключение основных стилей
document.head.insertAdjacentHTML('beforeend', '<style type="text/tailwindcss" file="main.css">' + stylFns['main.css'] + '</style>')
document.head.insertAdjacentHTML('beforeend', '<style type="text/tailwindcss" file="app/DesignTokens.styl">' + stylFns['app/DesignTokens.styl'] + '</style>')
document.head.insertAdjacentHTML('beforeend', '<style type="text/tailwindcss" file="app/app.styl">' + stylFns['app/app.styl'] + '</style>')
# Маршруты
routes = [
{ path: '/', component: require 'app/pages/Home' }
{ path: '/:path*', component: require 'app/pages/DocumentPage' }
]
globalThis._ = {}
# Глобальное определение vuejs приложения
globalThis.app = Vue.createApp
name: 'app'
data: () ->
return {
appState:
currentDocument: null
currentLanguage: 'ru'
availableLanguages: ['ru', 'en', 'tj']
loading: true
error: null
dbService: new (require('app/core/DB'))()
}
beforeMount: ->
globalThis.AppDB = new (require 'app/utils/AppDB')()
await globalThis.AppDB.init()
globalThis._ = @
computed:
currentLanguage: ->
@appState.currentLanguage
watch:
currentLanguage:
handler: (newDoc) ->
debug.dir newDoc
@loadDocumentForPath(window.location.pathname)
immediate: true
methods:
initializeApp: ->
# Определяем язык из URL или браузера
@detectLanguage()
# Загружаем документ для текущего пути
@loadDocumentForPath(window.location.pathname)
detectLanguage: ->
# Простая логика определения языка
pathLang = window.location.pathname.split('/')[1]
if pathLang in @appState.availableLanguages
@appState.currentLanguage = pathLang
else
browserLang = navigator.language.split('-')[0]
if browserLang in @appState.availableLanguages
@appState.currentLanguage = browserLang
loadDocumentForPath: (path) ->
try
@appState.loading = true
doc = await AppDB.getDocumentByPath(path, AppDB.currentLanguage)
@appState.currentDocument = doc
@appState.loading = false
# Устанавливаем заголовок страницы
if doc?.title
document.head.insertAdjacentHTML('beforeend', '<title>' + doc.title + '</title>')
catch error
@appState.error = "Ошибка загрузки документа: " + error
@appState.loading = false
render: (new Function '_ctx', '_cache', renderFns['app/app.pug'])()
components: {
'hero-section': require('shared/HeroSection')
'image-gallery': require('shared/ImageGallery')
}
# Создаем и настраиваем роутер
router = VueRouter.createRouter({
routes: routes
history: VueRouter.createWebHistory()
scrollBehavior: (to, from, savedPosition) ->
if savedPosition
return savedPosition
else
return { x: 0, y: 0 }
})
app.use(router)
app.mount('body')
// FILE: app/app.pug // TYPE: Pug Template // SIZE: 699 characters
include ../pug/base.pug
include ../pug/bem.pug
div(class="min-h-screen bg-white dark:bg-gray-900 transition-colors duration-300")
div(v-if="appState.loading" class="flex items-center justify-center min-h-screen")
div(class="text-center")
div(class="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600")
p(class="mt-4 text-gray-600 dark:text-gray-400") Загрузка...
div(v-else-if="appState.error" class="flex items-center justify-center min-h-screen")
div(class="text-center")
div(class="text-red-600 text-xl") Ошибка
p(class="text-gray-600 dark:text-gray-400") {{ appState.error }}
router-view(v-else)
// FILE: app/app.styl // TYPE: Stylus // SIZE: 1013 characters
// Переменные темы
:root
--primary-color: #3b82f6
--secondary-color: #1e40af
--text-primary: #1f2937
--text-secondary: #6b7280
--bg-primary: #ffffff
--bg-secondary: #f9fafb
[data-theme="dark"]
--primary-color: #60a5fa
--secondary-color: #3b82f6
--text-primary: #f9fafb
--text-secondary: #d1d5db
--bg-primary: #111827
--bg-secondary: #1f2937
// Базовые стили
body
font-family: 'Inter', system-ui, -apple-system, sans-serif
color: var(--text-primary)
background-color: var(--bg-primary)
transition: all 0.3s ease
// Стили для Markdown контента
.prose
h1, h2, h3, h4, h5, h6
color: var(--text-primary)
font-weight: 600
p
color: var(--text-secondary)
line-height: 1.7
a
color: var(--primary-color)
text-decoration: none
&:hover
text-decoration: underline
img
border-radius: 0.5rem
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1)
// FILE: app/layout.pug // TYPE: Pug Template // SIZE: 324 characters
include ../pug/base.pug
include ../pug/bem.pug
+mbh
header(class="sticky top-0 z-50 glass border-b border-neutral-300/60 transition-all duration-300")
div(class="navbar max-w-7xl mx-auto px-4 sm:px-6 lg:px-8")
block top-content
+mbl(class="py-8 md:py-12 lg:py-16")
.container
block content
// FILE: app/pages/Home.coffee // TYPE: CoffeeScript // SIZE: 697 characters
document.head.insertAdjacentHTML 'beforeend', '<style type="text/css" file="app/pages/Home.styl">'+stylFns['app/pages/Home.styl']+'</style>'
module.exports =
name: 'Home'
render: (new Function '_ctx', '_cache', renderFns['app/pages/Home.pug'])()
data: ->
return {
_: _
document: null
}
beforeMount: ->
try
@document = await AppDB.getDocumentByPath('/', AppDB.currentLanguage)
catch e
debug.log "Document load error:", e
components:
'app-link': require 'app/shared/AppLink'
'hero-section': require 'app/shared/HeroSection'
'image-gallery': require 'app/shared/ImageGallery'
// FILE: app/pages/Home.pug // TYPE: Pug Template // SIZE: 845 characters
include ../../pug/base.pug
include ../../pug/bem.pug
extends ../layout.pug
block top-content
hero-section(:document="document")
block content
div(class="max-w-4xl mx-auto px-4" v-if="document")
h1(class="text-3xl font-bold text-contrast-high animate-fade-in-up") {{ document.translations[_.currentLanguage]?.title || document.translations.en.title }}
p(class="text-xl text-contrast-medium mb-8 animate-fade-in-up") {{ document.translations[_.currentLanguage]?.subtitle || document.translations.en.subtitle }}
div(class="prose mt-6 animate-fade-in-up" v-html="marked.parse(document.translations[_.currentLanguage]?.content || document.translations.en.content)")
div(class="mt-12")
image-gallery(:images="document.translations[_.currentLanguage]?.gallery || document.translations.en.gallery")
// FILE: app/pages/Home.styl // TYPE: Stylus // SIZE: 281 characters
// Используем только утилиты из DesignTokens
.animate-fade-in-up
animation: fadeInUp var(--transition-slow) ease-out
@keyframes fadeInUp
from
opacity: 0
transform: translateY(var(--transition-px))
to
opacity: 1
transform: translateY(0)
// FILE: app/shared/AppLink.coffee // TYPE: CoffeeScript // SIZE: 633 characters
document.head.insertAdjacentHTML('beforeend','<style type="text/css" file="app/shared/AppLink.styl">'+stylFns['app/shared/AppLink.styl']+'</style>')
module.exports =
default:
render: (new Function '_ctx', '_cache', renderFns['app/shared/AppLink.pug'])()
name: 'AppLink'
props:
to:
type: [String, Object]
required: true
computed:
isExternal: ->
if typeof @.to == 'string'
return @.to.startsWith('http')
return false
data: ->
return {
_: _
}
// FILE: app/shared/AppLink.pug // TYPE: Pug Template // SIZE: 214 characters
include ../../pug/base.pug
include ../../pug/bem.pug
a.app-link(v-if="isExternal" v-bind="$attrs" :href="to" target="_blank" rel="noopener")
slot
router-link.app-link(v-else v-bind="$attrs" :to="to")
slot
// FILE: app/shared/AppLink.styl // TYPE: Stylus // SIZE: 1264 characters
@import '../DesignTokens.styl'
// Базовые стили для AppLink компонента
.app-link
color: var(--primary-color)
text-decoration: none
transition: color var(--transition-fast)
.app-link:hover
color: var(--primary-dark)
text-decoration: underline
.app-link.router-link-active
font-weight: var(--font-semibold)
color: var(--primary-dark)
// Стили для внешних ссылок
.app-link-external::after
content: " ↗"
font-size: 0.875em
opacity: 0.7
// Стили для кнопко-подобных ссылок
.app-link-button
display: inline-flex
align-items: center
gap: var(--space-2)
padding: var(--space-2) var(--space-4)
background: var(--primary-color)
color: white
border-radius: 6px
font-weight: var(--font-medium)
transition: all var(--transition-fast)
.app-link-button:hover
background: var(--primary-dark)
transform: translateY(-1px)
text-decoration: none
color: white
box-shadow: var(--shadow-md)
// Стили для иконок в ссылках
.app-link-with-icon
display: inline-flex
align-items: center
gap: var(--space-2)
.app-link-icon
width: 16px
height: 16px
transition: transform var(--transition-fast)
.app-link:hover .app-link-icon
transform: translateX(2px)
// FILE: app/shared/HeroSection.coffee // TYPE: CoffeeScript // SIZE: 311 characters
document.head.insertAdjacentHTML 'beforeend', '<style type="text/css" file="app/shared/HeroSection.styl">'+stylFns['app/shared/HeroSection.styl']+'</style>'
module.exports =
name: 'HeroSection'
props: [ 'document' ]
render: (new Function '_ctx', '_cache', renderFns['app/shared/HeroSection.pug'])()
// FILE: app/shared/HeroSection.pug // TYPE: Pug Template // SIZE: 574 characters
include ../../pug/base.pug
include ../../pug/bem.pug
section(v-if="document" class="hero-section bg-gradient")
div(class="max-w-7xl mx-auto px-4 py-24 text-center text-white")
h1(v-if="document.translations[_.appState.currentLanguage]" class="text-5xl font-bold mb-6") {{ document.translations[_.appState.currentLanguage].title || document.translations.en.title }}
p(v-if="document.translations[_.appState.currentLanguage]" class="text-xl opacity-90") {{ document.translations[_.appState.currentLanguage].subtitle || document.translations.en.subtitle }}
// FILE: app/shared/HeroSection.styl // TYPE: Stylus // SIZE: 125 characters
.hero-section
background: var(--gradient-primary)
color: white
padding: var(--space-16) 0
text-align: center
// FILE: app/shared/ImageGallery.coffee // TYPE: CoffeeScript // SIZE: 367 characters
document.head.insertAdjacentHTML 'beforeend', '<style type="text/css" file="app/shared/ImageGallery.styl">'+stylFns['app/shared/ImageGallery.styl']+'</style>'
module.exports =
name: 'ImageGallery'
props:
images:
type: Array
default: -> []
render: (new Function '_ctx', '_cache', renderFns['app/shared/ImageGallery.pug'])()
// FILE: app/shared/ImageGallery.pug // TYPE: Pug Template // SIZE: 414 characters
include ../../pug/base.pug
include ../../pug/bem.pug
div(v-if="images && images.length > 0" class="image-gallery grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6")
div(v-for="(img, idx) in images" :key="idx" class="overflow-hidden rounded-lg shadow-custom-md bg-surface")
img(:src="img.src" :alt="img.alt || ''" class="w-full h-auto object-cover transition-transform duration-300 hover:scale-105")
// FILE: app/shared/ImageGallery.styl // TYPE: Stylus // SIZE: 324 characters
.image-gallery
display: grid
gap: var(--space-6)
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))
.image-gallery img
width: 100%
height: auto
object-fit: cover
border-radius: 8px
transition: transform var(--transition-normal)
.image-gallery img:hover
transform: scale(1.05)
// FILE: app/utils/AppDB.coffee // TYPE: CoffeeScript // SIZE: 4684 characters
# FILE: app/utils/AppDB.coffee
class AppDB
currentLanguage: 'ru'
currentProject: 's5l.ru'
designDocVersion: '1.1' # управление версией дизайна
constructor: ->
@localDB = new PouchDB('s5l_local')
@remoteDB = new PouchDB('https://oleg:631074@couchdb.favt.ru.net/s5lru/')
@syncHandler = null
init: ->
try
await @remoteDB.info()
debug.log "Remote DB connected"
await @ensureDesignDocs()
await @ensureDefaultContent()
@startSync()
AppDB.currentLanguage = globalThis._?.appState?.currentLanguage or 'ru'
catch e
debug.log "DB init failed:", e
startSync: ->
@syncHandler = @localDB.sync(@remoteDB, { live: true, retry: true })
.on 'error', (err) -> debug.log "Sync error:", err
ensureDesignDocs: ->
adminDoc =
_id: '_design/admin'
version: @designDocVersion
views:
byPath:
map: (doc) ->
if doc.type == 'page'
emit [doc.domain, doc.path], doc
.toString()
byType:
map: (doc) ->
emit doc.type, doc
.toString()
try
existing = await @remoteDB.get('_design/admin')
if existing.version != @designDocVersion
adminDoc._rev = existing._rev
await @remoteDB.put(adminDoc)
debug.log "Design doc updated to v#{@designDocVersion}"
catch
await @remoteDB.put(adminDoc)
debug.log "Design doc created v#{@designDocVersion}"
ensureDefaultContent: ->
defaultHome =
_id: 'page::s5l.ru::/'
type: 'page'
domain: 's5l.ru'
path: '/'
translations:
ru:
title: "s5l.ru — мультиязычная offline-first платформа"
subtitle: "Разрабатывайте быстро, работайте везде"
content: '''
# Добро пожаловать на s5l.ru
**s5l.ru** — это платформа для быстрого запуска веб-проектов с поддержкой:
- offline-first через PouchDB/CouchDB
- автоматического переключения языка
- динамической подгрузки контента
- полной WCAG 2.2-совместимости
Все тексты хранятся в базе и легко редактируются через админку.
'''
gallery: [
{ src: "/assets/hero-s5l.svg", alt: "Hero illustration" }
]
en:
title: "s5l.ru — multilingual offline-first platform"
subtitle: "Build fast, work anywhere"
content: '''
# Welcome to s5l.ru
**s5l.ru** is a platform for rapid web project launches with:
- offline-first via PouchDB/CouchDB
- automatic language switching
- dynamic content loading
- full WCAG 2.2 compliance
All text is stored in the database and editable via admin panel.
'''
gallery: [
{ src: "/assets/hero-s5l.svg", alt: "Hero illustration" }
]
tj:
title: "s5l.ru — платформаи бисёрзабон ва аввал офлайн"
subtitle: "Бисёр тез бунёд кунед, дар ҳама ҷо кор кунед"
content: '''
# Ба s5l.ru хуш омадед
**s5l.ru** — ин платформа барои оғози тези лоиҳаҳои веб аст бо:
- офлайн-аввал аз рӯи PouchDB/CouchDB
- ивази худкори забон
- боркунии динамикӣ
- мутобиқати пурраи WCAG 2.2
Ҳамаи матнҳо дар база нигоҳ дошта мешаванд ва аз ҷониби панели маъмури озодона таҳрир карда мешаванд.
'''
gallery: [
{ src: "/assets/hero-s5l.svg", alt: "Hero illustration" }
]
try
await @remoteDB.get('page::s5l.ru::/')
catch
await @remoteDB.put(defaultHome)
debug.log "Default home page created"
getDocumentByPath: (path, lang = @currentLanguage) ->
path = path or '/'
try
result = await @localDB.query('admin/byPath', { key: [@currentProject, path], include_docs: true })
if result.rows.length > 0
doc = result.rows[0].doc
return doc
else
throw new Error "Document not found"
catch e
debug.log "Fallback for path:", path, "lang:", lang
# fallback to English if not found
if lang != 'en'
return await @getDocumentByPath(path, 'en')
else
throw new Error "Document not available even in English"
module.exports = AppDB
// FILE: doc.coffee // TYPE: CoffeeScript // SIZE: 1285 characters
module.exports =
version: "0.0.1"
adres: [
{
"adr": "https://cdn.tailwindcss.com/3.4.17"
"eventName": "tailwindReady"
"obj": "tailwind"
}
{
"adr": "https://unpkg.com/pouchdb/dist/pouchdb.min.js"
"eventName": "puochReady"
"obj": "PouchDB"
}
{
"adr": "https://unpkg.com/vue@3/dist/vue.runtime.global.prod.js"
"eventName": "vueReady"
"obj": "Vue"
}
{
"adr": "https://unpkg.com/vue-router@4/dist/vue-router.global.js"
"eventName": "VueRouterReady"
"obj": "VueRouter"
}
{
"adr": "https://cdn.jsdelivr.net/npm/@vue/compiler-sfc@3.5.22/dist/compiler-sfc.esm-browser.min.js"
"eventName": "compileTemplateReady"
"obj": "compileTemplate"
}
{
"adr": "https://cdn.jsdelivr.net/npm/marked@16.4.1/lib/marked.umd.min.js"
"eventName": "markedReady"
"obj": "marked"
}
{
"adr": "https://cdn.jsdelivr.net/npm/openlayers@4.6.5/dist/ol.min.js"
"css": "https://cdn.jsdelivr.net/npm/openlayers@4.6.5/dist/ol.min.css"
"eventName": "leafletReady"
"obj": "ol"
}
]
// FILE: lzma.coffee // TYPE: CoffeeScript // SIZE: 1010 characters
globalThis.debug = require('debug.coffee').default
require('headVue.coffee')
document.documentElement.classList.add('dark')
globalThis.initCount = 0
ic = ()->
if initCount > 5
window.location.reload()
else if not globalThis['appReady']
initCount++
#setTimeout ic, 200
ic()
# обязательно подключение глобальных массивов
globalThis.renderFns = require 'pug.json'
globalThis.stylFns = require 'styl.json'
try
init = (event={})->
debug.dir globalThis['vueReady']
debug.log "Init Start"
if not globalThis['appReady'] and globalThis['vueReady'] and globalThis['puochReady']
debug.log 'init start ok'
try
require('app/app.coffee')
globalThis['appReady'] = true
debug.log "init is ok"
catch err
debug.dir err
else if not globalThis['appReady']
debug.log 'pausedEvent appReady'
setTimeout init, 200
init()
// FILE: pug/base.pug // TYPE: Pug Template // SIZE: 504 characters
include ./bem.pug
mixin mbh
- var otherClasses = attributes.class || ''
+b('hfn')(class="h-max overflow-hidden relative")
+e('hfn','fon')
mixin mbl
- var otherClasses = attributes.class || ''
+b('hfn')(class="h-max overflow-hidden relative")
+e('hfn','fon')(class="[background:url('https://jahonnamo.s5l.ru/assets/jahonnamo.s5l.ru/bkg00.webp')_0_0_/_cover_no-repeat] bg-[#ffffff] absolute h-full w-full z-[-1]")
if block
block
// FILE: pug/bem.pug // TYPE: Pug Template // SIZE: 1035 characters
//- bem.pug
//- Базовый миксин для блоков и элементов
mixin b(blockName, elemName)
- var className = blockName
- var twClasses = attributes.tw || ''
- var otherClasses = attributes.class || ''
if elemName
- className += '__' + elemName
//- Обработка модификаторов
if attributes.mod
- className += ' ' + blockName + '--' + attributes.mod
if attributes.mods
each mod in attributes.mods.split(',')
- className += ' ' + blockName + '--' + mod.trim()
//- Собираем финальный класс
- var finalClass = [className, twClasses, otherClasses].filter(Boolean).join(' ')
//- Удаляем обработанные атрибуты
- attributes.tw = null
- attributes.mod = null
- attributes.mods = null
- attributes.class = null
//- Генерация элемента
if block
div(class=finalClass)&attributes(attributes)
block
else
div(class=finalClass)&attributes(attributes)
//- Миксин для элементов (альтернативный синтаксис)
mixin e(blockName, elemName)
+b(blockName, elemName)&attributes(attributes)
block