templates lab updates

This commit is contained in:
Alex 2026-04-12 21:03:18 +03:00
parent 5d1bf967ef
commit 21c6880005
535 changed files with 105172 additions and 1 deletions

28
.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
*~
node_modules
bower_components
.idea/
.idea
.build
build/
_build
*.zip
*.iml
modules.xml
vcs.xml
workspace.xml
cert.pem
key.pem
.vs/*
.history
.claude
.DS_Store
.playwright-mcp/
*.png
.venv/
__pycache__/
.env
*.pth
uploads/
results/

173
modernkit/README.md Normal file
View File

@ -0,0 +1,173 @@
# ModernKit Project
ModernKit is a versatile and performant project boilerplate built with Gulp, Twig, and SCSS. It provides a robust build system and a foundational UI kit to kickstart your web development.
## Features
- **Build System:**
- Gulp-powered task runner.
- SCSS compilation with Autoprefixer and minification (for production).
- JavaScript transpilation (Babel) and concatenation with separate development (unminified) and production (minified) builds.
- Image optimization (including WebP generation for production).
- Twig templating with dynamic data injection from JSON files (only top-level templates are processed into HTML files).
- Asset copying (e.g., other static files).
- Favicon copying.
- Font copying.
- SVG sprite generation and injection into HTML.
- Resource versioning (cache busting) using content hashing (for production).
- Source map generation for CSS and JavaScript (for development).
- **Code Quality:** Biome integration for JavaScript linting and formatting.
- Robust error handling with `gulp-plumber`.
- **Performance Optimization:** Utilizes `gulp-cached` and `gulp-remember` to speed up recompilation of unchanged files.
- Clean task to remove build artifacts.
- **UI Kit:**
- Basic styles and resets.
- Comprehensive typography.
- Responsive grid system.
- Utility classes for spacing, text alignment, display, and colors.
- Basic components (e.g., buttons).
## Getting Started
### Prerequisites
Make sure you have Node.js and npm installed on your system.
### Installation
1. Clone the repository:
```bash
git clone <your-repository-url>
cd modernkit
```
2. Install the dependencies:
```bash
npm install
```
## Available Commands
- `npm start`: Runs the default Gulp task, which starts a development server with BrowserSync, watches for file changes, and recompiles assets automatically. This uses the development JavaScript build (unminified) and generates source maps.
- `npm run build`: Performs a one-time development build. Cleans the `dist` directory, lints JavaScript, generates SVG sprites, compiles Twig templates, SCSS, JavaScript (development version), optimizes images, and copies assets, favicon, and fonts.
- `npm run build:prod`: Performs a one-time production build. Similar to `npm run build`, but uses minified CSS and JavaScript (production version), optimizes images more aggressively, generates WebP versions of images, and applies content hashing for cache busting.
- `npm run test`: Runs Jest tests.
- `npm run lint`: Runs Biome check on JavaScript files.
- `npm run format`: Runs Biome formatter on JavaScript files.
## Project Structure
```
modernkit/
├───gulpfile.js
├───jest.config.js
├───package-lock.json
├───package.json
├───biome.json
├───dist/ # Compiled output (ignored by Git)
├───node_modules/ # Node.js dependencies (ignored by Git)
└───src/
├───images/ # Source images
│ └───test.png
├───js/ # Source JavaScript files
│ ├───main.js
│ └───main.test.js
├───scss/ # Source SCSS files
│ ├───main.scss
│ ├───_variables.scss
│ ├───_base.scss
│ ├───_typography.scss
│ ├───_grid.scss
│ ├───_utilities.scss
│ └───_components.scss
├───templates/ # Twig templates
│ ├───index.twig
│ ├───about.twig
│ └───components/ # Twig components
│ ├───button.twig
│ ├───footer.twig
│ └───header.twig
├───data/ # JSON data for Twig templates
│ ├───global.json
│ └───about.json
├───assets/ # Static assets (e.g., other files)
│ └───test.txt
├───icons/ # SVG icons for sprite generation
│ ├───clock.svg
│ └───info.svg
└───fonts/ # Font files
└───test.ttf
```
## UI Kit Usage
### Typography
Use standard HTML heading tags (`<h1>` to `<h6>`) and paragraph tags (`<p>`). Utility classes like `.small` can be applied for smaller text.
### Grid System
Utilize the `.container`, `.row`, and `.col-*` classes for responsive layouts. Breakpoints are defined in `src/scss/_variables.scss`.
```html
<div class="container">
<div class="row">
<div class="col-12 col-md-6">
<!-- Content -->
</div>
<div class="col-12 col-md-6">
<!-- Content -->
</div>
</div>
</div>
```
### Buttons
Apply the `.btn` base class along with contextual classes like `.btn-primary`, `.btn-secondary`, etc.
```html
<button class="btn btn-primary">Primary Button</button>
<a href="#" class="btn btn-success">Success Link</a>
```
### Utility Classes
Various utility classes are available for common styling needs:
- **Spacing:** `m-1`, `pt-3`, `px-auto`, etc. (for margin and padding)
- **Text Alignment:** `text-left`, `text-center`, `text-right`
- **Display:** `d-block`, `d-inline-block`, `d-flex`
- **Colors:** `text-primary`, `bg-dark`, etc.
### SVG Icons
SVG icons from `src/icons` are compiled into a sprite and injected into your HTML. Use them with the `<svg><use>` pattern:
```html
<svg class="icon" width="24" height="24"><use xlink:href="#clock"></use></svg>
```
## Data Management
Twig templates can access global data from `src/data/global.json` and page-specific data from `src/data/<template-name>.json` (e.g., `src/data/about.json` for `about.twig`). Page-specific data will override global data if keys conflict.
Example in Twig:
```twig
<h1>{{ global.welcomeMessage }}</h1>
<p>{{ pageTitle }}</p>
```
## Testing
Jest is configured to run unit tests for JavaScript files. The test environment is set to `jsdom` to simulate a browser environment.
To run tests:
```bash
npm test
```
## Contributing
Feel free to contribute to this project by submitting issues or pull requests.

34
modernkit/dist/about.html vendored Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About Us - </title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div style="display: none;"></div>
<header class="bg-dark text-white py-3">
<div class="container d-flex justify-content-between align-items-center">
<a href="#" class="text-white h4 mb-0">ModernKit</a>
<nav>
<ul class="list-unstyled d-flex mb-0">
<li class="ml-3"><a href="#" class="text-white">Home</a></li>
<li class="ml-3"><a href="#" class="text-white">About</a></li>
<li class="ml-3"><a href="#" class="text-white">Contact</a></li>
</ul>
</nav>
</div>
</header>
<main class="container py-5">
<h1 class="mb-4">About Us</h1>
<p>This is the content for the about page. We are a team dedicated to building amazing things.</p>
</main>
<footer class="bg-dark text-white py-3 mt-5">
<div class="container text-center">
<p class="mb-0">&copy; 2025 ModernKit. All rights reserved.</p>
</div>
</footer> <script src="main.js"></script>
</body>
</html>

32
modernkit/dist/base.html vendored Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div style="display: none;"></div>
<header class="bg-dark text-white py-3">
<div class="container d-flex justify-content-between align-items-center">
<a href="#" class="text-white h4 mb-0">ModernKit</a>
<nav>
<ul class="list-unstyled d-flex mb-0">
<li class="ml-3"><a href="#" class="text-white">Home</a></li>
<li class="ml-3"><a href="#" class="text-white">About</a></li>
<li class="ml-3"><a href="#" class="text-white">Contact</a></li>
</ul>
</nav>
</div>
</header>
<main class="container py-5">
</main>
<footer class="bg-dark text-white py-3 mt-5">
<div class="container text-center">
<p class="mb-0">&copy; 2025 ModernKit. All rights reserved.</p>
</div>
</footer> <script src="js/main.js"></script>
</body>
</html>

106
modernkit/dist/components-showcase.html vendored Normal file
View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div style="display: none;"></div>
<header class="bg-dark text-white py-3">
<div class="container d-flex justify-content-between align-items-center">
<a href="#" class="text-white h4 mb-0">ModernKit</a>
<nav>
<ul class="list-unstyled d-flex mb-0">
<li class="ml-3"><a href="#" class="text-white">Home</a></li>
<li class="ml-3"><a href="#" class="text-white">About</a></li>
<li class="ml-3"><a href="#" class="text-white">Contact</a></li>
</ul>
</nav>
</div>
</header>
<main class="container py-5">
<h1>UI Components Showcase</h1>
<section>
<h2>Tabs</h2>
<div class="tabs">
<div class="tab-headers">
<button class="tab-button active" data-tab="tab1">Tab 1</button>
<button class="tab-button" data-tab="tab2">Tab 2</button>
<button class="tab-button" data-tab="tab3">Tab 3</button>
</div>
<div class="tab-content">
<div id="tab1" class="tab-pane active">
<h3>Content for Tab 1</h3>
<p>This is the content for the first tab. It can contain any HTML elements.</p>
</div>
<div id="tab2" class="tab-pane">
<h3>Content for Tab 2</h3>
<p>This is the content for the second tab. More information here.</p>
</div>
<div id="tab3" class="tab-pane">
<h3>Content for Tab 3</h3>
<p>This is the content for the third tab. Even more details.</p>
</div>
</div>
</div>
</section>
<section>
<h2>Slider (Swiper)</h2>
<!-- Slider main container -->
<div class="swiper">
<!-- Additional required wrapper -->
<div class="swiper-wrapper">
<!-- Slides -->
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
</div>
<!-- If we need pagination -->
<div class="swiper-pagination"></div>
<!-- If we need navigation buttons -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- If we need scrollbar -->
<div class="swiper-scrollbar"></div>
</div>
</section>
<section>
<h2>Modal (Micromodal)</h2>
<button data-micromodal-trigger="modal-1">Open Modal</button>
<div class="modal micromodal-slide" id="modal-1" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
<header class="modal__header">
<h2 class="modal__title" id="modal-1-title">
Micromodal Example
</h2>
<button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
</header>
<div class="modal__content" id="modal-1-content">
<p>This is a simple modal window powered by Micromodal.</p>
<p>You can put any content here.</p>
</div>
<footer class="modal__footer">
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Close</button>
</footer>
</div>
</div>
</div>
</section>
</main>
<footer class="bg-dark text-white py-3 mt-5">
<div class="container text-center">
<p class="mb-0">&copy; 2025 ModernKit. All rights reserved.</p>
</div>
</footer> <script src="js/main.js"></script>
</body>
</html>

5
modernkit/dist/icons.svg vendored Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol id="clock" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM12.5 7H11V13L16.25 16.15L17 14.92L12.5 12.25V7Z"/>
</symbol><symbol id="info" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z"/>
</symbol></svg>

After

Width:  |  Height:  |  Size: 666 B

131
modernkit/dist/index.html vendored Normal file
View File

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div style="display: none;"></div>
<header class="bg-dark text-white py-3">
<div class="container d-flex justify-content-between align-items-center">
<a href="#" class="text-white h4 mb-0">ModernKit</a>
<nav>
<ul class="list-unstyled d-flex mb-0">
<li class="ml-3"><a href="#" class="text-white">Home</a></li>
<li class="ml-3"><a href="#" class="text-white">About</a></li>
<li class="ml-3"><a href="#" class="text-white">Contact</a></li>
</ul>
</nav>
</div>
</header>
<main class="container py-5">
<h1 class="mb-4"></h1>
<section class="mb-5">
<h2 class="mb-3">Typography</h2>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<p>This is a paragraph of text. It demonstrates the default font size, line height, and color. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="small">This is a small paragraph of text.</p>
<blockquote>
<p>"The only way to do great work is to love what you do."</p>
<footer class="blockquote-footer">Steve Jobs</footer>
</blockquote>
</section>
<section class="mb-5">
<h2 class="mb-3">Grid System</h2>
<div class="row mb-3">
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-light border">Column 1</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-light border">Column 2</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-light border">Column 3</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="p-3 bg-light border">Half Width</div>
</div>
<div class="col-6">
<div class="p-3 bg-light border">Half Width</div>
</div>
</div>
</section>
<section class="mb-5">
<h2 class="mb-3">Buttons</h2>
<div class="mb-3">
<button type="button" class="btn btn-primary">
Primary Button
</button> <button type="button" class="btn btn-secondary">
Secondary Button
</button> <button type="button" class="btn btn-success">
Success Button
</button> <button type="button" class="btn btn-danger">
Danger Button
</button> <button type="button" class="btn btn-warning">
Warning Button
</button> <button type="button" class="btn btn-info">
Info Button
</button> <button type="button" class="btn btn-light">
Light Button
</button> <button type="button" class="btn btn-dark">
Dark Button
</button> </div>
</section>
<section class="mb-5">
<h2 class="mb-3">Utility Classes</h2>
<p class="text-primary">This text is primary colored.</p>
<p class="text-secondary">This text is secondary colored.</p>
<p class="text-success">This text is success colored.</p>
<p class="text-danger">This text is danger colored.</p>
<p class="text-warning">This text is warning colored.</p>
<p class="text-info">This text is info colored.</p>
<p class="text-light bg-dark">This text is light colored on dark background.</p>
<p class="text-dark">This text is dark colored.</p>
<div class="p-3 mb-3 bg-primary text-white">Background Primary</div>
<div class="p-3 mb-3 bg-secondary text-white">Background Secondary</div>
<div class="p-3 mb-3 bg-success text-white">Background Success</div>
<div class="p-3 mb-3 bg-danger text-white">Background Danger</div>
<div class="p-3 mb-3 bg-warning text-dark">Background Warning</div>
<div class="p-3 mb-3 bg-info text-white">Background Info</div>
<div class="p-3 mb-3 bg-light text-dark">Background Light</div>
<div class="p-3 mb-3 bg-dark text-white">Background Dark</div>
<p class="mt-5">Margin Top 5</p>
<p class="pb-4">Padding Bottom 4</p>
<p class="mx-auto d-block" style="width: 200px;">Centered Block</p>
</section>
<section class="mb-5">
<h2 class="mb-3">SVG Icons</h2>
<p>
<svg class="icon" width="24" height="24"><use xlink:href="#clock"></use></svg>
Clock Icon
</p>
<p>
<svg class="icon" width="24" height="24"><use xlink:href="#info"></use></svg>
Info Icon
</p>
</section>
</main>
<footer class="bg-dark text-white py-3 mt-5">
<div class="container text-center">
<p class="mb-0">&copy; 2025 ModernKit. All rights reserved.</p>
</div>
</footer> <script src="js/main.js"></script>
</body>
</html>

5
modernkit/dist/svg-sprite.svg vendored Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol id="clock" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM12.5 7H11V13L16.25 16.15L17 14.92L12.5 12.25V7Z"/>
</symbol><symbol id="info" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z"/>
</symbol></svg>

After

Width:  |  Height:  |  Size: 666 B

302
modernkit/gulpfile.js Normal file
View File

@ -0,0 +1,302 @@
import gulp from 'gulp';
import twig from 'gulp-twig';
import * as dartSass from 'sass';
import gulpSass from 'gulp-sass';
import autoprefixer from 'gulp-autoprefixer';
import browserSync from 'browser-sync';
import babel from 'gulp-babel';
import concat from 'gulp-concat';
import uglify from 'gulp-uglify';
import imagemin from 'gulp-imagemin';
import cleanCss from 'gulp-clean-css';
import { deleteAsync } from 'del';
import sourcemaps from 'gulp-sourcemaps';
import fs from 'fs';
import plumber from 'gulp-plumber';
import svgstore from 'gulp-svgstore';
import cheerio from 'gulp-cheerio';
import through2 from 'through2';
import data from 'gulp-data';
import path from 'path';
import webp from 'gulp-webp';
import rev from 'gulp-rev';
import revReplace from 'gulp-rev-replace';
import cached from 'gulp-cached';
import remember from 'gulp-remember';
import { exec } from 'child_process';
import puppeteer from 'puppeteer';
import postcss from 'gulp-postcss';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const sass = gulpSass(dartSass);
browserSync.create();
const cleanTask = () => {
return deleteAsync(['dist']);
};
const lintJsTask = (cb) => {
exec('npx @biomejs/biome check src/js', (err, stdout, stderr) => {
console.log(stdout);
console.error(stderr);
if (err) {
console.error('Biome check failed!', err);
return cb(err);
}
cb();
});
};
const formatJsTask = (cb) => {
exec('npx @biomejs/biome format --write src/js', (err, stdout, stderr) => {
console.log(stdout);
console.error(stderr);
if (err) {
console.error('Biome format failed!', err);
return cb(err);
}
cb();
});
};
export const svgSpriteTask = (cb) => {
let svgContent = '';
return gulp.src('src/icons/**/*.svg')
.pipe(cached('svgIcons')) // Cache SVG icons
.pipe(plumber())
.pipe(cheerio({
run: function ($) {
$('[fill]').removeAttr('fill');
$('[stroke]').removeAttr('stroke');
$('[style]').removeAttr('style');
},
parserOptions: { xmlMode: true }
}))
.pipe(svgstore({
inlineSvg: true
}))
.pipe(remember('svgIcons')) // Remember all SVG icons
.pipe(through2.obj(function (file, enc, cb2) {
svgContent = file.contents.toString();
cb2(null, file);
}))
.pipe(gulp.dest('dist')) // Still write to dist for consistency
.on('end', () => {
// Ensure the file is written before calling the callback
fs.promises.writeFile('./dist/svg-sprite.svg', svgContent)
.then(cb)
.catch(cb);
});
};
const twigTask = () => {
const globalData = JSON.parse(fs.readFileSync('./src/data/global.json'));
const svgSprite = fs.readFileSync('./dist/svg-sprite.svg', 'utf8');
return gulp.src('src/templates/*.twig') // Process only top-level twig files
.pipe(cached('twigTemplates')) // Cache twig templates
.pipe(plumber())
.pipe(data(function(file) {
const fileName = path.basename(file.path, '.twig');
const dataFilePath = `./src/data/${fileName}.json`;
let pageData = {};
if (fs.existsSync(dataFilePath)) {
pageData = JSON.parse(fs.readFileSync(dataFilePath));
}
return { ...globalData, ...pageData, svgSprite: svgSprite };
}))
.pipe(twig())
.pipe(remember('twigTemplates')) // Remember all twig templates
.pipe(gulp.dest('dist'))
.pipe(browserSync.stream());
};
const scssTask = () => {
return gulp.src('src/scss/main.scss')
.pipe(cached('scss')) // Cache SCSS files
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(sass().on('error', sass.logError))
.pipe(autoprefixer())
.pipe(sourcemaps.write('.'))
.pipe(remember('scss')) // Remember all SCSS files
.pipe(gulp.dest('dist'))
.pipe(browserSync.stream());
};
const scssProdTask = () => {
return gulp.src('src/scss/main.scss')
.pipe(cached('scssProd')) // Cache SCSS files for prod
.pipe(plumber())
.pipe(sass().on('error', sass.logError))
.pipe(autoprefixer())
.pipe(cleanCss())
.pipe(remember('scssProd')) // Remember all SCSS files for prod
.pipe(gulp.dest('dist'));
};
const jsDevTask = () => {
return gulp.src('src/js/**/*.js')
.pipe(cached('jsDev')) // Cache JS files for dev
.pipe(plumber())
.pipe(babel({
presets: ['@babel/env']
}))
.pipe(concat('main.js'))
.pipe(sourcemaps.write('.'))
.pipe(remember('jsDev')) // Remember all JS files for dev
.pipe(gulp.dest('dist/js'))
.pipe(browserSync.stream());
};
const jsMinTask = () => {
return gulp.src(['src/js/**/*.js', '!src/js/**/*.test.js'])
.pipe(cached('jsProd')) // Cache JS files for prod
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(babel({
presets: ['@babel/env']
}))
.pipe(concat('main.min.js'))
.pipe(uglify())
.pipe(sourcemaps.write('.'))
.pipe(remember('jsProd')) // Remember all JS files for prod
.pipe(gulp.dest('dist/js'))
.pipe(browserSync.stream());
};
const jsTask = () => {
return gulp.src(['src/js/**/*.js', '!src/js/**/*.test.js'])
.pipe(cached('jsProd')) // Cache JS files for prod
.pipe(plumber())
.pipe(sourcemaps.init())
.pipe(babel({
presets: ['@babel/env']
}))
.pipe(concat('main.js'))
.pipe(sourcemaps.write('.'))
.pipe(remember('jsProd')) // Remember all JS files for prod
.pipe(gulp.dest('dist/js'))
.pipe(browserSync.stream());
};
const imagesTask = () => {
return gulp.src('src/images/*')
.pipe(cached('images')) // Cache images
.pipe(plumber())
.pipe(imagemin())
.pipe(remember('images')) // Remember all images
.pipe(gulp.dest('dist/images'))
.pipe(browserSync.stream());
};
const imagesProdTask = () => {
return gulp.src('src/images/*')
.pipe(cached('imagesProd')) // Cache images for prod
.pipe(plumber())
.pipe(imagemin())
.pipe(gulp.dest('dist/images'))
.pipe(webp())
.pipe(gulp.dest('dist/images'));
};
const copyAssetsTask = () => {
return gulp.src('src/assets/**/*')
.pipe(cached('assets')) // Cache assets
.pipe(plumber())
.pipe(remember('assets')) // Remember all assets
.pipe(gulp.dest('dist/assets'))
.pipe(browserSync.stream());
};
const copyFaviconTask = () => {
return gulp.src('src/favicon.ico')
.pipe(cached('favicon')) // Cache favicon
.pipe(plumber())
.pipe(remember('favicon')) // Remember favicon
.pipe(gulp.dest('dist'))
.pipe(browserSync.stream());
};
const copyFontsTask = () => {
return gulp.src('src/fonts/**/*')
.pipe(cached('fonts')) // Cache fonts
.pipe(plumber())
.pipe(remember('fonts')) // Remember all fonts
.pipe(gulp.dest('dist/fonts'))
.pipe(browserSync.stream());
};
const revTask = () => {
return gulp.src(['dist/**/*.css', 'dist/**/*.js'], { base: 'dist' })
.pipe(gulp.dest('dist')) // write original assets to dist
.pipe(rev())
.pipe(gulp.dest('dist')) // write rev'd assets to dist
.pipe(rev.manifest())
.pipe(gulp.dest('dist')); // write manifest to dist
};
const revReplaceTask = () => {
const manifest = gulp.src('dist/rev-manifest.json');
return gulp.src('dist/**/*.html')
.pipe(revReplace({ manifest: manifest }))
.pipe(gulp.dest('dist'));
};
const screenshotsTask = async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const resolutions = [
{ width: 1920, height: 1080, name: 'desktop' },
{ width: 1366, height: 768, name: 'laptop' },
{ width: 768, height: 1024, name: 'tablet' },
{ width: 375, height: 667, name: 'mobile' },
];
// Ensure the screenshots directory exists
if (!fs.existsSync('dist/screenshots')) {
fs.mkdirSync('dist/screenshots');
}
for (const res of resolutions) {
await page.setViewport({ width: res.width, height: res.height });
await page.goto('file://' + path.resolve(__dirname, 'dist/index.html'));
await page.screenshot({ path: `dist/screenshots/screenshot-${res.name}-${res.width}x${res.height}.png` });
console.log(`Screenshot captured for ${res.name} (${res.width}x${res.height})`);
}
await browser.close();
};
const watchTask = () => {
browserSync.init({
server: {
baseDir: './dist'
},
https: true // Enable HTTPS
});
gulp.watch('src/templates/**/*.twig', twigTask);
gulp.watch('src/scss/**/*.scss', scssTask);
gulp.watch('src/js/**/*.js', gulp.series(lintJsTask, jsDevTask));
gulp.watch('src/images/*', imagesTask);
gulp.watch('src/data/**/*.json', twigTask);
gulp.watch('src/assets/**/*', copyAssetsTask);
gulp.watch('src/favicon.ico', copyFaviconTask);
gulp.watch('src/icons/**/*.svg', gulp.series(svgSpriteTask, twigTask)); // Watch for changes in SVG icons
gulp.watch('src/fonts/**/*', copyFontsTask); // Watch for changes in fonts
};
const devBuild = gulp.series(cleanTask, svgSpriteTask, gulp.parallel(scssTask, jsDevTask, imagesTask, copyAssetsTask, copyFaviconTask, copyFontsTask), twigTask);
export const build = gulp.series(cleanTask, lintJsTask, svgSpriteTask, twigTask, gulp.parallel(scssTask, jsDevTask, imagesTask, copyAssetsTask, copyFaviconTask, copyFontsTask));
export const buildProd = gulp.series(cleanTask, lintJsTask, svgSpriteTask, twigTask, gulp.parallel(scssProdTask, jsTask, jsMinTask, imagesProdTask, copyAssetsTask, copyFaviconTask, copyFontsTask), revTask, revReplaceTask, screenshotsTask);
export const screenshots = screenshotsTask;
export default gulp.series(devBuild, watchTask);

13689
modernkit/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

51
modernkit/package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "modernkit",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "gulp",
"build": "gulp build",
"build:prod": "gulp buildProd"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "module",
"devDependencies": {
"@babel/core": "^7.27.7",
"@babel/preset-env": "^7.27.2",
"@fullhuman/postcss-purgecss": "^7.0.2",
"browser-sync": "^3.0.4",
"del": "^8.0.0",
"gulp": "^5.0.1",
"gulp-autoprefixer": "^9.0.0",
"gulp-babel": "^8.0.0",
"gulp-cached": "^1.1.1",
"gulp-clean-css": "^4.3.0",
"gulp-concat": "^2.6.1",
"gulp-data": "^1.3.1",
"gulp-imagemin": "^9.1.0",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^10.0.0",
"gulp-remember": "^1.0.1",
"gulp-rev": "^10.0.0",
"gulp-rev-replace": "^0.4.4",
"gulp-sass": "^6.0.1",
"gulp-sourcemaps": "^3.0.0",
"gulp-svgstore": "^9.0.0",
"gulp-twig": "^1.2.0",
"gulp-uglify": "^3.0.2",
"gulp-webp": "^5.0.0",
"postcss": "^8.5.6",
"sass": "^1.89.2",
"through2": "^4.0.2",
"twig": "^1.17.1"
},
"dependencies": {
"gulp-cheerio": "^1.0.0",
"micromodal": "^0.6.1",
"puppeteer": "^24.10.2",
"swiper": "^11.2.8"
}
}

View File

@ -0,0 +1 @@
This is a test asset file.

View File

@ -0,0 +1,4 @@
{
"pageTitle": "About Us",
"aboutContent": "This is the content for the about page. We are a team dedicated to building amazing things."
}

View File

@ -0,0 +1,5 @@
{
"title": "ModernKit UI Kit",
"welcomeMessage": "Welcome to the ModernKit UI Kit Showcase!",
"year": 2025
}

View File

@ -0,0 +1 @@
This is a dummy favicon file.

View File

@ -0,0 +1 @@
This is a dummy font file.

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM12.5 7H11V13L16.25 16.15L17 14.92L12.5 12.25V7Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM11 15H13V17H11V15ZM11 7H13V13H11V7Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 334 B

59
modernkit/src/js/main.js Normal file
View File

@ -0,0 +1,59 @@
import Swiper from "swiper";
import { Navigation, Pagination } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import MicroModal from "micromodal";
const button = document.querySelector("button");
if (button) {
button.addEventListener("click", () => {
alert("Button clicked!");
});
}
// Tabs functionality
document.addEventListener("DOMContentLoaded", () => {
const tabButtons = document.querySelectorAll(".tab-button");
const tabPanes = document.querySelectorAll(".tab-pane");
tabButtons.forEach((button) => {
button.addEventListener("click", () => {
const targetTab = button.dataset.tab;
tabButtons.forEach((btn) => btn.classList.remove("active"));
button.classList.add("active");
tabPanes.forEach((pane) => {
if (pane.id === targetTab) {
pane.classList.add("active");
} else {
pane.classList.remove("active");
}
});
});
});
// Initialize first tab as active
if (tabButtons.length > 0) {
tabButtons[0].click();
}
});
// Swiper initialization
const _swiper = new Swiper(".swiper", {
modules: [Navigation, Pagination],
loop: true,
pagination: {
el: ".swiper-pagination",
clickable: true,
},
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
// Micromodal initialization
MicroModal.init();

View File

@ -0,0 +1,41 @@
@use 'variables';
@use "sass:color";
html {
box-sizing: border-box;
}
*,
*::before,
*::after {
box-sizing: inherit;
}
html {
font-size: 62.5%; /* 1rem = 10px */
}
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background-color: #f4f4f4;
color: #333;
font-size: 1.6rem; /* Default font size for body, equivalent to 16px */
}
a {
color: variables.$primary-color;
text-decoration: none;
&:hover {
color: color.adjust(variables.$primary-color, $lightness: -10%);
text-decoration: underline;
}
}
img {
max-width: 100%;
height: auto;
}

View File

@ -0,0 +1,89 @@
// Tabs
.tabs {
margin-bottom: 20px;
}
.tab-headers {
display: flex;
border-bottom: 1px solid #ccc;
margin-bottom: 15px;
}
.tab-button {
padding: 10px 15px;
border: none;
background-color: #f0f0f0;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
&.active {
background-color: #e0e0e0;
border-bottom: 2px solid #007bff;
}
&:hover {
background-color: #e0e0e0;
}
}
.tab-pane {
display: none;
&.active {
display: block;
}
}
// Swiper (Slider)
.swiper {
width: 100%;
height: 300px;
background-color: #f8f8f8;
border: 1px solid #eee;
}
.swiper-slide {
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
color: #333;
}
// Micromodal (Modal)
.modal {
/* styles for the outer modal wrapper */
}
.modal__overlay {
/* styles for the modal overlay */
}
.modal__container {
/* styles for the modal container */
}
.modal__header {
/* styles for the modal header */
}
.modal__title {
/* styles for the modal title */
}
.modal__close {
/* styles for the close button */
}
.modal__content {
/* styles for the modal content */
}
.modal__footer {
/* styles for the modal footer */
}
.modal__btn {
/* styles for modal buttons */
}

View File

@ -0,0 +1,49 @@
@use 'variables';
@use "sass:math";
.container {
width: 100%;
padding-right: math.div(variables.$grid-gutter-width, 2);
padding-left: math.div(variables.$grid-gutter-width, 2);
margin-right: auto;
margin-left: auto;
@each $breakpoint, $width in variables.$grid-breakpoints {
@if $width > 0 {
@media (min-width: $width) {
max-width: $width;
}
}
}
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: math.div(variables.$grid-gutter-width, -2);
margin-left: math.div(variables.$grid-gutter-width, -2);
}
@for $i from 1 through variables.$grid-columns {
.col-#{$i} {
flex: 0 0 auto;
width: math.div(100%, variables.$grid-columns) * $i;
padding-right: math.div(variables.$grid-gutter-width, 2);
padding-left: math.div(variables.$grid-gutter-width, 2);
}
}
@each $breakpoint, $width in variables.$grid-breakpoints {
@if $width > 0 {
@media (min-width: $width) {
@for $i from 1 through variables.$grid-columns {
.col-#{$breakpoint}-#{$i} {
flex: 0 0 auto;
width: math.div(100%, variables.$grid-columns) * $i;
padding-right: math.div(variables.$grid-gutter-width, 2);
padding-left: math.div(variables.$grid-gutter-width, 2);
}
}
}
}
}

View File

@ -0,0 +1,61 @@
@use 'variables';
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: (variables.$spacer * .5);
font-family: variables.$font-family-base;
font-weight: 500;
line-height: 1.2;
color: variables.$body-color;
}
h1 {
font-size: variables.$h1-font-size;
}
h2 {
font-size: variables.$h2-font-size;
}
h3 {
font-size: variables.$h3-font-size;
}
h4 {
font-size: variables.$h4-font-size;
}
h5 {
font-size: variables.$h5-font-size;
}
h6 {
font-size: variables.$h6-font-size;
}
p {
margin-top: 0;
margin-bottom: variables.$spacer;
}
small,
.small {
font-size: 80%;
}
mark,
.mark {
padding: .2em;
background-color: #fcf8e3;
}
blockquote {
margin: 0 0 variables.$spacer;
}
ul,
ol {
padding-left: 2rem;
margin-top: 0;
margin-bottom: variables.$spacer;
}

View File

@ -0,0 +1,109 @@
@use 'variables';
// Margin and Padding
@each $prop, $abbrev in (margin, m), (padding, p) {
@each $size, $length in variables.$spacers {
.#{$abbrev}-#{$size} {
#{$prop}: #{$length} !important;
}
.#{$abbrev}t-#{$size} {
#{$prop}-top: #{$length} !important;
}
.#{$abbrev}b-#{$size} {
#{$prop}-bottom: #{$length} !important;
}
.#{$abbrev}l-#{$size} {
#{$prop}-left: #{$length} !important;
}
.#{$abbrev}r-#{$size} {
#{$prop}-right: #{$length} !important;
}
.#{$abbrev}x-#{$size} {
#{$prop}-left: #{$length} !important;
#{$prop}-right: #{$length} !important;
}
.#{$abbrev}y-#{$size} {
#{$prop}-top: #{$length} !important;
#{$prop}-bottom: #{$length} !important;
}
}
}
// Text alignment
.text-left {
text-align: left !important;
}
.text-right {
text-align: right !important;
}
.text-center {
text-align: center !important;
}
// Display
.d-block {
display: block !important;
}
.d-inline-block {
display: inline-block !important;
}
.d-flex {
display: flex !important;
}
// Colors
.text-primary {
color: variables.$primary-color !important;
}
.text-secondary {
color: variables.$secondary-color !important;
}
.text-success {
color: variables.$success-color !important;
}
.text-danger {
color: variables.$danger-color !important;
}
.text-warning {
color: variables.$warning-color !important;
}
.text-info {
color: variables.$info-color !important;
}
.text-light {
color: variables.$light-color !important;
}
.text-dark {
color: variables.$dark-color !important;
}
.text-white {
color: variables.$white !important;
}
.bg-primary {
background-color: variables.$primary-color !important;
}
.bg-secondary {
background-color: variables.$secondary-color !important;
}
.bg-success {
background-color: variables.$success-color !important;
}
.bg-danger {
background-color: variables.$danger-color !important;
}
.bg-warning {
background-color: variables.$warning-color !important;
}
.bg-info {
background-color: variables.$info-color !important;
}
.bg-light {
background-color: variables.$light-color !important;
}
.bg-dark {
background-color: variables.$dark-color !important;
}
.bg-white {
background-color: variables.$white !important;
}

View File

@ -0,0 +1,48 @@
// Colors
$primary-color: #007bff;
$secondary-color: #6c757d;
$success-color: #28a745;
$danger-color: #dc3545;
$warning-color: #ffc107;
$info-color: #17a2b8;
$light-color: #f8f9fa;
$dark-color: #343a40;
$body-color: #212529;
$text-color: #333;
$white: #fff;
// Typography
$font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
$font-size-base: 1rem; // 16px
$line-height-base: 1.5;
$h1-font-size: 2.5rem;
$h2-font-size: 2rem;
$h3-font-size: 1.75rem;
$h4-font-size: 1.5rem;
$h5-font-size: 1.25rem;
$h6-font-size: 1rem;
// Spacing
$spacer: 1rem;
$spacers: (
0: 0,
1: ($spacer * .25), // 4px
2: ($spacer * .5), // 8px
3: $spacer, // 16px
4: ($spacer * 1.5), // 24px
5: ($spacer * 3) // 48px
);
// Breakpoints
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px
);
// Grid
$grid-columns: 12;
$grid-gutter-width: 30px;

View File

@ -0,0 +1,6 @@
@use 'variables';
@use 'base';
@use 'typography';
@use 'grid';
@use 'utilities';
@use 'components';

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ pageTitle }} - {{ global.title }}</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div style="display: none;">{{ svgSprite|raw }}</div>
{% include 'components/header.twig' %}
<main class="container py-5">
<h1 class="mb-4">{{ pageTitle }}</h1>
<p>{{ aboutContent }}</p>
</main>
{% include 'components/footer.twig' %}
<script src="main.js"></script>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ global.title }}</title>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div style="display: none;">{{ svgSprite|raw }}</div>
{% include 'components/header.twig' %}
<main class="container py-5">
{% block content %}{% endblock %}
</main>
{% include 'components/footer.twig' %}
<script src="js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,78 @@
{% extends "base.twig" %}
{% block content %}
<h1>UI Components Showcase</h1>
<section>
<h2>Tabs</h2>
<div class="tabs">
<div class="tab-headers">
<button class="tab-button active" data-tab="tab1">Tab 1</button>
<button class="tab-button" data-tab="tab2">Tab 2</button>
<button class="tab-button" data-tab="tab3">Tab 3</button>
</div>
<div class="tab-content">
<div id="tab1" class="tab-pane active">
<h3>Content for Tab 1</h3>
<p>This is the content for the first tab. It can contain any HTML elements.</p>
</div>
<div id="tab2" class="tab-pane">
<h3>Content for Tab 2</h3>
<p>This is the content for the second tab. More information here.</p>
</div>
<div id="tab3" class="tab-pane">
<h3>Content for Tab 3</h3>
<p>This is the content for the third tab. Even more details.</p>
</div>
</div>
</div>
</section>
<section>
<h2>Slider (Swiper)</h2>
<!-- Slider main container -->
<div class="swiper">
<!-- Additional required wrapper -->
<div class="swiper-wrapper">
<!-- Slides -->
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
</div>
<!-- If we need pagination -->
<div class="swiper-pagination"></div>
<!-- If we need navigation buttons -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- If we need scrollbar -->
<div class="swiper-scrollbar"></div>
</div>
</section>
<section>
<h2>Modal (Micromodal)</h2>
<button data-micromodal-trigger="modal-1">Open Modal</button>
<div class="modal micromodal-slide" id="modal-1" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
<header class="modal__header">
<h2 class="modal__title" id="modal-1-title">
Micromodal Example
</h2>
<button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
</header>
<div class="modal__content" id="modal-1-content">
<p>This is a simple modal window powered by Micromodal.</p>
<p>You can put any content here.</p>
</div>
<footer class="modal__footer">
<button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Close</button>
</footer>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,3 @@
<button type="button" class="btn {{ class }}">
{{ text }}
</button>

View File

@ -0,0 +1,5 @@
<footer class="bg-dark text-white py-3 mt-5">
<div class="container text-center">
<p class="mb-0">&copy; 2025 ModernKit. All rights reserved.</p>
</div>
</footer>

View File

@ -0,0 +1,12 @@
<header class="bg-dark text-white py-3">
<div class="container d-flex justify-content-between align-items-center">
<a href="#" class="text-white h4 mb-0">ModernKit</a>
<nav>
<ul class="list-unstyled d-flex mb-0">
<li class="ml-3"><a href="#" class="text-white">Home</a></li>
<li class="ml-3"><a href="#" class="text-white">About</a></li>
<li class="ml-3"><a href="#" class="text-white">Contact</a></li>
</ul>
</nav>
</div>
</header>

View File

@ -0,0 +1,95 @@
{% extends "base.twig" %}
{% block content %}
<h1 class="mb-4">{{ global.welcomeMessage }}</h1>
<section class="mb-5">
<h2 class="mb-3">Typography</h2>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<p>This is a paragraph of text. It demonstrates the default font size, line height, and color. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
<p class="small">This is a small paragraph of text.</p>
<blockquote>
<p>"The only way to do great work is to love what you do."</p>
<footer class="blockquote-footer">Steve Jobs</footer>
</blockquote>
</section>
<section class="mb-5">
<h2 class="mb-3">Grid System</h2>
<div class="row mb-3">
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-light border">Column 1</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-light border">Column 2</div>
</div>
<div class="col-12 col-md-6 col-lg-4">
<div class="p-3 bg-light border">Column 3</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="p-3 bg-light border">Half Width</div>
</div>
<div class="col-6">
<div class="p-3 bg-light border">Half Width</div>
</div>
</div>
</section>
<section class="mb-5">
<h2 class="mb-3">Buttons</h2>
<div class="mb-3">
{% include 'components/button.twig' with {text: 'Primary Button', class: 'btn-primary'} %}
{% include 'components/button.twig' with {text: 'Secondary Button', class: 'btn-secondary'} %}
{% include 'components/button.twig' with {text: 'Success Button', class: 'btn-success'} %}
{% include 'components/button.twig' with {text: 'Danger Button', class: 'btn-danger'} %}
{% include 'components/button.twig' with {text: 'Warning Button', class: 'btn-warning'} %}
{% include 'components/button.twig' with {text: 'Info Button', class: 'btn-info'} %}
{% include 'components/button.twig' with {text: 'Light Button', class: 'btn-light'} %}
{% include 'components/button.twig' with {text: 'Dark Button', class: 'btn-dark'} %}
</div>
</section>
<section class="mb-5">
<h2 class="mb-3">Utility Classes</h2>
<p class="text-primary">This text is primary colored.</p>
<p class="text-secondary">This text is secondary colored.</p>
<p class="text-success">This text is success colored.</p>
<p class="text-danger">This text is danger colored.</p>
<p class="text-warning">This text is warning colored.</p>
<p class="text-info">This text is info colored.</p>
<p class="text-light bg-dark">This text is light colored on dark background.</p>
<p class="text-dark">This text is dark colored.</p>
<div class="p-3 mb-3 bg-primary text-white">Background Primary</div>
<div class="p-3 mb-3 bg-secondary text-white">Background Secondary</div>
<div class="p-3 mb-3 bg-success text-white">Background Success</div>
<div class="p-3 mb-3 bg-danger text-white">Background Danger</div>
<div class="p-3 mb-3 bg-warning text-dark">Background Warning</div>
<div class="p-3 mb-3 bg-info text-white">Background Info</div>
<div class="p-3 mb-3 bg-light text-dark">Background Light</div>
<div class="p-3 mb-3 bg-dark text-white">Background Dark</div>
<p class="mt-5">Margin Top 5</p>
<p class="pb-4">Padding Bottom 4</p>
<p class="mx-auto d-block" style="width: 200px;">Centered Block</p>
</section>
<section class="mb-5">
<h2 class="mb-3">SVG Icons</h2>
<p>
<svg class="icon" width="24" height="24"><use xlink:href="#clock"></use></svg>
Clock Icon
</p>
<p>
<svg class="icon" width="24" height="24"><use xlink:href="#info"></use></svg>
Info Icon
</p>
</section>
{% endblock %}

15
vite-templates/._npmrc Normal file
View File

@ -0,0 +1,15 @@
# Разрешаем выполнение скриптов сборки для необходимых пакетов
enable-pre-post-scripts=true
# Разрешаем скрипты для пакетов оптимизации изображений
@parcel/watcher:registry=https://registry.npmjs.org/
cwebp-bin:registry=https://registry.npmjs.org/
esbuild:registry=https://registry.npmjs.org/
gifsicle:registry=https://registry.npmjs.org/
jpegtran-bin:registry=https://registry.npmjs.org/
mozjpeg:registry=https://registry.npmjs.org/
optipng-bin:registry=https://registry.npmjs.org/
pngquant-bin:registry=https://registry.npmjs.org/
# Или глобально разрешить все скрипты (менее безопасно, но проще)
# ignore-scripts=false

View File

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,4 @@
node_modules/
dist/
*.min.js
vite.config.js

View File

@ -0,0 +1,49 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: ['eslint:recommended', 'prettier'],
plugins: ['prettier'],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
// Prettier
'prettier/prettier': 'error',
// Общие правила
'no-console': 'warn',
'no-unused-vars': 'warn',
'no-undef': 'error',
'prefer-const': 'error',
'no-var': 'error',
// Стиль кода
indent: ['error', 2],
quotes: ['error', 'single'],
semi: ['error', 'always'],
'comma-dangle': ['error', 'never'],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': ['error', 'never'],
// Функции
'arrow-spacing': 'error',
'no-duplicate-imports': 'error',
'prefer-arrow-callback': 'error',
'prefer-template': 'error',
// Объекты и массивы
'object-shorthand': 'error',
'prefer-destructuring': [
'error',
{
array: false,
object: true
}
]
},
ignorePatterns: ['dist/', 'node_modules/', '*.min.js']
};

35
vite-templates/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build output
dist/
build/
# Environment vars
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
logs
*.log
# Cache
.cache/
.parcel-cache/
.eslintcache

View File

@ -0,0 +1,34 @@
{
"name": "modern-frontend-builder-lite",
"version": "1.0.0",
"description": "Современный frontend сборщик на Vite/Vituum (облегченная версия)",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .js,.ts,.vue --fix",
"format": "prettier --write .",
"lint:style": "stylelint \"src/**/*.{css,scss,sass}\" --fix",
"serve": "vite --host --https"
},
"dependencies": {
"normalize.css": "^8.0.1"
},
"devDependencies": {
"@vituum/vite-plugin-pug": "^1.0.15",
"@vituum/vite-plugin-twig": "^1.0.15",
"vite": "^5.0.0",
"vituum": "^1.1.0",
"sass": "^1.69.0",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0",
"stylelint": "^15.11.0",
"stylelint-config-standard-scss": "^11.1.0",
"stylelint-prettier": "^4.0.2",
"@vitejs/plugin-basic-ssl": "^1.0.2",
"glob": "^10.3.10"
}
}

View File

@ -0,0 +1,133 @@
import { defineConfig } from 'vite';
import vituum from 'vituum';
import pug from '@vituum/vite-plugin-pug';
import twig from '@vituum/vite-plugin-twig';
import { resolve } from 'path';
import { glob } from 'glob';
import basicSsl from '@vitejs/plugin-basic-ssl';
// Автоматическое определение входных точек
const getInputFiles = () => {
const htmlFiles = glob.sync('src/pages/**/*.{html,pug,twig}');
const jsFiles = glob.sync('src/js/**/*.js');
const input = {};
// HTML/PUG/TWIG файлы
htmlFiles.forEach(file => {
const name = file
.replace('src/pages/', '')
.replace(/\.(html|pug|twig)$/, '');
input[name] = resolve(__dirname, file);
});
// JS файлы
jsFiles.forEach(file => {
const name = file.replace('src/js/', '').replace('.js', '');
input[`js/${name}`] = resolve(__dirname, file);
});
return input;
};
export default defineConfig({
plugins: [
vituum({
pages: {
dir: './src/pages'
}
}),
// Поддержка Pug
pug({
root: './src',
options: {
pretty: true,
basedir: './src'
}
}),
// Поддержка Twig
twig({
root: './src',
namespaces: {
layouts: './src/layouts',
components: './src/components',
data: './src/data'
}
}),
// HTTPS для разработки
basicSsl()
],
// Настройки сервера разработки
server: {
host: true,
port: 3000,
https: true,
open: true
},
// Настройки сборки
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: true,
rollupOptions: {
input: getInputFiles(),
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: assetInfo => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
if (/\.(png|jpe?g|svg|gif|tiff|bmp|ico)$/i.test(assetInfo.name)) {
return `assets/images/[name]-[hash].${ext}`;
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return `assets/fonts/[name]-[hash].${ext}`;
}
if (/\.css$/i.test(assetInfo.name)) {
return `assets/css/[name]-[hash].${ext}`;
}
return `assets/[name]-[hash].${ext}`;
}
}
},
// Минификация
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
// Настройки CSS
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "./src/styles/_vars.scss"; @import "./src/styles/_scss";`
}
},
devSourcemap: true
},
// Алиасы путей
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@styles': resolve(__dirname, 'src/styles'),
'@js': resolve(__dirname, 'src/js'),
'@images': resolve(__dirname, 'src/images'),
'@components': resolve(__dirname, 'src/components'),
'@layouts': resolve(__dirname, 'src/layouts'),
'@data': resolve(__dirname, 'src/data')
}
}
});

View File

@ -0,0 +1,6 @@
node_modules
dist
*.min.js
*.min.css
package-lock.json
yarn.lock

View File

@ -0,0 +1,28 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf",
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"overrides": [
{
"files": "*.scss",
"options": {
"singleQuote": false
}
},
{
"files": "*.json",
"options": {
"singleQuote": false
}
}
]
}

View File

@ -0,0 +1,52 @@
module.exports = {
extends: ['stylelint-config-standard-scss', 'stylelint-prettier/recommended'],
plugins: ['stylelint-prettier'],
rules: {
// Prettier
'prettier/prettier': true,
// SCSS правила
'scss/at-rule-no-unknown': null,
'scss/at-import-partial-extension': null,
'scss/dollar-variable-pattern': '^[a-z][a-zA-Z0-9]*$',
'scss/percent-placeholder-pattern': '^[a-z][a-zA-Z0-9]*$',
// Общие правила
'color-hex-case': 'lower',
'color-hex-length': 'short',
'declaration-block-trailing-semicolon': 'always',
indentation: 2,
'max-empty-lines': 2,
'no-duplicate-selectors': true,
'no-empty-source': null,
// Селекторы
'selector-class-pattern':
'^[a-z]([a-z0-9-]+)?(__([a-z0-9]+-?)+)?(--([a-z0-9]+-?)+){0,2}$',
'selector-max-id': 0,
'selector-max-compound-selectors': 4,
// Свойства
'property-no-unknown': true,
'declaration-no-important': true,
// Единицы измерения
'unit-allowed-list': [
'px',
'em',
'rem',
'%',
'vh',
'vw',
'vmin',
'vmax',
'deg',
's',
'ms'
],
// Функции
'function-url-quotes': 'always'
},
ignoreFiles: ['dist/**/*', 'node_modules/**/*']
};

271
vite-templates/README.md Normal file
View File

@ -0,0 +1,271 @@
# 🚀 Полное руководство
## 📋 Подготовка к установке
### Системные требования
- **Node.js** 16.0+
- **npm** 7.0+ или **yarn** 1.22+
- **Git** (для клонирования)
### Проверка версий
```bash
node --version # v16.0+
npm --version # 7.0+
git --version # любая актуальная
```
## 🚀 Возможности
- **⚡ Vite + Vituum** - Молниеносная сборка и HMR
- **🎨 Шаблонизаторы** - Поддержка Pug и Twig
- **💅 SCSS** - Современный препроцессор CSS
- **🔍 Линтинг** - ESLint + Prettier + Stylelint
- **📱 Адаптивность** - Mobile-first подход
- **🖼️ Оптимизация изображений** - Автоматическое сжатие
- **🔒 HTTPS** - Безопасная разработка
- **📊 JSON данные** - Динамическая загрузка контента
- **🎯 Современный JS** - ES6+ модули и функции
## 📦 Установка
```bash
# Клонирование репозитория
git clone <repository-url>
cd modern-frontend-builder
# Установка зависимостей
npm install
```
## 🛠️ Использование
```bash
# Запуск сервера разработки
npm run dev
# Сборка для продакшена
npm run build
# Предварительный просмотр сборки
npm run preview
# Линтинг и форматирование
npm run lint # ESLint
npm run format # Prettier
npm run lint:style # Stylelint
# HTTPS сервер
npm run serve
```
## 📁 Структура проекта
```
src/
├── pages/ # Страницы (HTML/PUG/TWIG)
├── layouts/ # Макеты шаблонов
├── components/ # Переиспользуемые компоненты
├── styles/ # SCSS стили
├── js/ # JavaScript модули
├── images/ # Изображения
├── fonts/ # Шрифты
├── data/ # JSON данные
└── public/ # Статические файлы
```
## 🎨 Работа с шаблонами
### Pug
```pug
extends ../layouts/base
block content
.hero
h1= data.content.hero.title
p= data.content.hero.subtitle
```
### Twig
```twig
{% extends 'layouts/main.twig' %}
{% block content %}
<div class="hero">
<h1>{{ data.content.hero.title }}</h1>
<p>{{ data.content.hero.subtitle }}</p>
</div>
{% endblock %}
```
## 💅 Система стилей
- **vars** - Централизованные переменные
- **Mixins** - Переиспользуемые стили
- **Components** - Модульные компоненты
- **Utilities** - Вспомогательные классы
- **BEM методология** - Структурированное именование
## 🖼️ Оптимизация изображений
Автоматическая оптимизация:
- **JPEG** - mozjpeg сжатие
- **PNG** - pngquant оптимизация
- **SVG** - SVGO минификация
- **WebP** - Современный формат
## 📊 Работа с данными
JSON файлы автоматически доступны в шаблонах:
```javascript
// В JavaScript
import { loadData } from './utils/dataLoader.js';
const data = await loadData('/src/data/content.json');
```
## 🔧 Настройка
### Vite конфигурация
Основные настройки в `vite.config.js`:
- Алиасы путей
- Плагины оптимизации
- Настройки сборки
- HTTPS сертификаты
### Линтинг
- **ESLint** - JavaScript код
- **Stylelint** - CSS/SCSS стили
- **Prettier** - Форматирование
## 🚀 Развертывание
### Сборка для продакшена
```bash
npm run build
```
Результат будет в папке `dist/` - готов для загрузки на хостинг.
### Настройка для хостинга
Большинство хостингов поддерживают статические файлы из папки `dist/`.
Для **Netlify/Vercel**:
- Подключите Git репозиторий
- Команда сборки: `npm run build`
- Папка публикации: `dist`
Для **GitHub Pages**:
```bash
# Установите gh-pages
npm install --save-dev gh-pages
# Добавьте в package.json
"scripts": {
"deploy": "npm run build && gh-pages -d dist"
}
# Деплой
npm run deploy
```
## 🔄 Рабочий процесс
### Ежедневная разработка
1. **Запуск:** `npm run dev`
2. **Разработка:** Редактирование файлов в `src/`
3. **Проверка:** `npm run lint`
4. **Коммит:** Сохранение изменений в Git
### Перед релизом
1. **Тестирование:** `npm run build && npm run preview`
2. **Проверка качества:**
```bash
npm run lint
npm run format
npm run lint:style
```
3. **Сборка:** `npm run build`
4. **Деплой:** Загрузка `dist/` на хостинг
## 📚 Дополнительные возможности
### Добавление новых шаблонизаторов
В `vite.config.js` можно добавить поддержку других шаблонов:
```javascript
import handlebars from '@vituum/vite-plugin-handlebars';
// В plugins
handlebars({
root: './src'
});
```
### Интеграция с CMS
Для подключения к Headless CMS:
```javascript
// src/js/utils/cms.js
export const fetchContent = async endpoint => {
const response = await fetch(`https://api.your-cms.com/${endpoint}`);
return response.json();
};
```
### Добавление PWA
```bash
npm install --save-dev vite-plugin-pwa
# В vite.config.js
import { VitePWA } from 'vite-plugin-pwa';
// В plugins
VitePWA({
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg}']
}
})
```
### Анализ бандла
```bash
npm install --save-dev rollup-plugin-visualizer
# Добавить в vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
// В plugins
visualizer({
filename: 'dist/stats.html',
open: true
})
```
## 📖 Полезные ссылки
- **Vite документация:** https://vitejs.dev/
- **Vituum документация:** https://vituum.dev/
- **Pug документация:** https://pugjs.org/
- **Twig документация:** https://twig.symfony.com/
- **SCSS документация:** https://sass-lang.com/
- **ESLint правила:** https://eslint.org/docs/rules/
- **Prettier опции:** https://prettier.io/docs/en/options.html

View File

@ -0,0 +1,57 @@
{
"name": "modern-frontend-builder",
"version": "1.0.0",
"description": "Современный frontend сборщик на Vite/Vituum",
"type": "module",
"scripts": {
"dev": "vite --mode development",
"dev:safe": "vite --mode development --force",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist .vite node_modules/.vite",
"restart": "npm run clean && npm install && npm run dev",
"lint": "eslint . --ext .js,.ts,.vue --fix",
"format": "prettier --write .",
"check": "npm run lint && npm run format",
"lint:style": "stylelint \"src/**/*.{css,scss,sass}\" --fix",
"serve": "vite --host --https",
"postinstall": "node scripts/postinstall.js"
},
"dependencies": {
"normalize.css": "^8.0.1"
},
"devDependencies": {
"@vitejs/plugin-basic-ssl": "^1.0.2",
"@vituum/vite-plugin-handlebars": "^1.1.0",
"@vituum/vite-plugin-pug": "^1.0.15",
"@vituum/vite-plugin-twig": "^1.0.15",
"eslint": "^8.55.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.0.1",
"glob": "^10.3.10",
"imagemin-mozjpeg": "^10.0.0",
"imagemin-pngquant": "^9.0.2",
"imagemin-svgo": "^10.0.1",
"imagemin-webp": "^8.0.0",
"prettier": "^3.1.0",
"sass": "^1.69.0",
"stylelint": "^15.11.0",
"stylelint-config-standard-scss": "^11.1.0",
"stylelint-prettier": "^4.0.2",
"vite": "^5.0.0",
"vite-plugin-imagemin": "^0.6.1",
"vituum": "^1.1.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"cwebp-bin",
"esbuild",
"gifsicle",
"jpegtran-bin",
"mozjpeg",
"optipng-bin",
"pngquant-bin"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
import { build } from 'vite';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
async function buildProject() {
console.log('🚀 Начинаем сборку проекта...\n');
try {
// Линтинг перед сборкой
console.log('🔍 Проверка кода...');
await execAsync('npm run lint');
console.log('✅ Код проверен\n');
// Сборка
console.log('📦 Сборка проекта...');
await build();
console.log('✅ Проект собран\n');
// Анализ размера бандла
console.log('📊 Анализ размера файлов...');
await execAsync('npx vite-bundle-analyzer dist');
console.log('🎉 Сборка завершена успешно!');
} catch (error) {
console.error('❌ Ошибка сборки:', error);
process.exit(1);
}
}
buildProject();

View File

@ -0,0 +1,26 @@
import { createServer } from 'vite';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const config = require('../vite.config.js');
async function startDevServer() {
try {
const server = await createServer(config);
await server.listen();
console.log('🚀 Сервер разработки запущен!');
console.log('📱 Локальный адрес: https://localhost:3000');
console.log('🌐 Сетевой адрес доступен в терминале');
console.log('\n⚡ Готов к разработке!\n');
// Открытие браузера
const { default: open } = await import('open');
await open('https://localhost:3000');
} catch (error) {
console.error('❌ Ошибка запуска сервера:', error);
process.exit(1);
}
}
startDevServer();

View File

@ -0,0 +1,31 @@
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { existsSync } from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log('🔧 Проверка установки пакетов оптимизации изображений...');
const requiredBinaries = [
'node_modules/mozjpeg/vendor/cjpeg',
'node_modules/pngquant-bin/vendor/pngquant',
'node_modules/gifsicle/vendor/gifsicle'
];
let allInstalled = true;
requiredBinaries.forEach(binary => {
const fullPath = join(__dirname, '..', binary);
if (!existsSync(fullPath)) {
console.warn(`⚠️ Бинарный файл не найден: ${binary}`);
allInstalled = false;
}
});
if (allInstalled) {
console.log('✅ Все пакеты оптимизации изображений установлены корректно');
} else {
console.log(' Некоторые пакеты могут потребовать ручной установки');
console.log('💡 Попробуйте: pnpm rebuild или npm rebuild');
}

View File

@ -0,0 +1,51 @@
{
"hero": {
"title": "Современные веб-решения",
"subtitle": "Создаем качественные сайты и приложения с использованием передовых технологий"
},
"features": [
{
"icon": "speed.svg",
"title": "Высокая скорость",
"description": "Оптимизированная сборка для максимальной производительности"
},
{
"icon": "modern.svg",
"title": "Современные технологии",
"description": "Используем актуальные инструменты и подходы разработки"
},
{
"icon": "responsive.svg",
"title": "Адаптивность",
"description": "Идеальное отображение на всех устройствах и экранах"
}
],
"about": {
"title": "О нашей компании",
"description": "Мы команда профессиональных разработчиков, специализирующихся на создании современных веб-приложений."
},
"team": [
{
"name": "Анна Иванова",
"position": "Frontend разработчик",
"photo": "anna.jpg"
},
{
"name": "Максим Петров",
"position": "Backend разработчик",
"photo": "maxim.jpg"
},
{
"name": "Елена Сидорова",
"position": "UX/UI дизайнер",
"photo": "elena.jpg"
}
],
"footer": {
"description": "Создаем инновационные веб-решения для вашего бизнеса"
},
"contacts": {
"email": "info@example.com",
"phone": "+7 (999) 123-45-67"
}
}

View File

@ -0,0 +1,46 @@
{
"main": [
{
"title": "Главная",
"url": "/",
"active": true
},
{
"title": "О нас",
"url": "/about.html",
"active": false
},
{
"title": "Услуги",
"url": "/services.html",
"active": false
},
{
"title": "Контакты",
"url": "/contact.html",
"active": false
}
],
"footer": [
{
"title": "Главная",
"url": "/"
},
{
"title": "О компании",
"url": "/about.html"
},
{
"title": "Услуги",
"url": "/services.html"
},
{
"title": "Блог",
"url": "/blog.html"
},
{
"title": "Контакты",
"url": "/contact.html"
}
]
}

View File

@ -0,0 +1,54 @@
import { debounce } from '../utils/helpers.js';
/**
* Инициализация шапки сайта
*/
export const initHeader = () => {
const header = document.querySelector('.header');
const burger = document.querySelector('.header__burger');
const nav = document.querySelector('.header__nav');
if (!header) return;
// Скрытие/показ шапки при скролле
let lastScrollY = window.scrollY;
const handleScroll = debounce(() => {
const currentScrollY = window.scrollY;
if (currentScrollY > 100) {
if (currentScrollY > lastScrollY) {
header.style.transform = 'translateY(-100%)';
} else {
header.style.transform = 'translateY(0)';
}
} else {
header.style.transform = 'translateY(0)';
}
lastScrollY = currentScrollY;
}, 10);
window.addEventListener('scroll', handleScroll);
// Мобильное меню
if (burger && nav) {
burger.addEventListener('click', () => {
nav.classList.toggle('header__nav--open');
burger.classList.toggle('header__burger--active');
document.body.classList.toggle('nav-open');
});
// Закрытие меню при клике вне его
document.addEventListener('click', e => {
if (
!header.contains(e.target) &&
nav.classList.contains('header__nav--open')
) {
nav.classList.remove('header__nav--open');
burger.classList.remove('header__burger--active');
document.body.classList.remove('nav-open');
}
});
}
};

View File

@ -0,0 +1,166 @@
/**
* Модальные окна
*/
export class Modal {
constructor(selector, options = {}) {
this.modal =
typeof selector === 'string'
? document.querySelector(selector)
: selector;
if (!this.modal) {
console.warn('Модальное окно не найдено:', selector);
return;
}
this.options = {
closeOnEscape: true,
closeOnOverlay: true,
focusTrap: true,
animation: 'fade',
...options
};
this.isOpen = false;
this.previousFocus = null;
this.init();
}
init() {
this.createOverlay();
this.bindEvents();
this.setupAccessibility();
}
createOverlay() {
if (!this.modal.querySelector('.modal__overlay')) {
const overlay = document.createElement('div');
overlay.className = 'modal__overlay';
this.modal.insertBefore(overlay, this.modal.firstChild);
}
this.overlay = this.modal.querySelector('.modal__overlay');
}
bindEvents() {
// Кнопки открытия
document
.querySelectorAll(`[data-modal-open="${this.modal.id}"]`)
.forEach(btn => {
btn.addEventListener('click', e => {
e.preventDefault();
this.open();
});
});
// Кнопки закрытия
this.modal.querySelectorAll('[data-modal-close]').forEach(btn => {
btn.addEventListener('click', e => {
e.preventDefault();
this.close();
});
});
// Закрытие по оверлею
if (this.options.closeOnOverlay) {
this.overlay.addEventListener('click', () => this.close());
}
// Закрытие по Escape
if (this.options.closeOnEscape) {
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
});
}
}
setupAccessibility() {
this.modal.setAttribute('role', 'dialog');
this.modal.setAttribute('aria-modal', 'true');
if (!this.modal.getAttribute('aria-labelledby')) {
const title = this.modal.querySelector('.modal__title');
if (title) {
title.id = title.id || `modal-title-${Date.now()}`;
this.modal.setAttribute('aria-labelledby', title.id);
}
}
}
open() {
if (this.isOpen) return;
this.previousFocus = document.activeElement;
document.body.classList.add('modal-open');
this.modal.classList.add('modal--open');
this.isOpen = true;
// Фокус на модальном окне
if (this.options.focusTrap) {
this.trapFocus();
}
// Событие открытия
this.modal.dispatchEvent(new CustomEvent('modal:open'));
}
close() {
if (!this.isOpen) return;
document.body.classList.remove('modal-open');
this.modal.classList.remove('modal--open');
this.isOpen = false;
// Возврат фокуса
if (this.previousFocus) {
this.previousFocus.focus();
}
// Событие закрытия
this.modal.dispatchEvent(new CustomEvent('modal:close'));
}
trapFocus() {
const focusableElements = this.modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (firstElement) {
firstElement.focus();
}
this.modal.addEventListener('keydown', e => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
});
}
}
/**
* Инициализация всех модальных окон
*/
export const initModal = () => {
document.querySelectorAll('.modal').forEach(modal => {
new Modal(modal);
});
};

View File

@ -0,0 +1,82 @@
// import './utils/polyfills.js';
import { initHeader } from './components/header.js';
import { initModal } from './components/modal.js';
import { loadData } from './utils/dataLoader.js';
// Инициализация приложения
class App {
constructor() {
this.init();
}
async init() {
try {
// Загрузка данных
await this.loadAppData();
// Инициализация компонентов
this.initComponents();
// Обработчики событий
this.bindEvents();
console.log('✅ Приложение инициализировано');
} catch (error) {
console.error('❌ Ошибка инициализации:', error);
}
}
async loadAppData() {
try {
const data = await loadData('/src/data/content.json');
window.appData = data;
} catch (error) {
console.warn('⚠️ Не удалось загрузить данные:', error);
}
}
initComponents() {
initHeader();
initModal();
}
bindEvents() {
// Плавная прокрутка для якорных ссылок
document.addEventListener('click', e => {
const link = e.target.closest('a[href^="#"]');
if (link) {
e.preventDefault();
const target = document.querySelector(link.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}
});
// Ленивая загрузка изображений
if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
}
}
}
// Запуск приложения
document.addEventListener('DOMContentLoaded', () => {
new App();
});

View File

@ -0,0 +1,71 @@
/**
* Загрузчик JSON данных
*/
export class DataLoader {
constructor() {
this.cache = new Map();
}
/**
* Загрузка данных с кешированием
* @param {string} url - URL для загрузки
* @param {boolean} useCache - Использовать кеш
* @returns {Promise<any>}
*/
async load(url, useCache = true) {
if (useCache && this.cache.has(url)) {
return this.cache.get(url);
}
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (useCache) {
this.cache.set(url, data);
}
return data;
} catch (error) {
console.error(`Ошибка загрузки данных из ${url}:`, error);
throw error;
}
}
/**
* Загрузка нескольких источников данных
* @param {string[]} urls - Массив URL
* @returns {Promise<any[]>}
*/
async loadMultiple(urls) {
try {
const promises = urls.map(url => this.load(url));
return await Promise.all(promises);
} catch (error) {
console.error('Ошибка загрузки множественных данных:', error);
throw error;
}
}
/**
* Очистка кеша
*/
clearCache() {
this.cache.clear();
}
}
// Экземпляр загрузчика
const dataLoader = new DataLoader();
/**
* Простая функция загрузки данных
* @param {string} url - URL для загрузки
* @returns {Promise<any>}
*/
export const loadData = url => dataLoader.load(url);

View File

@ -0,0 +1,92 @@
/**
* Дебаунс функция
* @param {Function} func - Функция для дебаунса
* @param {number} wait - Время ожидания в мс
* @returns {Function}
*/
export const debounce = (func, wait) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
/**
* Троттлинг функция
* @param {Function} func - Функция для троттлинга
* @param {number} limit - Лимит времени в мс
* @returns {Function}
*/
export const throttle = (func, limit) => {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};
/**
* Получение параметров URL
* @returns {Object}
*/
export const getUrlParams = () => {
return Object.fromEntries(new URLSearchParams(window.location.search));
};
/**
* Форматирование даты
* @param {Date|string} date - Дата для форматирования
* @param {string} locale - Локаль
* @returns {string}
*/
export const formatDate = (date, locale = 'ru-RU') => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date(date));
};
/**
* Проверка поддержки WebP
* @returns {Promise<boolean>}
*/
export const supportsWebP = () => {
return new Promise(resolve => {
const webP = new Image();
webP.onload = webP.onerror = () => {
resolve(webP.height === 2);
};
webP.src =
'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
});
};
/**
* Анимация счетчика
* @param {HTMLElement} element - Элемент счетчика
* @param {number} end - Конечное значение
* @param {number} duration - Длительность анимации
*/
export const animateCounter = (element, end, duration = 2000) => {
let start = 0;
const increment = end / (duration / 16);
const timer = setInterval(() => {
start += increment;
element.textContent = Math.floor(start);
if (start >= end) {
element.textContent = end;
clearInterval(timer);
}
}, 16);
};

View File

@ -0,0 +1,85 @@
/**
* Полифиллы для старых браузеров
*/
// Intersection Observer
if (!('IntersectionObserver' in window)) {
import('intersection-observer');
}
// Резервный вариант для fetch
if (!window.fetch) {
window.fetch = async (url, options = {}) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', url);
Object.keys(options.headers || {}).forEach(key => {
xhr.setRequestHeader(key, options.headers[key]);
});
xhr.onload = () => {
resolve({
ok: xhr.status >= 200 && xhr.status < 300,
status: xhr.status,
statusText: xhr.statusText,
json: () => Promise.resolve(JSON.parse(xhr.responseText)),
text: () => Promise.resolve(xhr.responseText)
});
};
xhr.onerror = () => reject(new Error('Network Error'));
xhr.send(options.body);
});
};
}
// Object.assign полифилл
if (typeof Object.assign !== 'function') {
Object.assign = function (target, ...sources) {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
const to = Object(target);
sources.forEach(source => {
if (source != null) {
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
to[key] = source[key];
}
}
}
});
return to;
};
}
// Array.from полифилл
if (!Array.from) {
Array.from = function (arrayLike, mapFn, thisArg) {
const C = this;
const items = Object(arrayLike);
const len = parseInt(items.length) || 0;
const A = typeof C === 'function' ? Object(new C(len)) : new Array(len);
let k = 0;
while (k < len) {
const kValue = items[k];
if (mapFn) {
A[k] =
typeof thisArg === 'undefined'
? mapFn(kValue, k)
: mapFn.call(thisArg, kValue, k);
} else {
A[k] = kValue;
}
k += 1;
}
A.length = len;
return A;
};
}

View File

@ -0,0 +1,84 @@
@use "./variables" as *;
@use "./mixins" as *;
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: $font-family-base;
font-size: $font-size-base;
font-weight: $font-weight-normal;
line-height: 1.5;
color: $text-primary;
background-color: $bg-primary;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
@include container;
}
// Типографика
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 1rem;
font-family: $font-family-heading;
font-weight: $font-weight-bold;
line-height: 1.2;
color: $text-primary;
}
h1 {
font-size: $font-size-4xl;
}
h2 {
font-size: $font-size-3xl;
}
h3 {
font-size: $font-size-2xl;
}
h4 {
font-size: $font-size-xl;
}
h5 {
font-size: $font-size-lg;
}
h6 {
font-size: $font-size-base;
}
p {
margin: 0 0 1rem;
color: $text-secondary;
}
a {
color: $primary;
text-decoration: none;
transition: color $transition-base;
&:hover,
&:focus {
color: $primary-dark;
}
}
img {
max-width: 100%;
height: auto;
vertical-align: middle;
}

View File

@ -0,0 +1,131 @@
@use "./variables" as *;
// Контейнер
@mixin container {
max-width: $container-max-width;
margin: 0 auto;
padding: 0 $container-padding;
}
// Медиа-запросы
@mixin media($breakpoint) {
@if $breakpoint == sm {
@media (min-width: $breakpoint-sm) {
@content;
}
}
@if $breakpoint == md {
@media (min-width: $breakpoint-md) {
@content;
}
}
@if $breakpoint == lg {
@media (min-width: $breakpoint-lg) {
@content;
}
}
@if $breakpoint == xl {
@media (min-width: $breakpoint-xl) {
@content;
}
}
@if $breakpoint == 2xl {
@media (min-width: $breakpoint-2xl) {
@content;
}
}
}
// Флексбокс
@mixin flex(
$direction: row,
$justify: flex-start,
$align: stretch,
$wrap: nowrap
) {
display: flex;
flex-direction: $direction;
justify-content: $justify;
align-items: $align;
flex-wrap: $wrap;
}
// Центрирование
@mixin center($type: both) {
position: absolute;
@if $type == both {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
@if $type == horizontal {
left: 50%;
transform: translateX(-50%);
}
@if $type == vertical {
top: 50%;
transform: translateY(-50%);
}
}
// Скрытие текста
@mixin visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
// Кнопка сброса
@mixin button-reset {
background: none;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
outline: none;
&:focus-visible {
outline: 2px solid $primary;
outline-offset: 2px;
}
}
// Ссылка сброса
@mixin link-reset {
color: inherit;
text-decoration: none;
&:hover,
&:focus {
text-decoration: none;
}
}
// Грид
@mixin grid($columns: 1, $gap: 1rem) {
display: grid;
grid-template-columns: repeat($columns, 1fr);
gap: $gap;
}
// Обрезка текста
@mixin text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// Многострочное обрезание
@mixin text-clamp($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}

View File

@ -0,0 +1,75 @@
// Цвета
$primary: #3b82f6;
$primary-dark: #1d4ed8;
$primary-light: #93c5fd;
$secondary: #10b981;
$accent: #f59e0b;
$text-primary: #111827;
$text-secondary: #6b7280;
$text-muted: #9ca3af;
$bg-primary: #ffffff;
$bg-secondary: #f9fafb;
$bg-dark: #111827;
$border-color: #e5e7eb;
$border-color-hover: #d1d5db;
// Типографика
$font-family-base:
"Inter",
-apple-system,
BlinkMacSystemFont,
sans-serif;
$font-family-heading: "Poppins", sans-serif;
$font-size-xs: 0.75rem;
$font-size-sm: 0.875rem;
$font-size-base: 1rem;
$font-size-lg: 1.125rem;
$font-size-xl: 1.25rem;
$font-size-2xl: 1.5rem;
$font-size-3xl: 1.875rem;
$font-size-4xl: 2.25rem;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
// Размеры
$container-max-width: 1200px;
$container-padding: 1rem;
// Брейкпоинты
$breakpoint-sm: 576px;
$breakpoint-md: 768px;
$breakpoint-lg: 992px;
$breakpoint-xl: 1200px;
$breakpoint-2xl: 1400px;
// Z-index
$z-dropdown: 1000;
$z-modal: 1050;
$z-tooltip: 1070;
// Анимации
$transition-base: 0.3s ease;
$transition-fast: 0.15s ease;
$transition-slow: 0.5s ease;
// Радиусы
$border-radius-sm: 0.25rem;
$border-radius: 0.375rem;
$border-radius-lg: 0.5rem;
$border-radius-xl: 0.75rem;
// Тени
$shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
$shadow:
0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1);
$shadow-lg:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);

View File

@ -0,0 +1,6 @@
@import "./_buttons.scss";
@import "./_footer.scss";
@import "./_header.scss";
@import "./_main.scss";
@import "./_modal.scss";
@import "./_section.scss";

View File

@ -0,0 +1,84 @@
@use "sass:color";
@use "../variables" as *;
@use "../mixins" as *;
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
border: 1px solid transparent;
border-radius: $border-radius;
font-size: $font-size-base;
font-weight: $font-weight-medium;
line-height: 1;
text-align: center;
text-decoration: none;
cursor: pointer;
transition: all $transition-base;
user-select: none;
&:focus {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px $primary-light;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
// Основная кнопка
&--primary {
color: white;
background-color: $primary;
border-color: $primary;
&:hover:not(:disabled) {
background-color: $primary-dark;
border-color: $primary-dark;
}
}
// Вторичная кнопка
&--secondary {
color: $primary;
background-color: transparent;
border-color: $primary;
&:hover:not(:disabled) {
color: white;
background-color: $primary;
}
}
// Кнопка успеха
&--success {
color: white;
background-color: $secondary;
border-color: $secondary;
&:hover:not(:disabled) {
background-color: color.adjust($secondary, $lightness: -10%);
border-color: color.adjust($secondary, $lightness: -10%);
}
}
// Размеры
&--sm {
padding: 0.5rem 1rem;
font-size: $font-size-sm;
}
&--lg {
padding: 1rem 2rem;
font-size: $font-size-lg;
}
// Полная ширина
&--block {
display: flex;
width: 100%;
}
}

View File

@ -0,0 +1,10 @@
@use "../variables" as *;
@use "../mixins" as *;
.b-footer {
background-color: $bg-dark;
color: white;
margin-top: auto;
padding: 2rem 0;
text-align: center;
}

View File

@ -0,0 +1,32 @@
@use "../variables" as *;
@use "../mixins" as *;
.b-header {
background-color: $bg-dark;
color: white;
padding: 2rem 0;
text-align: center;
position: sticky;
top: 0;
z-index: $z-dropdown;
}
.nav-list {
@include flex(row, center, center);
gap: 2rem;
margin: 0;
padding: 0;
list-style: none;
&__link {
@include link-reset;
font-weight: $font-weight-medium;
padding: 0.5rem 0;
transition: color $transition-base;
&:hover,
&--active {
color: $primary;
}
}
}

View File

@ -0,0 +1,22 @@
.l-wrapper {
position: relative;
min-height: 1%;
height: auto;
display: flex;
flex-direction: column;
min-height: 100dvh;
// overflow: hidden;
main {
// overflow: hidden;
display: block;
width: 100%;
position: relative;
flex: 1;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
-webkit-align-self: stretch;
-ms-flex-item-align: stretch;
align-self: stretch;
}
}

View File

@ -0,0 +1,100 @@
@use "../variables" as *;
@use "../mixins" as *;
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: $z-modal;
// z-index: 1050;
opacity: 0;
visibility: hidden;
transition: all $transition-base;
&--open {
opacity: 1;
visibility: visible;
}
&__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(black, 0.5);
cursor: pointer;
}
&__container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 90vw;
max-height: 90vh;
overflow-y: auto;
cursor: default;
}
&__content {
background: white;
border-radius: $border-radius-lg;
box-shadow: $shadow-lg;
padding: 2rem;
position: relative;
@include media(md) {
padding: 3rem;
min-width: 500px;
}
}
&__header {
@include flex(row, space-between, center);
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid $border-color;
}
&__title {
margin: 0;
font-size: $font-size-2xl;
font-weight: $font-weight-bold;
}
&__close {
@include button-reset;
width: 32px;
height: 32px;
@include flex(row, center, center);
border-radius: 50%;
background: $bg-secondary;
color: $text-secondary;
font-size: 18px;
transition: all $transition-base;
&:hover {
background: $border-color;
color: $text-primary;
}
}
&__body {
margin-bottom: 1.5rem;
}
&__footer {
@include flex(row, flex-end, center);
gap: 1rem;
padding-top: 1rem;
border-top: 1px solid $border-color;
}
}
// Запрет скролла при открытом модальном окне
.modal-open {
overflow: hidden;
}

View File

@ -0,0 +1,5 @@
.section {
position: relative;
width: 100%;
padding: 3rem;
}

View File

@ -0,0 +1,20 @@
// Используем @use вместо @import (современный стандарт)
// @use 'sass:color';
// @use './variables' as *;
// @use './mixins' as *;
@use "./base";
// Компоненты
@use "components/main";
@use "components/header";
@use "components/footer";
@use "components/modal";
@use "components/section";
@use "components/buttons";
// // Страницы
// // @import 'pages/home';
// // @import 'pages/about';
// // Утилиты
@use "utilities/text";

View File

@ -0,0 +1 @@
@import "./_text.scss";

View File

@ -0,0 +1,3 @@
.text-center {
text-align: center;
}

View File

@ -0,0 +1,3 @@
<footer class="b-footer">
copyright &copy; {{ "now"|date("Y") }}
</footer>

View File

@ -0,0 +1,3 @@
<header class="b-header">
header...
</header>

View File

@ -0,0 +1,18 @@
<div class="modal" id="example-modal">
<div class="modal__overlay"></div>
<div class="modal__container">
<div class="modal__content">
<div class="modal__header">
<h2 class="modal__title">Заголовок модального окна</h2>
<button class="modal__close" data-modal-close>&times;</button>
</div>
<div class="modal__body">
<p>Содержимое модального окна...</p>
</div>
<div class="modal__footer">
<button class="btn btn--secondary" data-modal-close>Отмена</button>
<button class="btn btn--primary">Сохранить</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="{{ lang | default('ru') }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title | default('Мой сайт') }}</title>
<link rel="stylesheet" href="/src/styles/main.scss">
{% block head %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
{% block modal %}
{% include '../components/modal.twig' %}
{% endblock %}
<div class="l-wrapper">
{% block header %}
{% include '../components/header.twig' %}
{%endblock %}
<main class="main">
{% block content %}{% endblock %}
</main>
{% block footer %}
{% include '../components/footer.twig' %}
{% endblock %}
</div>
<script type="module" src="/src/js/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,30 @@
{% extends 'layouts/main.twig' %}
{% block content %}
<section class="about">
<div class="container">
<h1 class="about__title">{{ data.content.about.title }}</h1>
<div class="about__content">
<div class="about__text">
<p>{{ data.content.about.description }}</p>
</div>
<div class="about__image">
<img src="/src/images/about-hero.jpg" alt="О нас">
</div>
</div>
<div class="team">
<h2 class="team__title">Наша команда</h2>
<div class="team__grid">
{% for member in data.content.team %}
<div class="team-card">
<img class="team-card__photo" src="/src/images/team/{{ member.photo }}" alt="{{ member.name }}">
<h3 class="team-card__name">{{ member.name }}</h3>
<p class="team-card__position">{{ member.position }}</p>
</div>
{% endfor %}
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'layouts/main.twig' %}
{% block content %}
<section class="section text-center">
<div class="container">
<h1 class="sectiont__title">Пример модального окна</h1>
<button data-modal-open="example-modal">Открыть модальное окно</button>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends 'layouts/main.twig' %}
{% block content %}
<section class="section text-center">
<div class="container">
<h1 class="section__title">Главная страница</h1>
<h1 class="sectiont__title">Пример модального окна</h1>
<button data-modal-open="example-modal">Открыть модальное окно</button>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,168 @@
import { defineConfig } from 'vite';
import vituum from 'vituum';
import pug from '@vituum/vite-plugin-pug';
import twig from '@vituum/vite-plugin-twig';
import { resolve } from 'path';
import { glob } from 'glob';
import basicSsl from '@vitejs/plugin-basic-ssl';
process.on('unhandledRejection', (reason, promise) => {
console.warn('⚠️ Unhandled Promise Rejection:', reason);
// Не останавливать процесс - просто логировать
});
// Автоматическое определение всех Twig страниц
const getTwigPages = () => {
const pages = glob.sync('src/twig/pages/**/*.twig');
const input = {};
pages.forEach(page => {
const name = page
.replace('src/twig/pages/', '')
.replace('.twig', '')
.replace(/\//g, '-'); // Заменяем слеши на дефисы для вложенных папок
input[name] = resolve(__dirname, page);
});
console.log('📄 Найденные страницы для сборки:', Object.keys(input));
return input;
};
export default defineConfig({
plugins: [
vituum({
pages: {
dir: './src/twig/pages', // Указываем правильный путь к Twig страницам
formats: ['twig']
},
// Настройки для стабильной работы
options: {
reload: true,
verbose: true
}
}),
// Twig конфигурация
twig({
root: './src/twig',
namespaces: {
layouts: './src/twig/layouts',
components: './src/twig/components',
data: './src/data'
},
// Более мягкие настройки для разработки
options: {
autoescape: false,
strict_vars: false
}
}),
// Pug для других страниц (если нужно)
pug({
root: './src',
options: {
pretty: true,
basedir: './src'
}
}),
basicSsl()
],
server: {
host: true,
port: 3000,
https: true,
open: true,
// Настройки для стабильной работы HMR
hmr: {
overlay: true
},
// Не останавливать сервер при ошибках
middlewareMode: false
},
// Настройки сборки
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false, // Отключаем sourcemaps для чистоты
rollupOptions: {
input: getTwigPages(), // Автоматически находим все страницы
output: {
// Группируем CSS в один файл
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
if (ext === 'css') {
return 'assets/css/style-[hash].css';
}
if (/\.(png|jpe?g|svg|gif|tiff|bmp|ico)$/i.test(assetInfo.name)) {
return 'assets/images/[name]-[hash][extname]';
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return 'assets/fonts/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
// Группируем JS
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js'
}
},
// Минификация
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
// Минимальная обработка ошибок
esbuild: {
// Продолжать работу при ошибках
logOverride: { 'this-is-undefined-in-esm': 'silent' }
},
// Обработка ошибок
optimizeDeps: {
force: true
},
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: ['legacy-js-api']
// silenceDeprecations: ['legacy-js-api', 'import', 'global-builtin', 'color-functions'],
// quietDeps: true,
// logger: {
// warn: () => {}
// }
// additionalData: `
// @import "./src/styles/_vars.scss";
// @import "./src/styles/_scss";
// `
}
}
},
resolve: {
alias: {
'@': resolve(process.cwd(), 'src'),
'@twig': resolve(process.cwd(), 'src/twig'),
'@styles': resolve(process.cwd(), 'src/styles'),
'@js': resolve(process.cwd(), 'src/js'),
'@images': resolve(process.cwd(), 'src/images'),
'@data': resolve(process.cwd(), 'src/data')
}
}
});

View File

@ -0,0 +1,143 @@
import { defineConfig } from 'vite';
import vituum from 'vituum';
import pug from '@vituum/vite-plugin-pug';
import twig from '@vituum/vite-plugin-twig';
import { resolve } from 'path';
import basicSsl from '@vitejs/plugin-basic-ssl';
process.on('unhandledRejection', (reason, promise) => {
console.warn('⚠️ Unhandled Promise Rejection:', reason);
// Не останавливать процесс - просто логировать
});
export default defineConfig({
plugins: [
vituum({
pages: {
dir: './src/twig/pages', // Указываем правильный путь к Twig страницам
formats: ['twig']
},
// Настройки для стабильной работы
options: {
reload: true,
verbose: true
}
}),
// Twig конфигурация
twig({
root: './src/twig',
namespaces: {
layouts: './src/twig/layouts',
components: './src/twig/components',
data: './src/data'
},
// Более мягкие настройки для разработки
options: {
autoescape: false,
strict_vars: false
}
}),
// Pug для других страниц (если нужно)
pug({
root: './src',
options: {
pretty: true,
basedir: './src'
}
}),
basicSsl()
],
server: {
host: true,
port: 3000,
https: true,
open: true,
// Настройки для стабильной работы HMR
hmr: {
overlay: true
},
// Не останавливать сервер при ошибках
middlewareMode: false
},
// Настройки сборки
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false, // Отключаем sourcemaps для чистоты
rollupOptions: {
output: {
// Группируем CSS в один файл
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
if (ext === 'css') {
return 'assets/css/style-[hash].css'; // Один CSS файл
}
if (/\.(png|jpe?g|svg|gif|tiff|bmp|ico)$/i.test(assetInfo.name)) {
return 'assets/images/[name]-[hash][extname]';
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return 'assets/fonts/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
},
// Группируем JS
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js'
}
},
// Минификация
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
// Минимальная обработка ошибок
esbuild: {
// Продолжать работу при ошибках
logOverride: { 'this-is-undefined-in-esm': 'silent' }
},
// Обработка ошибок
optimizeDeps: {
force: true
},
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: ['legacy-js-api']
// additionalData: `
// @import "./src/styles/_vars.scss";
// @import "./src/styles/_scss";
// `
}
}
},
resolve: {
alias: {
'@': resolve(process.cwd(), 'src'),
'@twig': resolve(process.cwd(), 'src/twig'),
'@styles': resolve(process.cwd(), 'src/styles'),
'@js': resolve(process.cwd(), 'src/js'),
'@images': resolve(process.cwd(), 'src/images'),
'@data': resolve(process.cwd(), 'src/data')
}
}
});

View File

@ -0,0 +1,50 @@
import { defineConfig } from 'vite';
import vituum from 'vituum';
import pug from '@vituum/vite-plugin-pug';
import twig from '@vituum/vite-plugin-twig';
import { resolve } from 'path';
import basicSsl from '@vitejs/plugin-basic-ssl';
export default defineConfig({
plugins: [
vituum(),
pug({
root: './src'
}),
twig({
root: './src',
namespaces: {
layouts: './src/layouts',
components: './src/components'
}
}),
basicSsl()
],
server: {
host: true,
port: 3000,
https: true,
open: true
},
css: {
preprocessorOptions: {
scss: {
additionalData: `
@import "./src/styles/_vars.scss";
@import "./src/styles/_scss";
`
}
}
},
resolve: {
alias: {
'@': resolve(process.cwd(), 'src')
}
}
});

View File

@ -0,0 +1,31 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'prettier'
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'no-console': 'warn',
'no-unused-vars': 'warn',
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'error',
'prefer-arrow-callback': 'error',
'prefer-template': 'error',
'template-curly-spacing': 'error',
'quotes': ['error', 'single'],
'semi': ['error', 'always']
},
globals: {
MicroModal: 'readonly',
Toastify: 'readonly'
}
};

91
vitekit-claude/.gitignore vendored Normal file
View File

@ -0,0 +1,91 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Production builds
dist/
build/
# Development
.cache/
.parcel-cache/
.vite/
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# ESLint cache
.eslintcache
# Stylelint cache
.stylelintcache
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary folders
tmp/
temp/

View File

@ -0,0 +1,19 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf",
"overrides": [
{
"files": "*.twig",
"options": {
"parser": "html"
}
}
]
}

View File

@ -0,0 +1,32 @@
module.exports = {
extends: [
'stylelint-config-standard-scss'
],
plugins: [
'stylelint-order'
],
rules: {
'order/properties-alphabetical-order': true,
'scss/at-import-partial-extension': null,
'scss/at-import-no-partial-leading-underscore': null,
'selector-class-pattern': null,
'custom-property-pattern': null,
'keyframes-name-pattern': null,
'scss/dollar-variable-pattern': null,
'scss/percent-placeholder-pattern': null,
'scss/at-mixin-pattern': null,
'scss/at-function-pattern': null,
'declaration-empty-line-before': null,
'rule-empty-line-before': [
'always-multi-line',
{
except: ['first-nested'],
ignore: ['after-comment']
}
]
},
ignoreFiles: [
'dist/**/*',
'node_modules/**/*'
]
};

190
vitekit-claude/CLAUDE.md Normal file
View File

@ -0,0 +1,190 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
ViteKit Universal - современный универсальный сборщик проектов с Vite + Twig + компонентами. Полнофункциональная система для быстрой разработки веб-приложений с готовыми UI компонентами и модульной архитектурой.
## Common Commands
```bash
# Development
npm run dev # Запуск сервера разработки (localhost:3000)
npm run build # Сборка для продакшена
npm run preview # Предварительный просмотр сборки
# Code Quality
npm run lint # ESLint проверка JavaScript
npm run lint:fix # Автоисправление ESLint
npm run lint:css # Stylelint проверка SCSS
npm run lint:css:fix # Автоисправление Stylelint
npm run format # Prettier форматирование всех файлов
# Utilities
npm run clean # Очистка папки dist
```
## Project Architecture
### Technology Stack
- **Vite** - быстрый сборщик с HMR
- **@vituum/vite-plugin-twig** - поддержка Twig шаблонов
- **SCSS** - CSS препроцессор с модульной архитектурой
- **Vanilla JavaScript** - нативный JS без фреймворков
- **MicroModal** - доступные модальные окна (~1.9kb)
- **Toastify** - уведомления
### Directory Structure
```
src/
├── components/ # UI компоненты (modal, tabs, accordion, toast)
├── layouts/ # Twig шаблоны (base.twig)
├── pages/ # Страницы (index.twig, components-demo.twig)
├── data/ # JSON данные для Twig
├── assets/ # Статические ресурсы (images, icons, fonts)
├── styles/ # SCSS архитектура
│ ├── abstracts/ # Variables, mixins
│ ├── base/ # Reset, typography
│ ├── components/ # Component styles
│ ├── layout/ # Layout styles
│ └── pages/ # Page-specific styles
└── scripts/ # JavaScript модули
├── components/ # UI component logic
└── utils/ # Utilities and helpers
```
### Component System
- **Модульная архитектура** - каждый компонент изолирован
- **Data attributes** - инициализация через `data-*` атрибуты
- **Event-driven** - компоненты используют custom events
- **Accessibility** - полная поддержка ARIA и клавиатурной навигации
- **Responsive** - адаптивное поведение (табы → аккордеон на мобильных)
### SCSS Architecture
- **Abstracts**: переменные, миксины, функции
- **Base**: reset, типографика, базовые стили
- **Components**: стили UI компонентов
- **Layout**: сетка, контейнеры, навигация
- **Pages**: специфичные стили страниц
### Key Features
- Автоматическая оптимизация изображений
- SVG sprite injection для иконок
- Live reload и HMR
- Code splitting и tree shaking
- Legacy browser support
- Automated linting с pre-commit hooks
## Development Guidelines
### Adding New Components
1. Создать SCSS в `src/styles/components/_component.scss`
2. Создать JS в `src/scripts/components/component.js`
3. Добавить импорт в `src/styles/main.scss`
4. Добавить в компонентную систему через data attributes
### Twig Development
- Использовать JSON данные из `src/data/` для динамического контента
- Шаблоны наследуются от `layouts/base.twig`
- Все страницы автоматически обрабатываются Vite
### SCSS Best Practices
- Использовать предопределенные миксины для медиа-запросов
- Следовать БЭМ методологии для классов
- Использовать CSS custom properties для динамических значений
- Все цвета и размеры через переменные
### JavaScript Guidelines
- ES6+ modules с импортами
- Page Controller Pattern для архитектуры
- Event-driven компоненты через EventBus
- Класс-ориентированные компоненты наследуются от Component
- Performance monitoring встроен
- Graceful degradation без JavaScript
## Testing & Deployment
### Quality Assurance
- ESLint + Prettier для JavaScript
- Stylelint для SCSS
- Husky pre-commit hooks
- Автоматическое форматирование
### Build Process
- Vite handles bundling and optimization
- Automatic asset optimization (images, SVG)
- CSS/JS minification and tree shaking
- Legacy browser polyfills via @vitejs/plugin-legacy
### Performance Optimization
- Image optimization с vite-plugin-image-optimizer
- SVG sprite generation для иконок
- CSS/JS code splitting
- Preload critical resources
## Page Controller Architecture
### Создание нового page controller
```javascript
// src/scripts/pages/mypage.js
import { Component } from '../core/Component.js';
import { eventBus } from '../core/EventBus.js';
export default class MyPage extends Component {
constructor() {
super(document.body);
}
beforeInit() {
// Инициализация перед загрузкой
}
bindEvents() {
// Привязка событий страницы
}
afterInit() {
// Действия после инициализации
}
}
```
### Page detection
- Автоматическое определение по filename (mypage.html → mypage.js)
- Или через data-page атрибут: `<body data-page="custom-name">`
### Component API
```javascript
// Создание компонента
const myComponent = Component.create(element, options);
// EventBus для связи между компонентами
eventBus.on('custom:event', callback);
eventBus.emit('custom:event', data);
// Performance monitoring
performanceMonitor.mark('my-action');
performanceMonitor.measure('my-action', 'start-mark', 'end-mark');
```
### Utilities
```javascript
// DOM utilities
import { ready, createElement, scrollToElement } from '@scripts/utils/dom.js';
// Performance utilities
import { debounce, throttle, memoize } from '@scripts/utils/performance.js';
// Helper utilities
import { deepClone, formatDate, copyToClipboard } from '@scripts/utils/helpers.js';
```
## Special Considerations
- Команды терминала запускаются в фоновом режиме (не используйте циклические команды)
- Все пути используют алиасы (@, @styles, @components, @scripts, @assets)
- Responsive-first подход с мобильными брейкпоинтами
- Accessibility-first разработка с ARIA и keyboard support
- Page controllers загружаются автоматически по имени файла/страницы
- Используйте EventBus для связи между компонентами разных страниц

290
vitekit-claude/README.md Normal file
View File

@ -0,0 +1,290 @@
# ViteKit Universal
Современный универсальный сборщик проектов с Vite + Twig + компонентами для быстрой разработки веб-приложений.
## 🚀 Особенности
- **Быстрая сборка** с Vite
- **Шаблонизация** с Twig
- **Готовые UI компоненты** (модалы, табы, аккордеон, уведомления)
- **Адаптивная верстка** с SCSS и Flexbox/Grid
- **Оптимизация изображений** и SVG спрайты
- **Качество кода** с ESLint, Prettier, Stylelint
- **Модульная архитектура**
## 📦 Установка и запуск
```bash
# Установка зависимостей
npm install
# Запуск сервера разработки
npm run dev
# Сборка для продакшена
npm run build
# Предварительный просмотр сборки
npm run preview
```
## 🏗️ Структура проекта
```
src/
├── components/ # UI компоненты
│ ├── ui/ # Базовые компоненты
│ └── blocks/ # Блоки страниц
├── layouts/ # Шаблоны страниц
├── pages/ # Страницы Twig
├── data/ # JSON данные для Twig
├── assets/ # Статические ресурсы
│ ├── images/ # Изображения
│ ├── icons/ # SVG иконки для спрайтов
│ └── fonts/ # Шрифты
├── styles/ # SCSS стили
│ ├── abstracts/ # Переменные, миксины
│ ├── base/ # Сброс стилей, типографика
│ ├── components/ # Стили компонентов
│ ├── layout/ # Стили макета
│ └── pages/ # Стили страниц
└── scripts/ # JavaScript
├── components/ # JS компоненты
└── utils/ # Утилиты
```
## 🎯 Компоненты
### Toast Уведомления ✨
- **Исправлены стили позиционирования** - правильный z-index и анимации
- **Автоматическое скрытие** через настраиваемое время
- **Адаптивность** для мобильных устройств
- **Типизация**: success, error, warning, info
```javascript
ToastComponent.success('Операция успешна!', {
title: 'Готово!',
duration: 4000
});
```
### Модальные окна (MicroModal)
- Доступные модальные окна с ARIA
- Анимации открытия/закрытия
- Поддержка клавиатуры и фокуса
```html
<button data-micromodal-trigger="modal-id">Открыть модал</button>
```
### Табы (Адаптивные)
- **Автоматическое превращение в аккордеон** на мобильных
- Клавиатурная навигация (Arrow keys, Home, End)
- Плавные анимации переключения
```html
<div class="tabs" data-tabs>
<div class="tabs__nav">
<button class="tabs__tab tabs__tab--active">Вкладка 1</button>
</div>
<div class="tabs__content">
<div class="tabs__panel tabs__panel--active">Содержимое</div>
</div>
</div>
```
### Аккордеон
- Плавные CSS анимации с автоматическим расчетом высоты
- Поддержка множественного раскрытия
- Полная клавиатурная навигация и ARIA
```html
<div class="accordion" data-accordion>
<div class="accordion__item">
<button class="accordion__header">Заголовок</button>
<div class="accordion__content">Содержимое</div>
</div>
</div>
```
## 🎨 SCSS Утилиты
### Миксины для медиа-запросов
```scss
@include media-up('md') { /* >= 768px */ }
@include media-down('lg') { /* < 992px */ }
@include media-only('sm') { /* только для small */ }
```
### Flexbox утилиты
```scss
@include flex-center; // выравнивание по центру
@include flex-between; // space-between
@include flex-column; // flex-direction: column
```
### Grid утилиты
```scss
@include grid-container(12, $spacing-base); // 12 колонок
@include grid-column(6); // span 6 колонок
```
## 🔧 Конфигурация
### Vite
Настроен для работы с:
- Twig шаблонами
- SCSS с автоимпортом переменных
- Оптимизацией изображений
- SVG спрайтами
- Legacy поддержкой для старых браузеров
### Linting
- **ESLint** для JavaScript
- **Stylelint** для SCSS
- **Prettier** для форматирования
- **Husky** для pre-commit хуков
## 📱 Адаптивность
Проект использует mobile-first подход с брейкпоинтами:
- `sm`: 576px
- `md`: 768px
- `lg`: 992px
- `xl`: 1200px
- `2xl`: 1400px
## 🏗️ Page Controller Архитектура
ViteKit использует **Page Controller Pattern** для организации кода по страницам:
### Автоматическая загрузка
```javascript
// Файл: src/scripts/pages/about.js
// Загружается автоматически для about.html
export default class AboutPage extends Component {
constructor() {
super(document.body);
}
beforeInit() {
// Логика до инициализации
}
bindEvents() {
// События страницы
this.addEventListener('click', this.handleClick);
}
afterInit() {
// Логика после инициализации
}
}
```
### EventBus для связи компонентов
```javascript
import { eventBus } from '../core/EventBus.js';
// Подписка на события
eventBus.on('user:login', this.onUserLogin.bind(this));
// Испускание событий
eventBus.emit('page:ready', { page: 'home' });
```
### Утилиты производительности
```javascript
import { debounce, throttle, memoize } from '@scripts/utils/performance.js';
// Debounce для поиска
const search = debounce(this.performSearch.bind(this), 300);
// Throttle для скролла
const onScroll = throttle(this.updateScrollPosition.bind(this), 16);
// Мемоизация тяжелых вычислений
const expensiveCalculation = memoize(this.calculate.bind(this));
```
## 🎛️ API компонентов
### TabsComponent
```javascript
const tabs = new TabsComponent(element, {
activeClass: 'tabs__tab--active',
keyboardNavigation: true,
autoResponsive: true
});
tabs.next(); // следующая вкладка
tabs.prev(); // предыдущая вкладка
tabs.goTo(2); // перейти к вкладке по индексу
```
### AccordionComponent
```javascript
const accordion = new AccordionComponent(element, {
allowMultiple: false,
animationDuration: 300,
keyboardNavigation: true
});
accordion.open(0); // открыть первый элемент
accordion.close(0); // закрыть первый элемент
accordion.closeAll(); // закрыть все элементы
```
### ToastComponent
```javascript
// Базовые методы
ToastComponent.success('Успех!');
ToastComponent.error('Ошибка!');
ToastComponent.warning('Предупреждение!');
ToastComponent.info('Информация');
// Расширенные возможности
ToastComponent.promise(fetchData(), {
loading: 'Загрузка...',
success: 'Данные загружены!',
error: 'Ошибка загрузки'
});
ToastComponent.confirm('Удалить элемент?').then(confirmed => {
if (confirmed) {
// Действие подтверждено
}
});
```
## 🚀 Развертывание
```bash
# Сборка для продакшена
npm run build
# Файлы сборки будут в папке dist/
```
## 🤝 Вклад в проект
1. Fork проект
2. Создайте feature ветку
3. Commit изменения
4. Push в ветку
5. Создайте Pull Request
## 📝 Лицензия
MIT License - смотри файл [LICENSE](LICENSE) для деталей.
## 🛠️ Технологии
- [Vite](https://vitejs.dev/) - Сборщик
- [Twig](https://twig.symfony.com/) - Шаблонизатор
- [SCSS](https://sass-lang.com/) - CSS препроцессор
- [MicroModal](https://micromodal.vercel.app/) - Модальные окна
- [Toastify](https://apvarun.github.io/toastify-js/) - Уведомления
- [ESLint](https://eslint.org/) - Линтер JavaScript
- [Prettier](https://prettier.io/) - Форматирование кода

View File

@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Демонстрация компонентов - ViteKit Universal</title>
<link rel="stylesheet" href="/src/styles/main.scss">
</head>
<body data-page="showcase">
<header class="header">
<div class="container">
<nav class="nav">
<div class="nav__brand">
<a href="/" class="nav__logo">ViteKit Universal</a>
</div>
<ul class="nav__menu">
<li class="nav__item">
<a href="/" class="nav__link">Главная</a>
</li>
<li class="nav__item">
<a href="/components-demo.html" class="nav__link nav__link--active">Компоненты</a>
</li>
</ul>
</nav>
</div>
</header>
<main class="main">
<div class="demo-header">
<div class="container">
<h1>Демонстрация компонентов</h1>
<p>Интерактивные примеры всех доступных UI компонентов</p>
</div>
</div>
<!-- Toast Уведомления -->
<section class="showcase-section">
<div class="container">
<h2>Toast Уведомления</h2>
<p>Современные всплывающие уведомления с автоматическим скрытием</p>
<div class="demo-grid">
<button class="btn btn--primary" data-toast-trigger="success"
data-toast-title="Успех!" data-toast-message="Операция выполнена успешно">
Показать Success
</button>
<button class="btn btn--danger" data-toast-trigger="error"
data-toast-title="Ошибка!" data-toast-message="Что-то пошло не так">
Показать Error
</button>
<button class="btn btn--warning" data-toast-trigger="warning"
data-toast-title="Внимание!" data-toast-message="Будьте осторожны">
Показать Warning
</button>
<button class="btn btn--info" data-toast-trigger="info"
data-toast-title="Информация" data-toast-message="Полезная информация">
Показать Info
</button>
</div>
<div class="demo-code">
<pre><code>// Показать toast уведомление
ToastComponent.success('Операция успешна!', {
title: 'Готово!',
duration: 4000
});
// Использование через data-атрибуты
&lt;button data-toast-trigger="success"
data-toast-title="Успех!"
data-toast-message="Операция выполнена"&gt;
Показать уведомление
&lt;/button&gt;</code></pre>
</div>
</div>
</section>
<!-- Модальные окна -->
<section class="showcase-section">
<div class="container">
<h2>Модальные окна</h2>
<p>Доступные модальные окна с поддержкой клавиатуры и фокуса</p>
<div class="demo-grid">
<button class="btn btn--primary" data-micromodal-trigger="modal-demo">
Открыть модальное окно
</button>
</div>
<div class="demo-code">
<pre><code>// Открыть модальное окно программно
MicroModal.show('modal-id');
// Использование через data-атрибуты
&lt;button data-micromodal-trigger="modal-id"&gt;
Открыть модал
&lt;/button&gt;</code></pre>
</div>
</div>
</section>
<!-- Табы -->
<section class="showcase-section">
<div class="container">
<h2>Адаптивные табы</h2>
<p>Табы, которые превращаются в аккордеон на мобильных устройствах</p>
<div class="tabs" data-tabs>
<div class="tabs__nav">
<button class="tabs__tab tabs__tab--active" data-tab="demo-tab-1">Первая вкладка</button>
<button class="tabs__tab" data-tab="demo-tab-2">Вторая вкладка</button>
<button class="tabs__tab" data-tab="demo-tab-3">Третья вкладка</button>
</div>
<div class="tabs__content">
<div class="tabs__panel tabs__panel--active" id="demo-tab-1-panel" data-panel="demo-tab-1">
<p>Содержимое первой вкладки. Здесь может быть любой контент.</p>
</div>
<div class="tabs__panel" id="demo-tab-2-panel" data-panel="demo-tab-2">
<p>Содержимое второй вкладки с другой информацией.</p>
</div>
<div class="tabs__panel" id="demo-tab-3-panel" data-panel="demo-tab-3">
<p>Содержимое третьей вкладки для демонстрации функциональности.</p>
</div>
</div>
</div>
<div class="demo-code">
<pre><code>// HTML структура табов
&lt;div class="tabs" data-tabs&gt;
&lt;div class="tabs__nav"&gt;
&lt;button class="tabs__tab tabs__tab--active" data-tab="tab-1"&gt;
Первая вкладка
&lt;/button&gt;
&lt;/div&gt;
&lt;div class="tabs__content"&gt;
&lt;div class="tabs__panel tabs__panel--active" data-panel="tab-1"&gt;
Содержимое вкладки
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</code></pre>
</div>
</div>
</section>
<!-- Аккордеон -->
<section class="showcase-section">
<div class="container">
<h2>Аккордеон</h2>
<p>Складывающиеся секции с плавными анимациями</p>
<div class="accordion" data-accordion>
<div class="accordion__item">
<button class="accordion__header" data-accordion-trigger="demo-acc-1" aria-expanded="false">
<span>Что такое ViteKit Universal?</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content" id="demo-acc-1-content" data-accordion-content="demo-acc-1" aria-hidden="true">
<div class="accordion__body">
<p>ViteKit Universal - это современный универсальный сборщик проектов, который объединяет Vite, Twig, SCSS и готовые UI компоненты для быстрой разработки.</p>
</div>
</div>
</div>
<div class="accordion__item">
<button class="accordion__header" data-accordion-trigger="demo-acc-2" aria-expanded="false">
<span>Какие компоненты включены?</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content" id="demo-acc-2-content" data-accordion-content="demo-acc-2" aria-hidden="true">
<div class="accordion__body">
<p>В комплект входят: модальные окна, табы, аккордеон, toast уведомления, формы, кнопки и многое другое.</p>
</div>
</div>
</div>
<div class="accordion__item">
<button class="accordion__header" data-accordion-trigger="demo-acc-3" aria-expanded="false">
<span>Как начать использовать?</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content" id="demo-acc-3-content" data-accordion-content="demo-acc-3" aria-hidden="true">
<div class="accordion__body">
<p>Просто клонируйте репозиторий, установите зависимости командой <code>npm install</code> и запустите <code>npm run dev</code>.</p>
</div>
</div>
</div>
</div>
<div class="demo-code">
<pre><code>// HTML структура аккордеона
&lt;div class="accordion" data-accordion&gt;
&lt;div class="accordion__item"&gt;
&lt;button class="accordion__header" data-accordion-trigger="acc-1"&gt;
&lt;span&gt;Заголовок&lt;/span&gt;
&lt;span class="accordion__icon"&gt;&lt;/span&gt;
&lt;/button&gt;
&lt;div class="accordion__content" data-accordion-content="acc-1"&gt;
&lt;div class="accordion__body"&gt;
Содержимое аккордеона
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</code></pre>
</div>
</div>
</section>
</main>
<!-- Модальное окно для демонстрации -->
<div class="modal micromodal-slide" id="modal-demo" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true">
<header class="modal__header">
<h2 class="modal__title">Демонстрационное модальное окно</h2>
<button class="modal__close" aria-label="Закрыть" data-micromodal-close></button>
</header>
<main class="modal__content">
<p>Это пример модального окна с полной поддержкой accessibility:</p>
<ul>
<li>✅ Управление клавиатурой (Tab, Esc)</li>
<li>✅ Фокус и ARIA атрибуты</li>
<li>✅ Плавные анимации</li>
<li>✅ Закрытие по клику вне окна</li>
</ul>
</main>
<footer class="modal__footer">
<button class="btn btn--secondary" data-micromodal-close>Закрыть</button>
<button class="btn btn--primary">Действие</button>
</footer>
</div>
</div>
</div>
<script type="module" src="/src/scripts/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,167 @@
# План развития ViteKit Universal
## 🎯 Текущее состояние (v1.0.0)
✅ **Завершено:**
- Базовая архитектура проекта
- Конфигурация Vite + Twig
- Система сборки и оптимизации
- UI компоненты: модалы, табы, аккордеон, уведомления
- SCSS архитектура с утилитами
- Система линтинга и форматирования
- Адаптивная верстка
- Документация
## 🚀 Ближайшие планы (v1.1.0)
### Дополнительные компоненты
- [ ] **Carousel/Slider** - слайдер с touch поддержкой
- [ ] **Dropdown** - выпадающие меню с позиционированием
- [ ] **Tooltip** - всплывающие подсказки
- [ ] **Progress Bar** - индикаторы прогресса
- [ ] **Loading Spinner** - индикаторы загрузки
### Улучшения UX
- [ ] **Темная тема** - переключатель светлой/темной темы
- [ ] **Анимации** - расширенная библиотека анимаций
- [ ] **Skeleton Loading** - каркасная загрузка для контента
- [ ] **Infinite Scroll** - бесконечная прокрутка
- [ ] **Lazy Loading** - отложенная загрузка изображений
### Функциональность
- [ ] **Form Validation** - валидация форм в реальном времени
- [ ] **Cookie Consent** - управление согласием на cookie
- [ ] **Search/Filter** - поиск и фильтрация контента
- [ ] **Copy to Clipboard** - копирование в буфер обмена
## 🔧 Технические улучшения (v1.2.0)
### TypeScript поддержка
- [ ] Миграция JavaScript на TypeScript
- [ ] Типизация всех компонентов
- [ ] Автогенерация документации типов
### PWA возможности
- [ ] Service Worker для кэширования
- [ ] Web App Manifest
- [ ] Offline режим
- [ ] Push уведомления
### Производительность
- [ ] Bundle analyzer интеграция
- [ ] Critical CSS extraction
- [ ] Resource hints optimization
- [ ] WebP/AVIF поддержка для изображений
### Testing
- [ ] Unit тесты для компонентов
- [ ] E2E тестирование с Playwright
- [ ] Visual regression testing
- [ ] Accessibility testing
## 📦 Экосистема (v1.3.0)
### Интеграции
- [ ] **Strapi CMS** - готовые шаблоны для Strapi
- [ ] **Contentful** - интеграция с Contentful API
- [ ] **WordPress** - мост для WordPress тем
- [ ] **Shopify** - e-commerce шаблоны
### Developer Experience
- [ ] **CLI инструмент** - генератор проектов
- [ ] **VS Code extension** - сниппеты и автодополнение
- [ ] **Figma plugin** - экспорт компонентов из Figma
- [ ] **Storybook** - документация компонентов
### Шаблоны и стартеры
- [ ] **Blog template** - шаблон для блога
- [ ] **Portfolio template** - портфолио
- [ ] **E-commerce template** - интернет-магазин
- [ ] **Dashboard template** - админ панель
## 🌐 Расширенные возможности (v2.0.0)
### Многоязычность
- [ ] i18n поддержка
- [ ] RTL языки поддержка
- [ ] Автопереводы через API
### Advanced UI
- [ ] **Data Tables** - таблицы с сортировкой и фильтрацией
- [ ] **Calendar/Date Picker** - календарь и выбор дат
- [ ] **Rich Text Editor** - WYSIWYG редактор
- [ ] **File Upload** - загрузка файлов с drag&drop
### Build System
- [ ] **Multiple outputs** - различные сборки для разных целей
- [ ] **Micro-frontends** - поддержка микрофронтендов
- [ ] **WebAssembly** - интеграция WASM модулей
### Performance Monitoring
- [ ] **Core Web Vitals** - мониторинг производительности
- [ ] **Error tracking** - отслеживание ошибок
- [ ] **Analytics** - интеграция аналитики
## 🎨 Design System (v2.1.0)
### Система дизайна
- [ ] **Design tokens** - централизованные токены дизайна
- [ ] **Component variants** - множественные варианты компонентов
- [ ] **Theme builder** - конструктор тем
- [ ] **Brand guidelines** - руководство по бренду
### Accessibility
- [ ] **Screen reader** - полная поддержка скрин-ридеров
- [ ] **High contrast** - режим высокой контрастности
- [ ] **Motion preferences** - уважение к предпочтениям анимации
- [ ] **WCAG 2.1 AAA** - соответствие стандартам доступности
## 🚀 Экспериментальные возможности
### Cutting Edge
- [ ] **Web Components** - нативные веб-компоненты
- [ ] **View Transitions API** - плавные переходы между страницами
- [ ] **Container Queries** - контейнерные запросы
- [ ] **CSS @layer** - слои каскада
### AI Integration
- [ ] **AI Content Generation** - генерация контента с помощью ИИ
- [ ] **Smart Optimization** - автоматическая оптимизация производительности
- [ ] **Accessibility Checker** - ИИ проверка доступности
## 📊 Метрики успеха
### Производительность
- Lighthouse Score > 95
- Core Web Vitals в зеленой зоне
- Bundle size < 50KB (gzipped)
- First Contentful Paint < 1.5s
### Developer Experience
- Setup time < 5 минут
- Build time < 30 секунд
- Hot reload < 100ms
- 95%+ test coverage
### Community
- 1000+ GitHub stars
- 100+ contributors
- 50+ ecosystem packages
- 10000+ weekly downloads
## 🤝 Вклад в развитие
Приветствуется участие сообщества в развитии проекта:
1. **Обратная связь** - отчеты об ошибках и предложения
2. **Код** - pull requests с новыми возможностями
3. **Документация** - улучшение документации
4. **Тестирование** - тестирование новых возможностей
5. **Дизайн** - улучшение UX/UI
### Приоритеты
- 🔥 **Высокий** - критически важно для пользователей
- 🚀 **Средний** - улучшает опыт разработки
- 💡 **Низкий** - экспериментальные возможности
Roadmap обновляется ежемесячно на основе обратной связи от сообщества и анализа использования.

123
vitekit-claude/index.html Normal file
View File

@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ViteKit Universal - Test</title>
<link rel="stylesheet" href="src/styles/main.scss">
</head>
<body>
<header class="header">
<div class="container">
<nav class="nav">
<div class="nav__brand">
<a href="/" class="nav__logo">ViteKit Universal</a>
</div>
<ul class="nav__menu">
<li class="nav__item">
<a href="/" class="nav__link nav__link--active">Главная</a>
</li>
<li class="nav__item">
<a href="/components-demo.html" class="nav__link">Компоненты</a>
</li>
</ul>
</nav>
</div>
</header>
<main class="main">
<div class="hero">
<div class="container">
<div class="hero__content">
<h1 class="hero__title">ViteKit Universal</h1>
<p class="hero__subtitle">
Современный универсальный сборщик проектов с Vite + Twig + компонентами
</p>
<div class="hero__actions">
<button class="btn btn--primary btn--lg" data-micromodal-trigger="modal-basic">
Открыть модал
</button>
<button class="btn btn--outline btn--lg" data-toast-trigger="success"
data-toast-title="Успех!" data-toast-message="Тест уведомления">
Показать уведомление
</button>
</div>
</div>
</div>
</div>
<section id="components" style="padding: 4rem 0;">
<div class="container">
<h2>Тестирование компонентов</h2>
<!-- Табы -->
<div class="tabs" data-tabs style="margin-bottom: 2rem;">
<div class="tabs__nav">
<button class="tabs__tab tabs__tab--active" data-tab="tab-1">Первая вкладка</button>
<button class="tabs__tab" data-tab="tab-2">Вторая вкладка</button>
<button class="tabs__tab" data-tab="tab-3">Третья вкладка</button>
</div>
<div class="tabs__content">
<div class="tabs__panel tabs__panel--active" id="tab-1-panel" data-panel="tab-1">
<p>Содержимое первой вкладки</p>
</div>
<div class="tabs__panel" id="tab-2-panel" data-panel="tab-2">
<p>Содержимое второй вкладки</p>
</div>
<div class="tabs__panel" id="tab-3-panel" data-panel="tab-3">
<p>Содержимое третьей вкладки</p>
</div>
</div>
</div>
<!-- Аккордеон -->
<div class="accordion" data-accordion>
<div class="accordion__item">
<button class="accordion__header" data-accordion-trigger="acc-1" aria-expanded="false">
<span>Что такое ViteKit?</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content" id="acc-1-content" data-accordion-content="acc-1" aria-hidden="true">
<div class="accordion__body">
<p>ViteKit - это современный сборщик проектов.</p>
</div>
</div>
</div>
<div class="accordion__item">
<button class="accordion__header" data-accordion-trigger="acc-2" aria-expanded="false">
<span>Как использовать?</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content" id="acc-2-content" data-accordion-content="acc-2" aria-hidden="true">
<div class="accordion__body">
<p>Просто установите зависимости и запустите сервер разработки.</p>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Модальное окно -->
<div class="modal micromodal-slide" id="modal-basic" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true">
<header class="modal__header">
<h2 class="modal__title">Тестовое модальное окно</h2>
<button class="modal__close" aria-label="Закрыть" data-micromodal-close></button>
</header>
<main class="modal__content">
<p>Это тестовое модальное окно для проверки функциональности.</p>
</main>
<footer class="modal__footer">
<button class="btn btn--secondary" data-micromodal-close>Закрыть</button>
<button class="btn btn--primary">Подтвердить</button>
</footer>
</div>
</div>
</div>
<script type="module" src="src/scripts/main.js"></script>
</body>
</html>

10273
vitekit-claude/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
{
"name": "vitekit-universal",
"version": "1.0.0",
"description": "Modern universal project builder with Vite + Twig + Components",
"type": "module",
"scripts": {
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint src --ext .js",
"lint:fix": "eslint src --ext .js --fix",
"lint:css": "stylelint 'src/**/*.scss'",
"lint:css:fix": "stylelint 'src/**/*.scss' --fix",
"format": "prettier --write 'src/**/*.{js,scss,twig,json}'",
"prepare": "husky install",
"clean": "rm -rf dist"
},
"devDependencies": {
"@vituum/vite-plugin-twig": "^1.1.0",
"@pivanov/vite-plugin-svg-sprite": "^3.0.0",
"vite-plugin-image-optimizer": "^2.0.1",
"@vitejs/plugin-legacy": "^5.4.2",
"vite": "^5.4.8",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.30.0",
"prettier": "^3.3.3",
"stylelint": "^16.9.0",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-order": "^6.0.4",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"sass": "^1.79.4",
"glob": "^11.0.0"
},
"dependencies": {
"micromodal": "^0.4.10",
"toastify-js": "^1.12.0"
},
"lint-staged": {
"*.js": [
"eslint --fix",
"prettier --write"
],
"*.scss": [
"stylelint --fix",
"prettier --write"
],
"*.{twig,json}": [
"prettier --write"
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View File

@ -0,0 +1,496 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Components Showcase - ViteKit Universal</title>
<link rel="stylesheet" href="src/styles/main.scss">
<style>
/* Showcase specific styles */
.showcase-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4rem 0;
text-align: center;
}
.showcase-section {
padding: 3rem 0;
border-bottom: 1px solid #e5e7eb;
}
.showcase-section:last-child {
border-bottom: none;
}
.component-demo {
background: #f9fafb;
padding: 2rem;
border-radius: 8px;
margin-bottom: 2rem;
}
.demo-controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.demo-code {
background: #1f2937;
color: #e5e7eb;
padding: 1rem;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
overflow-x: auto;
margin-top: 1rem;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.feature-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.interactive-demo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin: 2rem 0;
}
@media (max-width: 768px) {
.interactive-demo {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- Navigation -->
<header class="header">
<div class="container">
<nav class="nav">
<div class="nav__brand">
<a href="/" class="nav__logo">ViteKit Universal</a>
</div>
<ul class="nav__menu">
<li class="nav__item">
<a href="index.html" class="nav__link">Главная</a>
</li>
<li class="nav__item">
<a href="showcase.html" class="nav__link nav__link--active">Showcase</a>
</li>
</ul>
</nav>
</div>
</header>
<!-- Showcase Header -->
<div class="showcase-header">
<div class="container">
<h1>Components Showcase</h1>
<p>Интерактивная демонстрация всех компонентов ViteKit Universal</p>
</div>
</div>
<main class="main">
<!-- Toast Notifications -->
<section class="showcase-section">
<div class="container">
<h2>Toast Уведомления</h2>
<p>Всплывающие уведомления с автоматическим скрытием и анимациями</p>
<div class="component-demo">
<h3>Типы уведомлений</h3>
<div class="demo-controls">
<button class="btn btn--success" data-toast-trigger="success"
data-toast-title="Успешно!" data-toast-message="Операция выполнена успешно">
Success Toast
</button>
<button class="btn btn--error" data-toast-trigger="error"
data-toast-title="Ошибка!" data-toast-message="Произошла ошибка при выполнении операции">
Error Toast
</button>
<button class="btn btn--warning" data-toast-trigger="warning"
data-toast-title="Предупреждение!" data-toast-message="Проверьте введенные данные">
Warning Toast
</button>
<button class="btn btn--secondary" data-toast-trigger="info"
data-toast-title="Информация" data-toast-message="Новая версия доступна для скачивания">
Info Toast
</button>
</div>
<div class="demo-code">
// JavaScript API
ToastComponent.success('Операция выполнена!');
ToastComponent.error('Произошла ошибка!');
ToastComponent.warning('Внимание!');
ToastComponent.info('Новая информация');
// HTML атрибуты
&lt;button data-toast-trigger="success"
data-toast-title="Заголовок"
data-toast-message="Сообщение"&gt;
Показать уведомление
&lt;/button&gt;
</div>
</div>
<div class="feature-grid">
<div class="feature-card">
<h4>✨ Автоматическое скрытие</h4>
<p>Уведомления автоматически исчезают через 4 секунды</p>
</div>
<div class="feature-card">
<h4>📱 Адаптивность</h4>
<p>Корректное отображение на всех устройствах</p>
</div>
<div class="feature-card">
<h4>🎨 Типизация</h4>
<p>4 типа: success, error, warning, info</p>
</div>
<div class="feature-card">
<h4>🔧 API</h4>
<p>Простой JavaScript API и HTML атрибуты</p>
</div>
</div>
</div>
</section>
<!-- Modal Windows -->
<section class="showcase-section">
<div class="container">
<h2>Модальные окна</h2>
<p>Доступные модальные окна с поддержкой ARIA и клавиатурной навигации</p>
<div class="component-demo">
<h3>Типы модальных окон</h3>
<div class="demo-controls">
<button class="btn btn--primary" data-micromodal-trigger="modal-basic">
Базовое модальное окно
</button>
<button class="btn btn--secondary" data-micromodal-trigger="modal-form">
Модальное окно с формой
</button>
<button class="btn btn--outline" data-micromodal-trigger="modal-confirmation">
Окно подтверждения
</button>
</div>
<div class="demo-code">
// JavaScript API
MicroModal.show('modal-id');
MicroModal.close('modal-id');
// HTML разметка
&lt;div class="modal micromodal-slide" id="modal-id"&gt;
&lt;div class="modal__overlay" data-micromodal-close&gt;
&lt;div class="modal__container"&gt;
&lt;header class="modal__header"&gt;
&lt;h2 class="modal__title"&gt;Заголовок&lt;/h2&gt;
&lt;button class="modal__close" data-micromodal-close&gt;&lt;/button&gt;
&lt;/header&gt;
&lt;main class="modal__content"&gt;Содержимое&lt;/main&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</div>
</div>
<div class="feature-grid">
<div class="feature-card">
<h4>♿ Доступность</h4>
<p>Полная поддержка ARIA и скрин-ридеров</p>
</div>
<div class="feature-card">
<h4>⌨️ Клавиатура</h4>
<p>Навигация через Tab, Enter, Escape</p>
</div>
<div class="feature-card">
<h4>🎬 Анимации</h4>
<p>Плавные анимации открытия и закрытия</p>
</div>
<div class="feature-card">
<h4>🔒 Фокус</h4>
<p>Автоматическое управление фокусом</p>
</div>
</div>
</div>
</section>
<!-- Tabs -->
<section class="showcase-section">
<div class="container">
<h2>Система табов</h2>
<p>Адаптивные табы с поддержкой клавиатурной навигации</p>
<div class="component-demo">
<h3>Интерактивные табы</h3>
<div class="tabs" data-tabs>
<div class="tabs__nav">
<button class="tabs__tab tabs__tab--active" data-tab="features">Возможности</button>
<button class="tabs__tab" data-tab="usage">Использование</button>
<button class="tabs__tab" data-tab="examples">Примеры</button>
<button class="tabs__tab" data-tab="api">API</button>
</div>
<div class="tabs__content">
<div class="tabs__panel tabs__panel--active" data-panel="features">
<h4>Основные возможности</h4>
<ul>
<li>📱 <strong>Адаптивность:</strong> Автоматическое превращение в аккордеон на мобильных</li>
<li>⌨️ <strong>Клавиатурная навигация:</strong> Arrow keys, Home, End</li>
<li>🎨 <strong>Кастомизация:</strong> Легко настраиваемые стили</li>
<li>🚀 <strong>Производительность:</strong> Минимальный overhead</li>
</ul>
</div>
<div class="tabs__panel" data-panel="usage">
<h4>Как использовать</h4>
<p>Создайте HTML структуру с нужными классами и добавьте атрибут <code>data-tabs</code>:</p>
<div class="demo-code">
&lt;div class="tabs" data-tabs&gt;
&lt;div class="tabs__nav"&gt;
&lt;button class="tabs__tab tabs__tab--active"&gt;Вкладка 1&lt;/button&gt;
&lt;button class="tabs__tab"&gt;Вкладка 2&lt;/button&gt;
&lt;/div&gt;
&lt;div class="tabs__content"&gt;
&lt;div class="tabs__panel tabs__panel--active"&gt;Содержимое 1&lt;/div&gt;
&lt;div class="tabs__panel"&gt;Содержимое 2&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</div>
</div>
<div class="tabs__panel" data-panel="examples">
<h4>Примеры использования</h4>
<p>Табы отлично подходят для:</p>
<ul>
<li>Навигации по разделам контента</li>
<li>Организации форм по шагам</li>
<li>Переключения между видами данных</li>
<li>Фильтрации контента по категориям</li>
</ul>
</div>
<div class="tabs__panel" data-panel="api">
<h4>JavaScript API</h4>
<div class="demo-code">
// Создание экземпляра
const tabs = new TabsComponent(element, options);
// Методы
tabs.next(); // Следующая вкладка
tabs.prev(); // Предыдущая вкладка
tabs.goTo(2); // Перейти к вкладке по индексу
// События
element.addEventListener('tabchange', (e) => {
console.log('Активна вкладка:', e.detail.index);
});
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Accordion -->
<section class="showcase-section">
<div class="container">
<h2>Аккордеон</h2>
<p>Сворачиваемые панели для экономии места с плавными анимациями</p>
<div class="interactive-demo">
<div>
<h3>Интерактивный аккордеон</h3>
<div class="accordion" data-accordion>
<div class="accordion__item">
<button class="accordion__header" data-accordion-trigger="acc-features" aria-expanded="false">
<span>🚀 Возможности аккордеона</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content" data-accordion-content="acc-features" aria-hidden="true">
<div class="accordion__body">
<ul>
<li><strong>Плавные анимации:</strong> CSS transitions для smooth UX</li>
<li><strong>Множественное раскрытие:</strong> Настраиваемое поведение</li>
<li><strong>Клавиатурная навигация:</strong> Полная поддержка a11y</li>
<li><strong>Автоматические ARIA атрибуты:</strong> Доступность из коробки</li>
</ul>
</div>
</div>
</div>
<div class="accordion__item">
<button class="accordion__header" data-accordion-trigger="acc-usage" aria-expanded="false">
<span>💡 Использование в проектах</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content" data-accordion-content="acc-usage" aria-hidden="true">
<div class="accordion__body">
<p>Аккордеон идеален для:</p>
<ul>
<li>FAQ секций</li>
<li>Списков продуктов с детальной информацией</li>
<li>Документации с разворачиваемыми разделами</li>
<li>Фильтров в боковых панелях</li>
</ul>
</div>
</div>
</div>
<div class="accordion__item">
<button class="accordion__header" data-accordion-trigger="acc-config" aria-expanded="false">
<span>⚙️ Настройки и конфигурация</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content" data-accordion-content="acc-config" aria-hidden="true">
<div class="accordion__body">
<div class="demo-code">
// Опции конфигурации
const accordion = new AccordionComponent(element, {
allowMultiple: false, // Разрешить открытие нескольких панелей
animationDuration: 300, // Длительность анимации в мс
autoClose: true, // Автозакрытие других панелей
keyboardNavigation: true // Клавиатурная навигация
});
// API методы
accordion.open(0); // Открыть панель по индексу
accordion.close(1); // Закрыть панель
accordion.toggle(2); // Переключить панель
accordion.closeAll(); // Закрыть все панели
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<h3>Особенности реализации</h3>
<div class="feature-grid" style="grid-template-columns: 1fr;">
<div class="feature-card">
<h4>🎭 Анимации</h4>
<p>Плавные CSS transitions с автоматическим расчетом высоты контента</p>
</div>
<div class="feature-card">
<h4>♿ Доступность</h4>
<p>Полная поддержка ARIA атрибутов и клавиатурной навигации</p>
</div>
<div class="feature-card">
<h4>📐 Гибкость</h4>
<p>Настраиваемое поведение: единичное или множественное раскрытие</p>
</div>
<div class="feature-card">
<h4>🎨 Стилизация</h4>
<p>Легко настраиваемые стили через CSS переменные и модификаторы</p>
</div>
</div>
<h4>События</h4>
<div class="demo-code">
// Слушаем события аккордеона
element.addEventListener('accordionopen', (e) => {
console.log('Открыта панель:', e.detail.index);
});
element.addEventListener('accordionclose', (e) => {
console.log('Закрыта панель:', e.detail.index);
});
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Modals -->
<div class="modal micromodal-slide" id="modal-basic" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true">
<header class="modal__header">
<h2 class="modal__title">Базовое модальное окно</h2>
<button class="modal__close" aria-label="Закрыть" data-micromodal-close></button>
</header>
<main class="modal__content">
<p>Это базовое модальное окно с минимальным содержимым. Оно демонстрирует:</p>
<ul>
<li>Правильную структуру HTML</li>
<li>ARIA атрибуты для доступности</li>
<li>Анимации открытия и закрытия</li>
<li>Управление фокусом</li>
</ul>
<p>Закрыть окно можно кликом на overlay, кнопку закрытия или нажатием Escape.</p>
</main>
<footer class="modal__footer">
<button class="btn btn--secondary" data-micromodal-close>Закрыть</button>
<button class="btn btn--primary">Понятно</button>
</footer>
</div>
</div>
</div>
<div class="modal micromodal-slide" id="modal-form" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true">
<header class="modal__header">
<h2 class="modal__title">Модальное окно с формой</h2>
<button class="modal__close" aria-label="Закрыть" data-micromodal-close></button>
</header>
<main class="modal__content">
<form class="modal-form">
<div class="form-group">
<label for="modal-name" class="form-label">Имя</label>
<input type="text" id="modal-name" class="form-control" placeholder="Введите ваше имя">
</div>
<div class="form-group">
<label for="modal-email" class="form-label">Email</label>
<input type="email" id="modal-email" class="form-control" placeholder="example@email.com">
</div>
<div class="form-group">
<label for="modal-message" class="form-label">Сообщение</label>
<textarea id="modal-message" class="form-control" rows="3" placeholder="Ваше сообщение"></textarea>
</div>
</form>
</main>
<footer class="modal__footer">
<button class="btn btn--secondary" data-micromodal-close>Отмена</button>
<button class="btn btn--primary">Отправить</button>
</footer>
</div>
</div>
</div>
<div class="modal micromodal-slide" id="modal-confirmation" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true">
<header class="modal__header">
<h2 class="modal__title">Подтверждение действия</h2>
<button class="modal__close" aria-label="Закрыть" data-micromodal-close></button>
</header>
<main class="modal__content">
<p>Вы уверены, что хотите выполнить это действие?</p>
<p><strong>Внимание:</strong> Это действие нельзя будет отменить.</p>
</main>
<footer class="modal__footer">
<button class="btn btn--secondary" data-micromodal-close>Отмена</button>
<button class="btn btn--error">Подтвердить</button>
</footer>
</div>
</div>
</div>
<script type="module" src="src/scripts/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,86 @@
{
"modal": {
"title": "Модальные окна",
"description": "Доступные модальные окна с поддержкой ARIA и фокуса",
"examples": [
{
"id": "modal-basic",
"title": "Базовое модальное окно",
"content": "Это простое модальное окно с базовым функционалом."
},
{
"id": "modal-form",
"title": "Модальное окно с формой",
"content": "Модальное окно, содержащее форму для ввода данных."
}
]
},
"tabs": {
"title": "Вкладки",
"description": "Система табов с поддержкой клавиатурной навигации",
"examples": [
{
"id": "tab-1",
"title": "Первая вкладка",
"content": "Содержимое первой вкладки с демонстрацией функционала."
},
{
"id": "tab-2",
"title": "Вторая вкладка",
"content": "Содержимое второй вкладки с дополнительной информацией."
},
{
"id": "tab-3",
"title": "Третья вкладка",
"content": "Содержимое третьей вкладки с примерами использования."
}
]
},
"accordion": {
"title": "Аккордеон",
"description": "Сворачиваемые панели для экономии места",
"examples": [
{
"id": "accordion-1",
"title": "Что такое ViteKit?",
"content": "ViteKit - это современный сборщик проектов, созданный для быстрой разработки."
},
{
"id": "accordion-2",
"title": "Как использовать компоненты?",
"content": "Компоненты легко интегрируются в любой проект и настраиваются через атрибуты."
},
{
"id": "accordion-3",
"title": "Поддержка браузеров",
"content": "Все компоненты поддерживают современные браузеры и имеют полифиллы для старых версий."
}
]
},
"toast": {
"title": "Уведомления",
"description": "Всплывающие уведомления для информирования пользователей",
"examples": [
{
"type": "success",
"title": "Успешно!",
"message": "Операция выполнена успешно."
},
{
"type": "error",
"title": "Ошибка!",
"message": "Произошла ошибка при выполнении операции."
},
{
"type": "warning",
"title": "Внимание!",
"message": "Проверьте введенные данные."
},
{
"type": "info",
"title": "Информация",
"message": "Новая версия доступна для скачивания."
}
]
}
}

View File

@ -0,0 +1,24 @@
{
"title": "ViteKit Universal",
"description": "Modern universal project builder with Vite + Twig + Components",
"author": "ViteKit Team",
"url": "http://localhost:3000",
"language": "ru",
"navigation": [
{
"title": "Главная",
"url": "/",
"active": true
},
{
"title": "Компоненты",
"url": "/components-demo",
"active": false
}
],
"meta": {
"viewport": "width=device-width, initial-scale=1.0",
"robots": "index, follow",
"theme-color": "#3b82f6"
}
}

View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="{{ site.language }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="{{ site.meta.viewport }}">
<meta name="robots" content="{{ site.meta.robots }}">
<meta name="theme-color" content="{{ site.meta.theme_color }}">
<title>{% block title %}{{ site.title }}{% endblock %}</title>
<meta name="description" content="{% block description %}{{ site.description }}{% endblock %}">
<!-- Preload critical resources -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Styles -->
<link rel="stylesheet" href="/src/styles/main.scss">
{% block head %}{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
<!-- Header -->
{% block header %}
<header class="header">
<div class="container">
<nav class="nav">
<div class="nav__brand">
<a href="/" class="nav__logo">{{ site.title }}</a>
</div>
<ul class="nav__menu">
{% for item in site.navigation %}
<li class="nav__item">
<a href="{{ item.url }}" class="nav__link{% if item.active %} nav__link--active{% endif %}">
{{ item.title }}
</a>
</li>
{% endfor %}
</ul>
</nav>
</div>
</header>
{% endblock %}
<!-- Main Content -->
<main class="main">
{% block content %}{% endblock %}
</main>
<!-- Footer -->
{% block footer %}
<footer class="footer">
<div class="container">
<div class="footer__content">
<p>&copy; 2024 {{ site.title }}. Все права защищены.</p>
</div>
</div>
</footer>
{% endblock %}
<!-- Scripts -->
<script type="module" src="/src/scripts/main.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,162 @@
{% extends "layouts/base.twig" %}
{% block title %}Демонстрация компонентов - {{ site.title }}{% endblock %}
{% block content %}
<div class="demo-header">
<div class="container">
<h1>Демонстрация компонентов</h1>
<p>Интерактивные примеры всех доступных UI компонентов</p>
</div>
</div>
<!-- Модальные окна -->
<section class="demo-section">
<div class="container">
<div class="demo-title">
<h2>{{ components.modal.title }}</h2>
<p>{{ components.modal.description }}</p>
</div>
<div class="demo-content">
<div class="demo-buttons">
{% for example in components.modal.examples %}
<button class="btn btn--primary" data-micromodal-trigger="{{ example.id }}">
{{ example.title }}
</button>
{% endfor %}
</div>
</div>
<!-- Модальные окна -->
{% for example in components.modal.examples %}
<div class="modal micromodal-slide" id="{{ example.id }}" aria-hidden="true">
<div class="modal__overlay" tabindex="-1" data-micromodal-close>
<div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="{{ example.id }}-title">
<header class="modal__header">
<h2 class="modal__title" id="{{ example.id }}-title">
{{ example.title }}
</h2>
<button class="modal__close" aria-label="Закрыть модальное окно" data-micromodal-close></button>
</header>
<main class="modal__content" id="{{ example.id }}-content">
<p>{{ example.content }}</p>
{% if example.id == 'modal-form' %}
<form class="modal-form">
<div class="form-group">
<label for="name" class="form-label">Имя</label>
<input type="text" id="name" class="form-control" placeholder="Введите ваше имя">
</div>
<div class="form-group">
<label for="email" class="form-label">Email</label>
<input type="email" id="email" class="form-control" placeholder="example@email.com">
</div>
<div class="form-group">
<label for="message" class="form-label">Сообщение</label>
<textarea id="message" class="form-control" rows="3" placeholder="Ваше сообщение"></textarea>
</div>
</form>
{% endif %}
</main>
<footer class="modal__footer">
<button class="btn btn--secondary" data-micromodal-close aria-label="Закрыть это модальное окно">Закрыть</button>
{% if example.id == 'modal-form' %}
<button class="btn btn--primary">Отправить</button>
{% endif %}
</footer>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
<!-- Вкладки -->
<section class="demo-section">
<div class="container">
<div class="demo-title">
<h2>{{ components.tabs.title }}</h2>
<p>{{ components.tabs.description }}</p>
</div>
<div class="demo-content">
<div class="tabs" data-tabs>
<div class="tabs__nav">
{% for example in components.tabs.examples %}
<button class="tabs__tab{% if loop.first %} tabs__tab--active{% endif %}"
data-tab="{{ example.id }}"
aria-controls="{{ example.id }}-panel">
{{ example.title }}
</button>
{% endfor %}
</div>
<div class="tabs__content">
{% for example in components.tabs.examples %}
<div class="tabs__panel{% if loop.first %} tabs__panel--active{% endif %}"
id="{{ example.id }}-panel"
data-panel="{{ example.id }}">
<p>{{ example.content }}</p>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</section>
<!-- Аккордеон -->
<section class="demo-section">
<div class="container">
<div class="demo-title">
<h2>{{ components.accordion.title }}</h2>
<p>{{ components.accordion.description }}</p>
</div>
<div class="demo-content">
<div class="accordion" data-accordion>
{% for example in components.accordion.examples %}
<div class="accordion__item">
<button class="accordion__header"
data-accordion-trigger="{{ example.id }}"
aria-expanded="false"
aria-controls="{{ example.id }}-content">
<span>{{ example.title }}</span>
<span class="accordion__icon"></span>
</button>
<div class="accordion__content"
id="{{ example.id }}-content"
data-accordion-content="{{ example.id }}">
<div class="accordion__body">
<p>{{ example.content }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</section>
<!-- Уведомления -->
<section class="demo-section">
<div class="container">
<div class="demo-title">
<h2>{{ components.toast.title }}</h2>
<p>{{ components.toast.description }}</p>
</div>
<div class="demo-content">
<div class="demo-buttons">
{% for example in components.toast.examples %}
<button class="btn btn--{{ example.type }}"
data-toast-trigger="{{ example.type }}"
data-toast-title="{{ example.title }}"
data-toast-message="{{ example.message }}">
{{ example.title }}
</button>
{% endfor %}
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,114 @@
{% extends "layouts/base.twig" %}
{% block title %}{{ site.title }} - Главная страница{% endblock %}
{% block content %}
<div class="hero">
<div class="container">
<div class="hero__content">
<h1 class="hero__title">ViteKit Universal</h1>
<p class="hero__subtitle">
Современный универсальный сборщик проектов с Vite + Twig + компонентами
</p>
<div class="hero__actions">
<a href="/components-demo" class="btn btn--primary btn--lg">
Посмотреть компоненты
</a>
<a href="#features" class="btn btn--outline btn--lg">
Узнать больше
</a>
</div>
</div>
</div>
</div>
<section id="features" class="features">
<div class="container">
<div class="section-header">
<h2>Возможности</h2>
<p>Все необходимое для современной веб-разработки</p>
</div>
<div class="row">
<div class="col-md-4">
<div class="feature">
<div class="feature__icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="feature__title">Быстрая сборка</h3>
<p class="feature__description">
Основанный на Vite для молниеносной разработки и сборки проектов
</p>
</div>
</div>
<div class="col-md-4">
<div class="feature">
<div class="feature__icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="feature__title">Готовые компоненты</h3>
<p class="feature__description">
Набор протестированных UI компонентов для быстрого прототипирования
</p>
</div>
</div>
<div class="col-md-4">
<div class="feature">
<div class="feature__icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 00-2.91-.09z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 15l8.5-8.5a2.12 2.12 0 000-3L18 1l-8.5 8.5a2.12 2.12 0 000 3L12 15z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="feature__title">Легкая настройка</h3>
<p class="feature__description">
Гибкая архитектура и конфигурация под любые потребности проекта
</p>
</div>
</div>
</div>
</div>
</section>
<section class="technologies">
<div class="container">
<div class="section-header">
<h2>Технологический стек</h2>
<p>Современные инструменты для эффективной разработки</p>
</div>
<div class="tech-grid">
<div class="tech-item">
<h4>Vite</h4>
<p>Быстрый сборщик проектов</p>
</div>
<div class="tech-item">
<h4>Twig</h4>
<p>Мощный шаблонизатор</p>
</div>
<div class="tech-item">
<h4>SCSS</h4>
<p>Расширенный CSS</p>
</div>
<div class="tech-item">
<h4>Vanilla JS</h4>
<p>Нативный JavaScript</p>
</div>
<div class="tech-item">
<h4>ESLint</h4>
<p>Качество кода</p>
</div>
<div class="tech-item">
<h4>Prettier</h4>
<p>Форматирование</p>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,317 @@
export class AccordionComponent {
constructor(element, options = {}) {
this.element = element;
this.options = {
allowMultiple: false,
animationDuration: 300,
autoClose: true,
keyboardNavigation: true,
...options
};
this.items = [];
this.activeItems = new Set();
this.init();
}
init() {
this.bindElements();
this.bindEvents();
this.setInitialState();
this.setupAccessibility();
}
bindElements() {
const itemElements = this.element.querySelectorAll('.accordion__item');
this.items = Array.from(itemElements).map(item => {
const trigger = item.querySelector('.accordion__header');
const content = item.querySelector('.accordion__content');
const id = trigger?.dataset.accordionTrigger || this.generateId();
return {
element: item,
trigger,
content,
id,
isOpen: false
};
});
if (this.items.length === 0) {
console.warn('AccordionComponent: No accordion items found');
return;
}
}
bindEvents() {
this.items.forEach((item, index) => {
if (!item.trigger || !item.content) return;
// Click events
item.trigger.addEventListener('click', (e) => {
e.preventDefault();
this.toggle(index);
});
// Keyboard events
if (this.options.keyboardNavigation) {
item.trigger.addEventListener('keydown', (e) => {
this.handleKeyboard(e, index);
});
}
});
}
setInitialState() {
this.items.forEach((item, index) => {
const isInitiallyOpen = item.trigger?.getAttribute('aria-expanded') === 'true';
if (isInitiallyOpen) {
this.open(index, false); // Open without animation initially
} else {
this.close(index, false);
}
});
}
setupAccessibility() {
this.items.forEach((item, index) => {
if (!item.trigger || !item.content) return;
const triggerId = `accordion-trigger-${item.id}`;
const contentId = `accordion-content-${item.id}`;
// Set up ARIA attributes
item.trigger.setAttribute('id', triggerId);
item.trigger.setAttribute('aria-controls', contentId);
item.trigger.setAttribute('tabindex', '0');
item.content.setAttribute('id', contentId);
item.content.setAttribute('aria-labelledby', triggerId);
item.content.setAttribute('role', 'region');
});
}
toggle(index) {
const item = this.items[index];
if (!item) return;
if (item.isOpen) {
this.close(index);
} else {
this.open(index);
}
}
open(index, animate = true) {
const item = this.items[index];
if (!item || item.isOpen) return;
// Close other items if multiple is not allowed
if (!this.options.allowMultiple && this.options.autoClose) {
this.closeAll(index);
}
item.isOpen = true;
this.activeItems.add(index);
// Update ARIA attributes
item.trigger.setAttribute('aria-expanded', 'true');
item.content.setAttribute('aria-hidden', 'false');
// Add active class
item.element.classList.add('accordion__item--active');
if (animate) {
this.animateOpen(item);
} else {
item.content.style.maxHeight = 'none';
}
// Emit custom event
this.element.dispatchEvent(new CustomEvent('accordionopen', {
detail: { index, item }
}));
}
close(index, animate = true) {
const item = this.items[index];
if (!item || !item.isOpen) return;
item.isOpen = false;
this.activeItems.delete(index);
// Update ARIA attributes
item.trigger.setAttribute('aria-expanded', 'false');
item.content.setAttribute('aria-hidden', 'true');
// Remove active class
item.element.classList.remove('accordion__item--active');
if (animate) {
this.animateClose(item);
} else {
item.content.style.maxHeight = '0';
}
// Emit custom event
this.element.dispatchEvent(new CustomEvent('accordionclose', {
detail: { index, item }
}));
}
closeAll(except = -1) {
this.items.forEach((item, index) => {
if (index !== except && item.isOpen) {
this.close(index);
}
});
}
openAll() {
if (!this.options.allowMultiple) return;
this.items.forEach((item, index) => {
if (!item.isOpen) {
this.open(index);
}
});
}
animateOpen(item) {
const content = item.content;
const scrollHeight = content.scrollHeight;
// Set initial state
content.style.maxHeight = '0';
content.style.overflow = 'hidden';
// Force reflow
content.offsetHeight;
// Add transition
content.style.transition = `max-height ${this.options.animationDuration}ms ease-out`;
// Animate to full height
content.style.maxHeight = `${scrollHeight}px`;
// Clean up after animation
setTimeout(() => {
content.style.maxHeight = 'none';
content.style.overflow = '';
content.style.transition = '';
}, this.options.animationDuration);
}
animateClose(item) {
const content = item.content;
const scrollHeight = content.scrollHeight;
// Set initial state
content.style.maxHeight = `${scrollHeight}px`;
content.style.overflow = 'hidden';
// Force reflow
content.offsetHeight;
// Add transition
content.style.transition = `max-height ${this.options.animationDuration}ms ease-in`;
// Animate to zero height
content.style.maxHeight = '0';
// Clean up after animation
setTimeout(() => {
content.style.overflow = '';
content.style.transition = '';
}, this.options.animationDuration);
}
handleKeyboard(e, index) {
const currentItem = this.items[index];
if (!currentItem) return;
switch (e.key) {
case 'Enter':
case ' ':
e.preventDefault();
this.toggle(index);
break;
case 'ArrowDown':
e.preventDefault();
this.focusNext(index);
break;
case 'ArrowUp':
e.preventDefault();
this.focusPrev(index);
break;
case 'Home':
e.preventDefault();
this.focusFirst();
break;
case 'End':
e.preventDefault();
this.focusLast();
break;
}
}
focusNext(currentIndex) {
const nextIndex = currentIndex < this.items.length - 1 ?
currentIndex + 1 : 0;
this.items[nextIndex]?.trigger?.focus();
}
focusPrev(currentIndex) {
const prevIndex = currentIndex > 0 ?
currentIndex - 1 : this.items.length - 1;
this.items[prevIndex]?.trigger?.focus();
}
focusFirst() {
this.items[0]?.trigger?.focus();
}
focusLast() {
this.items[this.items.length - 1]?.trigger?.focus();
}
generateId() {
return `accordion-${Math.random().toString(36).substr(2, 9)}`;
}
// Public API
getActiveItems() {
return Array.from(this.activeItems);
}
isOpen(index) {
return this.items[index]?.isOpen || false;
}
destroy() {
// Remove event listeners and reset state
this.items.forEach(item => {
if (item.trigger) {
item.trigger.removeAttribute('aria-expanded');
item.trigger.removeAttribute('aria-controls');
item.trigger.removeAttribute('tabindex');
}
if (item.content) {
item.content.removeAttribute('aria-hidden');
item.content.removeAttribute('aria-labelledby');
item.content.removeAttribute('role');
item.content.style.maxHeight = '';
item.content.style.overflow = '';
item.content.style.transition = '';
}
item.element.classList.remove('accordion__item--active');
});
this.activeItems.clear();
}
}

View File

@ -0,0 +1,233 @@
export class TabsComponent {
constructor(element, options = {}) {
this.element = element;
this.options = {
activeClass: 'tabs__tab--active',
panelActiveClass: 'tabs__panel--active',
keyboardNavigation: true,
autoResponsive: true,
...options
};
this.tabs = [];
this.panels = [];
this.activeIndex = 0;
this.init();
}
init() {
this.bindElements();
this.bindEvents();
this.setInitialState();
if (this.options.autoResponsive) {
this.handleResponsive();
}
}
bindElements() {
this.tabNav = this.element.querySelector('.tabs__nav');
this.tabContent = this.element.querySelector('.tabs__content');
this.tabs = Array.from(this.element.querySelectorAll('.tabs__tab'));
this.panels = Array.from(this.element.querySelectorAll('.tabs__panel'));
if (!this.tabNav || !this.tabContent || this.tabs.length === 0) {
console.warn('TabsComponent: Missing required elements');
return;
}
}
bindEvents() {
// Tab click events
this.tabs.forEach((tab, index) => {
tab.addEventListener('click', (e) => {
e.preventDefault();
this.activateTab(index);
});
});
// Keyboard navigation
if (this.options.keyboardNavigation) {
this.tabNav.addEventListener('keydown', (e) => {
this.handleKeyboard(e);
});
}
// Responsive handling
if (this.options.autoResponsive) {
window.addEventListener('resize', () => {
this.handleResponsive();
});
}
}
setInitialState() {
// Find initially active tab or default to first
const activeTab = this.tabs.find(tab =>
tab.classList.contains(this.options.activeClass)
);
if (activeTab) {
this.activeIndex = this.tabs.indexOf(activeTab);
}
this.activateTab(this.activeIndex);
}
activateTab(index) {
if (index < 0 || index >= this.tabs.length) return;
// Remove active state from all tabs and panels
this.tabs.forEach(tab => {
tab.classList.remove(this.options.activeClass);
tab.setAttribute('aria-selected', 'false');
tab.setAttribute('tabindex', '-1');
});
this.panels.forEach(panel => {
panel.classList.remove(this.options.panelActiveClass);
panel.setAttribute('aria-hidden', 'true');
});
// Add active state to selected tab and panel
const activeTab = this.tabs[index];
const activePanel = this.panels[index];
if (activeTab && activePanel) {
activeTab.classList.add(this.options.activeClass);
activeTab.setAttribute('aria-selected', 'true');
activeTab.setAttribute('tabindex', '0');
activePanel.classList.add(this.options.panelActiveClass);
activePanel.setAttribute('aria-hidden', 'false');
this.activeIndex = index;
// Emit custom event
this.element.dispatchEvent(new CustomEvent('tabchange', {
detail: {
index,
tab: activeTab,
panel: activePanel
}
}));
}
}
handleKeyboard(e) {
const { activeElement } = document;
const currentIndex = this.tabs.indexOf(activeElement);
if (currentIndex === -1) return;
let newIndex = currentIndex;
switch (e.key) {
case 'ArrowLeft':
e.preventDefault();
newIndex = currentIndex > 0 ? currentIndex - 1 : this.tabs.length - 1;
break;
case 'ArrowRight':
e.preventDefault();
newIndex = currentIndex < this.tabs.length - 1 ? currentIndex + 1 : 0;
break;
case 'Home':
e.preventDefault();
newIndex = 0;
break;
case 'End':
e.preventDefault();
newIndex = this.tabs.length - 1;
break;
case 'Enter':
case ' ':
e.preventDefault();
this.activateTab(currentIndex);
return;
}
if (newIndex !== currentIndex) {
this.tabs[newIndex].focus();
this.activateTab(newIndex);
}
}
handleResponsive() {
const isMobile = window.innerWidth < 768;
if (isMobile && !this.element.classList.contains('tabs--responsive')) {
this.element.classList.add('tabs--responsive');
this.convertToAccordion();
} else if (!isMobile && this.element.classList.contains('tabs--responsive')) {
this.element.classList.remove('tabs--responsive');
this.convertToTabs();
}
}
convertToAccordion() {
// Add mobile accordion behavior
this.panels.forEach((panel, index) => {
const tab = this.tabs[index];
if (tab && panel) {
panel.setAttribute('data-tab-title', tab.textContent);
panel.addEventListener('click', (e) => {
if (e.target === panel.querySelector('::before') ||
e.target.closest('.tabs__panel') === panel) {
this.togglePanel(index);
}
});
}
});
}
convertToTabs() {
// Remove mobile accordion behavior
this.panels.forEach(panel => {
panel.removeAttribute('data-tab-title');
});
}
togglePanel(index) {
const panel = this.panels[index];
const isActive = panel.classList.contains(this.options.panelActiveClass);
if (isActive) {
panel.classList.remove(this.options.panelActiveClass);
} else {
this.activateTab(index);
}
}
// Public API
next() {
const nextIndex = this.activeIndex < this.tabs.length - 1 ?
this.activeIndex + 1 : 0;
this.activateTab(nextIndex);
}
prev() {
const prevIndex = this.activeIndex > 0 ?
this.activeIndex - 1 : this.tabs.length - 1;
this.activateTab(prevIndex);
}
goTo(index) {
this.activateTab(index);
}
destroy() {
// Remove event listeners and reset state
this.tabs.forEach(tab => {
tab.removeAttribute('aria-selected');
tab.removeAttribute('tabindex');
});
this.panels.forEach(panel => {
panel.removeAttribute('aria-hidden');
});
this.element.classList.remove('tabs--responsive');
}
}

View File

@ -0,0 +1,268 @@
import Toastify from 'toastify-js';
export class ToastComponent {
static defaultOptions = {
duration: 4000,
position: 'right',
gravity: 'top',
close: true,
stopOnFocus: true,
style: {
background: 'transparent',
boxShadow: 'none',
padding: '0',
borderRadius: '0'
},
offset: {
x: 16,
y: 16
}
};
static types = {
success: {
className: 'toast-success',
icon: '✓'
},
error: {
className: 'toast-error',
icon: '✕'
},
warning: {
className: 'toast-warning',
icon: '⚠'
},
info: {
className: 'toast-info',
icon: ''
},
default: {
className: 'toast-default',
icon: '●'
}
};
static show(message, type = 'default', options = {}) {
if (!message) {
console.warn('ToastComponent: Message is required');
return null;
}
const typeConfig = this.types[type] || this.types.default;
const config = { ...this.defaultOptions, ...options };
// Build toast content
const content = this.buildToastContent(message, config.title, typeConfig.icon);
const toastOptions = {
text: content,
duration: config.duration,
gravity: config.gravity,
position: config.position,
stopOnFocus: config.stopOnFocus,
className: `toastify ${typeConfig.className}`,
escapeMarkup: false,
offset: config.offset,
style: {
...this.defaultOptions.style,
...config.style
},
onClick: config.onClick,
onClose: config.onClose
};
// Create and show toast
const toast = Toastify(toastOptions);
toast.showToast();
// Add progress bar if duration is set
if (config.duration > 0 && config.showProgress !== false) {
this.addProgressBar(toast, config.duration);
}
return toast;
}
static buildToastContent(message, title, icon) {
let content = '<div class="toast-enhanced">';
if (icon) {
content += `<div class="toast-enhanced__icon">${icon}</div>`;
}
content += '<div class="toast-enhanced__content">';
if (title) {
content += `<div class="toast-enhanced__title">${this.escapeHtml(title)}</div>`;
}
content += `<div class="toast-enhanced__message">${this.escapeHtml(message)}</div>`;
content += '</div>';
content += '<button class="toast-enhanced__close" onclick="this.closest(\'.toastify\').remove()"></button>';
content += '</div>';
return content;
}
static addProgressBar(toast, duration) {
const toastElement = toast.toastElement;
if (!toastElement) return;
const progressBar = document.createElement('div');
progressBar.className = 'toast-progress';
progressBar.innerHTML = '<div class="toast-progress__bar"></div>';
toastElement.appendChild(progressBar);
const bar = progressBar.querySelector('.toast-progress__bar');
if (bar) {
bar.style.animationDuration = `${duration}ms`;
}
}
static escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Convenience methods
static success(message, options = {}) {
return this.show(message, 'success', options);
}
static error(message, options = {}) {
return this.show(message, 'error', options);
}
static warning(message, options = {}) {
return this.show(message, 'warning', options);
}
static info(message, options = {}) {
return this.show(message, 'info', options);
}
// Advanced toast types
static promise(promise, messages = {}, options = {}) {
const {
loading = 'Загрузка...',
success = 'Успешно!',
error = 'Ошибка!'
} = messages;
// Show loading toast
const loadingToast = this.show(loading, 'info', {
duration: 0, // Don't auto-close
showProgress: false,
...options
});
return promise
.then(result => {
// Close loading toast
if (loadingToast && loadingToast.toastElement) {
loadingToast.toastElement.remove();
}
// Show success toast
const successMessage = typeof success === 'function' ? success(result) : success;
this.success(successMessage, options);
return result;
})
.catch(err => {
// Close loading toast
if (loadingToast && loadingToast.toastElement) {
loadingToast.toastElement.remove();
}
// Show error toast
const errorMessage = typeof error === 'function' ? error(err) : error;
this.error(errorMessage, options);
throw err;
});
}
static confirm(message, options = {}) {
return new Promise((resolve) => {
const confirmOptions = {
duration: 0,
title: 'Подтверждение',
showProgress: false,
...options
};
const content = `
<div class="toast-enhanced__content">
<div class="toast-enhanced__icon">?</div>
<div class="toast-enhanced__text">
<div class="toast-enhanced__title">${this.escapeHtml(confirmOptions.title)}</div>
<div class="toast-enhanced__message">${this.escapeHtml(message)}</div>
<div class="toast-enhanced__actions" style="margin-top: 12px; display: flex; gap: 8px;">
<button class="btn btn--sm btn--primary toast-confirm-yes">Да</button>
<button class="btn btn--sm btn--secondary toast-confirm-no">Нет</button>
</div>
</div>
</div>
`;
const toast = Toastify({
text: content,
gravity: 'top',
position: 'center',
className: 'toastify toast-warning toast-enhanced toast-confirm',
escapeMarkup: false,
duration: 0,
stopOnFocus: true
});
toast.showToast();
// Add event listeners
const toastElement = toast.toastElement;
if (toastElement) {
const yesBtn = toastElement.querySelector('.toast-confirm-yes');
const noBtn = toastElement.querySelector('.toast-confirm-no');
if (yesBtn) {
yesBtn.addEventListener('click', () => {
toastElement.remove();
resolve(true);
});
}
if (noBtn) {
noBtn.addEventListener('click', () => {
toastElement.remove();
resolve(false);
});
}
}
});
}
// Utility method to remove all toasts
static clear() {
const toasts = document.querySelectorAll('.toastify');
toasts.forEach(toast => toast.remove());
}
// Method to update toast position for responsive design
static updatePosition() {
const isMobile = window.innerWidth < 768;
this.defaultOptions.position = isMobile ? 'center' : 'right';
}
}
// Update position on resize
if (typeof window !== 'undefined') {
window.addEventListener('resize', () => {
ToastComponent.updatePosition();
});
// Set initial position
ToastComponent.updatePosition();
}

Some files were not shown because too many files have changed in this diff Show More