# PROJECT ANALYSIS REPORT # Generated: 2025-11-05T09:26:28.189Z # Project: template ## PROJECT SUMMARY - Total Files: 24 - Total Size: 33266 characters - Main Components: 0 - Routes: 0 - Priority Documentation: 2 - Work Files: 0 - Priority Configs: 0 ## PRIORITY DOCUMENTATION (Highest Priority) - README.md (Markdown, 7142 chars) - app/DesignTokens.styl (Stylus, 5908 chars) ## CURRENT WORK FILES No files in this category ## PRIORITY CONFIGURATION FILES No files in this category ## FILE TYPE DISTRIBUTION - Markdown: 1 files - Stylus: 6 files - Unknown: 1 files - CoffeeScript: 8 files - Pug Template: 8 files ## PROJECT STRUCTURE ### PRIORITY DOCUMENTATION #### README.md (Markdown, 7142 chars) ```markdown # Текущее задание (Выполнить) ## Общее задание: Разработать веб-приложение [название], используя платформу **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', '' ``` - **Шаблон**: рендерить через ```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','' 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'` — всё глобально Следуй этому промту для всех новых проектов. --- ``` #### app/DesignTokens.styl (Stylus, 5908 chars) ```stylus :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) ``` ### OTHER FILES #### .gitignore (Unknown, 29 chars) ```text html.json pug.json styl.json ``` #### app/app.coffee (CoffeeScript, 3584 chars) ```coffeescript # Подключение мета информации document.head.insertAdjacentHTML 'beforeend', '' document.head.insertAdjacentHTML 'beforeend', '' # Настройка tailwind #tailwind.config = require 'tailwind.config.js' # Подключение основных стилей document.head.insertAdjacentHTML('beforeend', '') document.head.insertAdjacentHTML('beforeend', '') document.head.insertAdjacentHTML('beforeend', '') # Маршруты 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', '' + doc.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') ``` #### app/app.pug (Pug Template, 699 chars) ```pug 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) ``` #### app/app.styl (Stylus, 1013 chars) ```stylus // Переменные темы :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) ``` #### app/layout.pug (Pug Template, 324 chars) ```pug 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 ``` #### app/pages/Home.coffee (CoffeeScript, 697 chars) ```coffeescript document.head.insertAdjacentHTML 'beforeend', '' 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' ``` #### app/pages/Home.pug (Pug Template, 845 chars) ```pug 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") ``` #### app/pages/Home.styl (Stylus, 281 chars) ```stylus // Используем только утилиты из 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) ``` #### app/shared/AppLink.coffee (CoffeeScript, 633 chars) ```coffeescript document.head.insertAdjacentHTML('beforeend','') 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 { _: _ } ``` #### app/shared/AppLink.pug (Pug Template, 214 chars) ```pug 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 ``` #### app/shared/AppLink.styl (Stylus, 1264 chars) ```stylus @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) ``` #### app/shared/HeroSection.coffee (CoffeeScript, 311 chars) ```coffeescript document.head.insertAdjacentHTML 'beforeend', '' module.exports = name: 'HeroSection' props: [ 'document' ] render: (new Function '_ctx', '_cache', renderFns['app/shared/HeroSection.pug'])() ``` #### app/shared/HeroSection.pug (Pug Template, 574 chars) ```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 }} ``` #### app/shared/HeroSection.styl (Stylus, 125 chars) ```stylus .hero-section background: var(--gradient-primary) color: white padding: var(--space-16) 0 text-align: center ``` #### app/shared/ImageGallery.coffee (CoffeeScript, 367 chars) ```coffeescript document.head.insertAdjacentHTML 'beforeend', '' module.exports = name: 'ImageGallery' props: images: type: Array default: -> [] render: (new Function '_ctx', '_cache', renderFns['app/shared/ImageGallery.pug'])() ``` #### app/shared/ImageGallery.pug (Pug Template, 414 chars) ```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") ``` #### app/shared/ImageGallery.styl (Stylus, 324 chars) ```stylus .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) ``` #### app/utils/AppDB.coffee (CoffeeScript, 4684 chars) ```coffeescript # 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 ``` #### doc.coffee (CoffeeScript, 1285 chars) ```coffeescript 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" } ] ``` #### lzma.coffee (CoffeeScript, 1010 chars) ```coffeescript 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() ``` #### pug/base.pug (Pug Template, 504 chars) ```pug 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 ``` #### pug/bem.pug (Pug Template, 1035 chars) ```pug //- 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 ``` ## MAIN COMPONENTS ## ROUTES ## DEPENDENCIES ## FULL SOURCE CODE ## PRIORITY DOCUMENTATION SOURCE CODE // FILE: README.md // TYPE: Markdown // SIZE: 7142 characters // CATEGORY: PRIORITY DOCUMENTATION ```markdown # Текущее задание (Выполнить) ## Общее задание: Разработать веб-приложение [название], используя платформу **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', '' ``` - **Шаблон**: рендерить через ```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','' 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 ```stylus :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) ``` ## OTHER FILES SOURCE CODE // FILE: .gitignore // TYPE: Unknown // SIZE: 29 characters ```text html.json pug.json styl.json ``` // FILE: app/app.coffee // TYPE: CoffeeScript // SIZE: 3584 characters ```coffeescript # Подключение мета информации document.head.insertAdjacentHTML 'beforeend', '' document.head.insertAdjacentHTML 'beforeend', '' # Настройка tailwind #tailwind.config = require 'tailwind.config.js' # Подключение основных стилей document.head.insertAdjacentHTML('beforeend', '') document.head.insertAdjacentHTML('beforeend', '') document.head.insertAdjacentHTML('beforeend', '') # Маршруты 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', '' + doc.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 ```pug 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 ```stylus // Переменные темы :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 ```pug 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 ```coffeescript document.head.insertAdjacentHTML 'beforeend', '' 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 ```pug 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 ```stylus // Используем только утилиты из 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 ```coffeescript document.head.insertAdjacentHTML('beforeend','') 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 ```pug 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 ```stylus @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 ```coffeescript document.head.insertAdjacentHTML 'beforeend', '' 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 ```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 }} ``` // FILE: app/shared/HeroSection.styl // TYPE: Stylus // SIZE: 125 characters ```stylus .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 ```coffeescript document.head.insertAdjacentHTML 'beforeend', '' 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 ```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") ``` // FILE: app/shared/ImageGallery.styl // TYPE: Stylus // SIZE: 324 characters ```stylus .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 ```coffeescript # 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 ```coffeescript 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 ```coffeescript 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 ```pug 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 ```pug //- 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 ```