|
@@ -0,0 +1,2403 @@
|
|
|
|
|
+# 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 # Основной шаблон с Хедером, <router-view>, Футером
|
|
|
|
|
+├── 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', '<style type="text/tailwindcss">'+stylFns['app/shared/NewsList.styl']+'</style>'
|
|
|
|
|
+ ```
|
|
|
|
|
+- **Шаблон**: рендерить через
|
|
|
|
|
+ ```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','<style type="text/tailwindcss">'+stylFns['app/shared/NewsList.styl']+'</style>'
|
|
|
|
|
+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', '<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')
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 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', '<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'
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 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','<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 {
|
|
|
|
|
+ _: _
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 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', '<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'])()
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 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', '<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'])()
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### 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 # Основной шаблон с Хедером, <router-view>, Футером
|
|
|
|
|
+├── 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', '<style type="text/tailwindcss">'+stylFns['app/shared/NewsList.styl']+'</style>'
|
|
|
|
|
+ ```
|
|
|
|
|
+- **Шаблон**: рендерить через
|
|
|
|
|
+ ```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','<style type="text/tailwindcss">'+stylFns['app/shared/NewsList.styl']+'</style>'
|
|
|
|
|
+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', '<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
|
|
|
|
|
+```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', '<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
|
|
|
|
|
+```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','<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
|
|
|
|
|
+```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', '<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
|
|
|
|
|
+```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', '<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
|
|
|
|
|
+```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
|
|
|
|
|
+
|
|
|
|
|
+```
|