├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .stylelintrc ├── GUIDE.md ├── GULP-GUIDE.md ├── README.md ├── csscomb.json ├── eslintconfig.cjs ├── gulp ├── compileHtml.mjs ├── compileScripts.mjs ├── compileStyles.mjs ├── copyAssets.mjs └── optimizeImages.mjs ├── gulpfile.js ├── package.json ├── source ├── fonts │ ├── rouble.woff │ └── rouble.woff2 ├── html │ ├── base │ │ ├── footer.html │ │ ├── head.html │ │ ├── header.html │ │ └── modal.html │ ├── components.html │ ├── components │ │ ├── modal-feedback.html │ │ └── modal-success.html │ ├── index.html │ └── sitemap.html ├── img │ ├── bg │ │ └── placeholder.jpg │ ├── content │ │ └── placeholder.jpg │ ├── slides │ │ └── placeholder.jpg │ ├── sprite │ │ └── icon-close.svg │ └── svg │ │ └── not-needed-in-sprite.svg ├── js │ ├── main.js │ ├── modules │ │ └── modals │ │ │ ├── init-modals.js │ │ │ └── modals.js │ ├── utils │ │ ├── focus-lock.js │ │ ├── ios-checker.js │ │ ├── ios-vh-fix.js │ │ └── scroll-lock.js │ ├── vendor.js │ └── vendor │ │ ├── focus-visible-polyfill.js │ │ └── swiper.js └── sass │ ├── blocks │ ├── btn.scss │ ├── footer.scss │ ├── header.scss │ ├── main-nav.scss │ └── modal.scss │ ├── functions.scss │ ├── global │ ├── container.scss │ ├── fonts.scss │ ├── reboot.scss │ └── utils.scss │ ├── keyframes.scss │ ├── mixins.scss │ ├── style.scss │ ├── variables.scss │ └── vendor │ ├── normalize.scss │ └── swiper.scss ├── stylelintconfig.cjs └── webpack.config.cjs /.editorconfig: -------------------------------------------------------------------------------- 1 | # Файл с настройками для редактора. 2 | # 3 | # Если вы разрабатываете в редакторе WebStorm, BBEdit, Coda или SourceLair 4 | # этот файл уже поддерживается и не нужно производить никаких дополнительных 5 | # действий. 6 | # 7 | # Если вы ведёте разработку в другом редакторе, зайдите 8 | # на https://editorconfig.org и в разделе «Download a Plugin» 9 | # скачайте дополнение для вашего редактора. 10 | 11 | root = true 12 | 13 | [*] 14 | charset = utf-8 15 | end_of_line = lf 16 | indent_size = 2 17 | indent_style = space 18 | insert_final_newline = true 19 | trim_trailing_whitespace = true 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | source/js/vendor/*.js 3 | gulpfile.js 4 | webpack.config.js 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | parserOptions: 4 | ecmaVersion: 2015 5 | sourceType: 'module' 6 | 7 | env: 8 | es6: true 9 | browser: true 10 | commonjs: true 11 | 12 | extends: "./eslintconfig.cjs" 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.avif binary 7 | *.webp binary 8 | *.webm binary 9 | *.gif binary 10 | *.woff binary 11 | *.woff2 binary 12 | *.pdf binary 13 | *.mp3 binary 14 | *.mp4 binary 15 | *.avi binary 16 | *.mov binary 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | package-lock.json 5 | *.ai 6 | *.log 7 | *.psd 8 | *.sublime* 9 | node_modules/ 10 | npm-debug.* 11 | source/css/*.css 12 | source/css/*.map 13 | Thumbs.db 14 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./stylelintconfig.cjs" 3 | } 4 | -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | # Гайд по работе со сборкой 2 | ## Основные отличия от старой сборки 3 | 1. PNG \ JPG автоматически не ужимаются, webp не создается. 4 | При необходимости используйте `npm run webp` и `npm run imagemin` 5 | Webp - создание в source, imagemin - оптимизация в build 6 | 2. `PostHtml` плагин для инклюдов заменен на `gulp-file-include` 7 | 3. Добавлены дополнительные вотчеры, чтобы при изменении js и картинок не приходилось перезапускать сборку 8 | 4. Добавлена **[сущность модалок](#Модалки)** 9 | 5. Немного изменен eslint. Косые кавычки теперь должны использоваться только в шаблонных строках 10 | 6. Добавлены различные фиксы для ie11 11 | 7. Обновлена файловая структура, добавлен пример `container.scss`, в `reboot.scss` собраны различные ресеты \ кроссбраузерные фиксы 12 | 13 | ❗ Обязательно обратите внимание на `chrome autofill background removal` в `reboot.scss`. Там необходимо заменить цвета на используемые в проекте. 14 | 15 | ## Gulp-file-include 16 | Так как в текущее время разработка большинства проектов связана с созданием копонентов - возникла необходимость передавать переменные в инклюды. Ведь в стандартном `posthtml-include` чтобы расширить компонент модификатором - приходилось создавать новый html файл, копипастить содержимое. И при изменении базы компонента - приходилось бы редактировать и модифицированную версию, а это бы нарушало принцип компонентного подхода. 17 | 18 | На помощь приходит плагин `gulp-file-include`. 19 | 20 | ❗ Важное уточнение. Если у вас крупный проект, множестно расширяемых компонентов - используйте `pug` или другой шаблонизатор, заточенный под такие задачи. Этот плагин не решит тех задач, которые стоят перед шаблонизаторами. 21 | 22 | ### Стандартный include 23 | Раньше для инклюда с `post-html` мы использовали такой синтаксис 24 | 25 | ```html 26 | 27 | ``` 28 | 29 | Теперь он станет таким 30 | 31 | ```html 32 | @@include("source/html/components/card.html") 33 | ``` 34 | 35 | ### Использования переменных. 36 | Важно понимать, что `gulp-file-include` подчиняется законам javascript, но у него есть свои особенности и ограничения. 37 | 38 | #### Объявление переменной 39 | 1. Локальное 40 | 41 | ``` 42 | @@include("source/html/components/card.html", { 43 | "title": "text", 44 | "mod": "tall" 45 | }) 46 | ``` 47 | 48 | ❗ Если внутри одного инклюда используется другой инклюд - переменная переданная в родителя не может быть прочитана у потомков. Чтобы решить эту проблему требуется создать цепочку передачи переменных. 49 | 50 | ``` 51 | @@include("source/html/components/card-child.html", { 52 | "title": "@@text" 53 | }) 54 | ``` 55 | 56 | 2. Глобальное 57 | 58 | Глобальное объявление переменных доступно только в `gulpfile.js`, объявить глобальную переменную на страницу невозможно. 59 | 60 | Глобальную переменную можно переопределить для конкретного инклюда просто передав её в него с другим значением. 61 | 62 | ❗ Тщательно подбирайте нейминг для глобальных переменных. Не `navLinks`, а `headerNavLinks`, чтобы любой разработчик мог уже в gulp понять область применения этой глобальной переменной. Также нельзя передавать в глобал переменную `mod`. 63 | 64 | ```js 65 | gulp.task(`html`, function () { 66 | return gulp.src([`source/html/*.html`]) 67 | .pipe(fileinclude({ 68 | prefix: `@@`, 69 | basepath: `@root`, 70 | context: { 71 | featuresCardTitle: "text", 72 | featuresCardMod: "tall" 73 | } 74 | })) 75 | .pipe(gulp.dest(`build`)); 76 | }); 77 | ``` 78 | 79 | #### Обращение к переменной 80 | 81 | ```html 82 |
83 |

@@title

84 |
85 | ``` 86 | 87 | ### Условия 88 | Переменные и условия можно вставить буквально куда угодно. В классы, атрибуты и пр. 89 | 90 | Чаще всего условия будут применяться, чтобы добавить модификатор к компоненту или если на одной странице в компонент переменная передается, а на другой нет. 91 | 92 | ```html 93 |
94 | @@if (context.title) { 95 |

@@title

96 | } 97 |
98 | ``` 99 | 100 | Также условия понадобятся для того чтобы `header` на разных страницах имел разные активные ссылки, а на главной логотип был без `href`(старый плагин так не умел, а некоторые заказчики просили отобразить смену пунктов меню и для этого приходилось плодить компонент шапки). 101 | 102 | В текущей сборке в `header` уже добавлены условия для решения этой задачи. Вам остается лишь копипастить пункты меню до нужного количества увеичивая индексы `context.headerActiveLinkIndex === 2` → `context.headerActiveLinkIndex === 3`. 103 | 104 | #### Почему в условиях стоит применять `(context.mod)`, а не `(mod)` как указано в спецификации плагина? 105 | Проблема может возникнуть, если компонент на одной странице передается с переменной, а на другой без + если данная переменная отсутствует в глобале в `gulpfile.js`. 106 | Если в таком случае написать `@@if (mod) { header--@@mod }`, то сборка выдаст ошибку 'mod is not defined', ведь на одной из страниц мы его не передаем. 107 | 108 | Решение этой проблемы - использование объекта `context`, который в этом плагине отвечает за попадание переменных в глобал `@@if (context.mod) { header--@@mod }`. 109 | 110 | ❗ Нельзя в переменных использовать дефис `test-1`, если в компоненте вы их вызываете через `context.test-1` 111 | 112 | ### Циклы. 113 | Циклы в этом плагине очень недружелюбны, по ним нет адекватной информации в спецификации. 114 | 115 | **В 99% случаев если на проекте необходимы циклы и такого рода компоненты - лучше использовать `pug`.** 116 | 117 | Но все же полезно будет узнать, что еще умеет этот плагин. 118 | 119 | Плагин парсит @@if раньше чем @@for. Соответственно если поместить @@if внутрь @@for, к примеру, `@@if (headerActiveLinkIndex == `+i+`)` - консоль выдаст ошибку, ведь на момент парсинга условия переменная `i` от цикла еще не была создана. Выход из этой проблемы - использование тернарного оператора. 120 | 121 | ```html 122 | 131 | ``` 132 | 133 | Также в gulp необходимо задать глобальные переменные, чтобы не копипастить их на каждую страницу, что нарушало бы логику компонентности 134 | 135 | ``` 136 | context: { 137 | headerActiveLinkIndex: null, 138 | headerNavLinks: [ 139 | { 140 | "text": "О компании", 141 | "href": "about.html" 142 | }, 143 | { 144 | "text": "Каталог", 145 | "href": "catalog.html" 146 | }, 147 | { 148 | "text": "Контакты", 149 | "href": "contacts.html" 150 | } 151 | ] 152 | ``` 153 | 154 | headerActiveLinkIndex переопределяется для каждого компонента по необходимости. Если же мы не задаем эту переменную для определенной страницы - сборка не выдаст ошибку. Ведь в глобале есть `null`. 155 | 156 | --- 157 | 158 | ## Модалки 159 | В сборку добавлена сущность модалок. 160 | 161 | `html/base/modal.html` + `sass/blocks/modal.scss` + `js/utils/modal.js` + `js/modules/init-modals.js` 162 | 163 | Также в main.html добавлены две модалки для примера. 164 | 165 | ### Создаем модалку 166 | 1. В html для интерактивных элементов вызывающих модалку вам необходимо добавить `data-modal="x"`. 167 | 2. В инклюд передаем `name` соответствующий второй части имени файла модалки в `html/components/modal-name` и соответствующий тому что вы передали в 1 пункте, который также автоматически создаст модификатор у модалки `modal--@@name` 168 | 3. Передаем `fitContent`, если у вас стоит задача, чтобы модалка подстраивалась под ширину контента 169 | 4. Передаем `mod`, если нам необходим дополнительный модификатор (к примеру, `no-overlay`) 170 | 171 | ```html 172 | 173 | 174 | 175 | 176 | @@include("source/html/base/modal.html", { 177 | "name": "success", 178 | "fitContent": true, 179 | "mod": "some-mod" 180 | }) 181 | ``` 182 | 183 | В `js/modules/init-modals.js` необходимо найти модалку + ссылки на нее и передать как аргументы в `setupModal()`. 184 | 185 | ```js 186 | // аргументы setupModal(modal, closeCallback, modalBtns, openCallback, noPrevDefault) 187 | // возможна инициализация только с первыми аргументом, 188 | // если вам нужно открывать модалку в другом месте под какими-нибудь условиями 189 | const initModals = () => { 190 | if (modalFeedback && modalFeedbackBtns.length) { 191 | setupModal(modalFeedback, false, modalFeedbackBtns, false, false); 192 | } 193 | if (modalSuccess && modalSuccessBtns.length) { 194 | setupModal(modalSuccess, false, modalSuccessBtns); 195 | } 196 | }; 197 | ``` 198 | 199 | Готово. Остается поправить стили через модификатор при необходимости. 200 | Также стоит учесть, если на проекте в разных модалках одинаковый размер заголовков, вертикальные отступы и т.д. - такие элементы имеет смысл внести в базу `sass/blocks/modal.scss`, а не стилизовать каждый раз отдельно для каждой модалки. 201 | 202 | ❗ Новые базовые модификаторы пишем в `sass/blocks/modal.scss`, а под каждую новую модалку, если ей требуется дополнительная стилизация стоит создавать новый scss файл, а не плодить модификаторы в базовом `modal.scss`. 203 | 204 | --- 205 | 206 | ### Если вы незнакомы с gulp или вам не до конца понятна как работает сборка - предлагаем ознакомиться с [кратким описанием работы gulp](/GULP-GUIDE.md) 📘 207 | -------------------------------------------------------------------------------- /GULP-GUIDE.md: -------------------------------------------------------------------------------- 1 | # Краткоe описание работы gulp 2 | 3 | Запустив команду `npm i` на проект установятся все зависимости, необходимые для работы. 4 | 5 | Основные таски сборки представлены в виде модулей и сложены в папку gulp находящуюся в корне проекта 6 | 7 | В gulpfile.js мы импортируем зависимости. 8 | 9 | ```js 10 | import gulp from 'gulp'; // основа gulp 11 | import browserSync from 'browser-sync'; // дополнительный плагин 12 | import del from 'del'; // дополнительный плагин 13 | ``` 14 | 15 | Далее мы импортируем таски из модулей. 16 | 17 | ```js 18 | import styles from './gulp/compileStyles.mjs'; // стили 19 | import { copy, copyImages, copySvg } from './gulp/copyAssets.mjs'; // копирование 20 | import js from './gulp/compileScripts.mjs'; // js 21 | import { svgo, sprite, createWebp, optimizeImages } from './gulp/optimizeImages.mjs'; // работа с графикой 22 | import html from './gulp/compileHtml.mjs'; // html 23 | ``` 24 | 25 | Пример модуля с таской 26 | 27 | Сначала мы импортируем все необходимые зависимости. 28 | 29 | ```js 30 | import gulp from 'gulp'; 31 | import plumber from 'gulp-plumber'; 32 | import dartSass from 'sass'; 33 | import gulpSass from 'gulp-sass'; 34 | import postcss from 'gulp-postcss'; 35 | import autoprefixer from 'autoprefixer'; 36 | import csso from 'gulp-csso'; 37 | import gcmq from 'gulp-group-css-media-queries'; 38 | import rename from 'gulp-rename'; 39 | ``` 40 | 41 | Далее создаём функцию. 42 | 43 | ```js 44 | const sass = gulpSass(dartSass); 45 | 46 | const compileStyles = () => 47 | gulp.src('source/sass/style.scss', {sourcemaps: true}) 48 | .pipe(plumber()) 49 | .pipe(sass()) 50 | .pipe(postcss([autoprefixer({ 51 | grid: true, 52 | })])) 53 | .pipe(gcmq()) // выключите, если в проект импортятся шрифты через ссылку на внешний источник 54 | .pipe(gulp.dest('build/css')) 55 | .pipe(csso()) 56 | .pipe(rename('style.min.css')) 57 | .pipe(gulp.dest('build/css', {sourcemaps: '.'})); 58 | ``` 59 | Далее экспортим функцию. 60 | 61 | ```js 62 | export default compileStyles; 63 | ``` 64 | 65 | В gulpfile.js оставдены только базовые таски. 66 | 67 | ```js 68 | const server = browserSync.create(); 69 | const streamStyles = () => styles().pipe(server.stream()); 70 | const clean = () => del('build'); 71 | const refresh = (done) => { 72 | server.reload(); 73 | done(); 74 | }; 75 | 76 | 77 | const syncServer = () => { 78 | server.init({ 79 | server: 'build/', 80 | index: 'sitemap.html', 81 | notify: false, 82 | open: true, 83 | cors: true, 84 | ui: false, 85 | }); 86 | 87 | gulp.watch('source/pug/**/*.pug', gulp.series(html, refresh)); 88 | gulp.watch('source/sass/**/*.{scss,sass}', streamStyles); 89 | gulp.watch('source/js/**/*.{js,json}', gulp.series(js, refresh)); 90 | gulp.watch('source/data/**/*.{js,json}', gulp.series(copy, refresh)); 91 | gulp.watch('source/img/**/*.svg', gulp.series(copySvg, sprite, html, refresh)); 92 | gulp.watch('source/img/**/*.{png,jpg,webp}', gulp.series(copyImages, html, refresh)); 93 | 94 | gulp.watch('source/favicon/**', gulp.series(copy, refresh)); 95 | gulp.watch('source/video/**', gulp.series(copy, refresh)); 96 | gulp.watch('source/downloads/**', gulp.series(copy, refresh)); 97 | gulp.watch('source/*.php', gulp.series(copy, refresh)); 98 | }; 99 | ``` 100 | 101 | Создание и экспорт комманд. 102 | 103 | ```js 104 | const build = gulp.series(clean, svgo, copy, styles, sprite, js, html); 105 | const start = gulp.series(build, syncServer); 106 | 107 | export { optimizeImages as imagemin, createWebp as webp, build, start }; 108 | ``` 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [Гайд по работе со сборкой](/GUIDE.md) 📕 2 | 3 | ## Краткая инструкция по работе 4 | Для начала работы у вас должент быть установлен **Node.js** 14 версии 5 | 6 | ### Основные команды для работы 7 | - Установка - `npm i` 8 | - Запуск локального сервера - `npm start` 9 | - Сборка проекта, минификация скриптов
10 | и оптимизация изображений перед деплоем на прод - `npm run build` 11 | - Запуск тестирования на соответствия кодгайдам - `npm test` 12 | - Создание webp изображений в директории source - `npm run webp` 13 | 14 | ### Вся разработка ведётся в директории `source` 15 | ### Итоговый код попадает в директорию `build` 16 | -------------------------------------------------------------------------------- /csscomb.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | ".git/**", 4 | "node_modules/**" 5 | ], 6 | 7 | "always-semicolon": true, 8 | "block-indent": " ", 9 | "color-case": "lower", 10 | "color-shorthand": false, 11 | "element-case": "lower", 12 | "eof-newline": true, 13 | "leading-zero": true, 14 | "quotes": "double", 15 | "remove-empty-rulesets": true, 16 | "sort-order-fallback": "abc", 17 | "space-before-colon": "", 18 | "space-after-colon": " ", 19 | "space-before-combinator": " ", 20 | "space-after-combinator": " ", 21 | "space-between-declarations": "\n", 22 | "space-before-opening-brace": " ", 23 | "space-after-opening-brace": "\n", 24 | "space-before-selector-delimiter": "", 25 | "space-after-selector-delimiter": "\n", 26 | "space-before-closing-brace": "\n", 27 | "strip-spaces": true, 28 | "tab-size": 2, 29 | "unitless-zero": true, 30 | "vendor-prefix-align": true, 31 | "sort-order": [ 32 | [ 33 | "content", 34 | "position", 35 | "top", 36 | "right", 37 | "bottom", 38 | "left", 39 | "z-index" 40 | ], 41 | [ 42 | "display", 43 | "-webkit-box", 44 | "-webkit-flex", 45 | "-moz-box", 46 | "-ms-flexbox", 47 | "flex", 48 | "-webkit-box-flex", 49 | "-webkit-flex-grow", 50 | "-moz-box-flex", 51 | "-ms-flex-positive", 52 | "flex-grow", 53 | "-webkit-flex-shrink", 54 | "-ms-flex-negative", 55 | "flex-shrink", 56 | "-webkit-flex-basis", 57 | "-ms-flex-preferred-size", 58 | "flex-basis", 59 | "-webkit-flex-flow", 60 | "-ms-flex-flow", 61 | "flex-flow", 62 | "-webkit-box-orient", 63 | "-webkit-box-direction", 64 | "-webkit-flex-direction", 65 | "-moz-box-orient", 66 | "-moz-box-direction", 67 | "-ms-flex-direction", 68 | "flex-direction", 69 | "-webkit-flex-wrap", 70 | "-ms-flex-wrap", 71 | "flex-wrap", 72 | "-webkit-box-pack", 73 | "-webkit-justify-content", 74 | "-moz-box-pack", 75 | "-ms-flex-pack", 76 | "justify-content", 77 | "-webkit-align-content", 78 | "-ms-flex-line-pack", 79 | "align-content", 80 | "-webkit-box-align", 81 | "-webkit-align-items", 82 | "-moz-box-align", 83 | "-ms-flex-align", 84 | "align-items", 85 | "-webkit-box-ordinal-group", 86 | "-webkit-order", 87 | "-moz-box-ordinal-group", 88 | "-ms-flex-order", 89 | "order", 90 | "-webkit-align-self", 91 | "-ms-flex-item-align", 92 | "-ms-grid-row-align", 93 | "align-self", 94 | "float", 95 | "clear", 96 | "-webkit-box-sizing", 97 | "-moz-box-sizing", 98 | "box-sizing", 99 | "width", 100 | "min-width", 101 | "max-width", 102 | "height", 103 | "min-height", 104 | "max-height", 105 | "margin", 106 | "margin-top", 107 | "margin-right", 108 | "margin-bottom", 109 | "margin-left", 110 | "padding", 111 | "padding-top", 112 | "padding-right", 113 | "padding-bottom", 114 | "padding-left", 115 | "overflow", 116 | "-ms-overflow-x", 117 | "overflow-x", 118 | "-ms-overflow-y", 119 | "overflow-y", 120 | "-webkit-overflow-scrolling" 121 | ], 122 | [ 123 | "list-style", 124 | "list-style-position", 125 | "list-style-type", 126 | "list-style-image", 127 | "border-collapse", 128 | "border-spacing", 129 | "table-layout", 130 | "empty-cells", 131 | "caption-side", 132 | "font", 133 | "font-style", 134 | "font-variant", 135 | "font-weight", 136 | "font-size", 137 | "line-height", 138 | "font-family", 139 | "vertical-align", 140 | "text-align", 141 | "direction", 142 | "color", 143 | "text-transform", 144 | "-webkit-text-decoration", 145 | "-moz-text-decoration", 146 | "text-decoration", 147 | "font-size-adjust", 148 | "font-stretch", 149 | "font-effect", 150 | "font-emphasize", 151 | "font-emphasize-position", 152 | "font-emphasize-style", 153 | "font-smooth", 154 | "-webkit-text-align-last", 155 | "-moz-text-align-last", 156 | "-ms-text-align-last", 157 | "text-align-last", 158 | "letter-spacing", 159 | "word-spacing", 160 | "white-space", 161 | "-webkit-text-emphasis", 162 | "text-emphasis", 163 | "-webkit-text-emphasis-color", 164 | "text-emphasis-color", 165 | "-webkit-text-emphasis-style", 166 | "text-emphasis-style", 167 | "-webkit-text-emphasis-position", 168 | "text-emphasis-position", 169 | "text-indent", 170 | "-ms-text-justify", 171 | "text-justify", 172 | "-ms-writing-mode", 173 | "text-outline", 174 | "text-wrap", 175 | "-ms-text-overflow", 176 | "-o-text-overflow", 177 | "text-overflow", 178 | "text-overflow-ellipsis", 179 | "text-overflow-mode", 180 | "text-orientation", 181 | "-ms-word-wrap", 182 | "word-wrap", 183 | "-ms-word-break", 184 | "word-break", 185 | "-moz-tab-size", 186 | "-o-tab-size", 187 | "tab-size", 188 | "-webkit-hyphens", 189 | "-moz-hyphens", 190 | "-ms-hyphens", 191 | "hyphens", 192 | "unicode-bidi", 193 | "-webkit-columns", 194 | "-moz-columns", 195 | "columns", 196 | "-webkit-column-count", 197 | "-moz-column-count", 198 | "column-count", 199 | "-webkit-column-fill", 200 | "-moz-column-fill", 201 | "column-fill", 202 | "-webkit-column-gap", 203 | "-moz-column-gap", 204 | "column-gap", 205 | "-webkit-column-rule", 206 | "-moz-column-rule", 207 | "column-rule", 208 | "-webkit-column-rule-color", 209 | "-moz-column-rule-color", 210 | "column-rule-color", 211 | "-webkit-column-rule-style", 212 | "-moz-column-rule-style", 213 | "column-rule-style", 214 | "-webkit-column-rule-width", 215 | "-moz-column-rule-width", 216 | "column-rule-width", 217 | "-webkit-column-span", 218 | "-moz-column-span", 219 | "column-span", 220 | "-webkit-column-width", 221 | "-moz-column-width", 222 | "column-width", 223 | "text-shadow", 224 | "page-break-after", 225 | "page-break-before", 226 | "page-break-inside" 227 | ], 228 | [ 229 | "background", 230 | "background-color", 231 | "background-image", 232 | "-webkit-gradient", 233 | "-webkit-linear-gradient", 234 | "-moz-linear-gradient", 235 | "-o-linear-gradient", 236 | "linear-gradient", 237 | "background-repeat", 238 | "background-position", 239 | "-ms-background-position-x", 240 | "background-position-x", 241 | "-ms-background-position-y", 242 | "background-position-y", 243 | "-webkit-background-size", 244 | "-moz-background-size", 245 | "-o-background-size", 246 | "background-size", 247 | "-webkit-background-clip", 248 | "-moz-background-clip", 249 | "background-clip", 250 | "-webkit-background-origin", 251 | "-moz-background-origin", 252 | "-o-background-origin", 253 | "background-origin", 254 | "background-attachment", 255 | "-webkit-box-decoration-break", 256 | "box-decoration-break", 257 | "background-blend-mode", 258 | "border", 259 | "border-width", 260 | "border-style", 261 | "border-color", 262 | "border-top", 263 | "border-top-width", 264 | "border-top-style", 265 | "border-top-color", 266 | "border-right", 267 | "border-right-width", 268 | "border-right-style", 269 | "border-right-color", 270 | "border-bottom", 271 | "border-bottom-width", 272 | "border-bottom-style", 273 | "border-bottom-color", 274 | "border-left", 275 | "border-left-width", 276 | "border-left-style", 277 | "border-left-color", 278 | "-webkit-border-radius", 279 | "-moz-border-radius", 280 | "border-radius", 281 | "-webkit-border-top-left-radius", 282 | "-moz-border-radius-topleft", 283 | "border-top-left-radius", 284 | "-webkit-border-top-right-radius", 285 | "-moz-border-radius-topright", 286 | "border-top-right-radius", 287 | "-webkit-border-bottom-right-radius", 288 | "-moz-border-radius-bottomright", 289 | "border-bottom-right-radius", 290 | "-webkit-border-bottom-left-radius", 291 | "-moz-border-radius-bottomleft", 292 | "border-bottom-left-radius", 293 | "-webkit-border-image", 294 | "-moz-border-image", 295 | "-o-border-image", 296 | "border-image", 297 | "-webkit-border-image-source", 298 | "-moz-border-image-source", 299 | "-o-border-image-source", 300 | "border-image-source", 301 | "-webkit-border-image-slice", 302 | "-moz-border-image-slice", 303 | "-o-border-image-slice", 304 | "border-image-slice", 305 | "-webkit-border-image-width", 306 | "-moz-border-image-width", 307 | "-o-border-image-width", 308 | "border-image-width", 309 | "-webkit-border-image-outset", 310 | "-moz-border-image-outset", 311 | "-o-border-image-outset", 312 | "border-image-outset", 313 | "-webkit-border-image-repeat", 314 | "-moz-border-image-repeat", 315 | "-o-border-image-repeat", 316 | "border-image-repeat", 317 | "outline", 318 | "outline-width", 319 | "outline-style", 320 | "outline-color", 321 | "outline-offset", 322 | "-webkit-box-shadow", 323 | "-moz-box-shadow", 324 | "box-shadow", 325 | "-webkit-transform", 326 | "-moz-transform", 327 | "-ms-transform", 328 | "-o-transform", 329 | "transform", 330 | "-webkit-transform-origin", 331 | "-moz-transform-origin", 332 | "-ms-transform-origin", 333 | "-o-transform-origin", 334 | "transform-origin", 335 | "-webkit-backface-visibility", 336 | "-moz-backface-visibility", 337 | "backface-visibility", 338 | "-webkit-perspective", 339 | "-moz-perspective", 340 | "perspective", 341 | "-webkit-perspective-origin", 342 | "-moz-perspective-origin", 343 | "perspective-origin", 344 | "-webkit-transform-style", 345 | "-moz-transform-style", 346 | "transform-style", 347 | "visibility", 348 | "cursor", 349 | "opacity", 350 | "-webkit-filter", 351 | "filter" 352 | ], 353 | [ 354 | "-webkit-transition", 355 | "-moz-transition", 356 | "-ms-transition", 357 | "-o-transition", 358 | "transition", 359 | "-webkit-transition-delay", 360 | "-moz-transition-delay", 361 | "-ms-transition-delay", 362 | "-o-transition-delay", 363 | "transition-delay", 364 | "-webkit-transition-timing-function", 365 | "-moz-transition-timing-function", 366 | "-ms-transition-timing-function", 367 | "-o-transition-timing-function", 368 | "transition-timing-function", 369 | "-webkit-transition-duration", 370 | "-moz-transition-duration", 371 | "-ms-transition-duration", 372 | "-o-transition-duration", 373 | "transition-duration", 374 | "-webkit-transition-property", 375 | "-moz-transition-property", 376 | "-ms-transition-property", 377 | "-o-transition-property", 378 | "transition-property", 379 | "-webkit-animation", 380 | "-moz-animation", 381 | "-ms-animation", 382 | "-o-animation", 383 | "animation", 384 | "-webkit-animation-name", 385 | "-moz-animation-name", 386 | "-ms-animation-name", 387 | "-o-animation-name", 388 | "animation-name", 389 | "-webkit-animation-duration", 390 | "-moz-animation-duration", 391 | "-ms-animation-duration", 392 | "-o-animation-duration", 393 | "animation-duration", 394 | "-webkit-animation-play-state", 395 | "-moz-animation-play-state", 396 | "-ms-animation-play-state", 397 | "-o-animation-play-state", 398 | "animation-play-state", 399 | "-webkit-animation-timing-function", 400 | "-moz-animation-timing-function", 401 | "-ms-animation-timing-function", 402 | "-o-animation-timing-function", 403 | "animation-timing-function", 404 | "-webkit-animation-delay", 405 | "-moz-animation-delay", 406 | "-ms-animation-delay", 407 | "-o-animation-delay", 408 | "animation-delay", 409 | "-webkit-animation-iteration-count", 410 | "-moz-animation-iteration-count", 411 | "-ms-animation-iteration-count", 412 | "-o-animation-iteration-count", 413 | "animation-iteration-count", 414 | "-webkit-animation-direction", 415 | "-moz-animation-direction", 416 | "-ms-animation-direction", 417 | "-o-animation-direction", 418 | "animation-direction" 419 | ], 420 | [ 421 | "quotes", 422 | "counter-reset", 423 | "counter-increment", 424 | "resize", 425 | "-webkit-user-select", 426 | "-moz-user-select", 427 | "-ms-user-select", 428 | "user-select", 429 | "nav-index", 430 | "nav-up", 431 | "nav-right", 432 | "nav-down", 433 | "nav-left", 434 | "pointer-events", 435 | "will-change", 436 | "clip", 437 | "-webkit-clip-path", 438 | "clip-path", 439 | "zoom" 440 | ] 441 | ] 442 | } 443 | -------------------------------------------------------------------------------- /eslintconfig.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | // ECMAScript 6 4 | // http://eslint.org/docs/rules/#ecmascript-6 5 | // ------------------------------------------ 6 | 'no-cond-assign': 'error', // eslint:recommended 7 | 'no-irregular-whitespace': 'error', // eslint:recommended 8 | 'no-unexpected-multiline': 'error', // eslint:recommended 9 | 'valid-jsdoc': ['error', { 10 | requireParamDescription: false, 11 | requireReturnDescription: false, 12 | requireReturn: false, 13 | prefer: {returns: 'return'}, 14 | }], 15 | 16 | 'no-console': 'error', 17 | 'no-constant-condition': 'error', 18 | 'no-control-regex': 'error', 19 | 'no-debugger': 'error', 20 | 'no-dupe-keys': 'error', 21 | 'no-dupe-args': 'error', 22 | 'no-duplicate-case': 'error', 23 | 'no-empty': 'error', 24 | 'no-empty-character-class': 'error', 25 | 'no-ex-assign': 'error', 26 | 'no-extra-boolean-cast': 'error', 27 | 'no-extra-semi': 'error', 28 | 'no-func-assign': 'error', 29 | 'no-inner-declarations': ['error', 'functions'], 30 | 'no-invalid-regexp': 'error', 31 | 'no-unsafe-negation': 'error', 32 | 'no-obj-calls': 'error', 33 | 'no-regex-spaces': 'error', 34 | 'no-sparse-arrays': 'error', 35 | 'no-unreachable': 'error', 36 | 'use-isnan': 'error', 37 | 'valid-typeof': 'error', 38 | 39 | // Best Practices 40 | // http://eslint.org/docs/rules/#best-practices 41 | // -------------------------------------------- 42 | 43 | 'guard-for-in': 'error', 44 | 'max-nested-callbacks': ['error', {max: 3}], 45 | 'no-caller': 'error', 46 | 'no-extend-native': 'error', 47 | 'no-extra-bind': 'error', 48 | 'no-invalid-this': 'error', 49 | 'no-multi-spaces': 'error', 50 | 'no-multi-str': 'error', 51 | 'no-new-wrappers': 'error', 52 | 'no-throw-literal': 'error', // eslint:recommended 53 | 'no-with': 'error', 54 | 'consistent-return': 'error', 55 | 'curly': ['error', 'all'], 56 | 'eqeqeq': 'error', 57 | 'no-alert': 'error', 58 | 'no-eval': 'error', 59 | 'no-fallthrough': 'error', 60 | 'no-floating-decimal': 'error', 61 | 'no-implied-eval': 'error', 62 | 'no-iterator': 'error', 63 | 'no-labels': 'error', 64 | 'no-lone-blocks': 'error', 65 | 'no-global-assign': 'error', 66 | 'no-new': 'error', 67 | 'no-new-func': 'error', 68 | 'no-octal': 'error', // default 69 | 'no-octal-escape': 'error', 70 | 'no-proto': 'error', 71 | 'no-redeclare': 'error', // default 72 | 'no-return-assign': 'error', 73 | 'no-script-url': 'error', 74 | 'no-sequences': 'error', 75 | 'no-unused-expressions': 'error', 76 | 'radix': 'error', 77 | 78 | // Variables 79 | // http://eslint.org/docs/rules/#variables 80 | // --------------------------------------- 81 | 'no-unused-vars': ['error', {args: 'after-used', argsIgnorePattern: '^_'}], // check that all args are used¬ 82 | 'no-delete-var': 'error', // eslint:recommended 83 | 'no-label-var': 'error', 84 | 'no-shadow': 'error', 85 | 'no-shadow-restricted-names': 'error', 86 | 'no-undef': 'error', // default 87 | 'no-undef-init': 'error', 88 | 'no-undefined': 'error', 89 | 90 | // Node.js and CommonJS 91 | // http://eslint.org/docs/rules/#nodejs-and-commonjs 92 | // ------------------------------------------------- 93 | 'no-process-exit': 'error', 94 | 95 | // Stylistic Issues 96 | // http://eslint.org/docs/rules/#stylistic-issues 97 | // ---------------------------------------------- 98 | 'indent': ['error', 2, { 99 | SwitchCase: 1, 100 | // continuation indent 101 | VariableDeclarator: 1, // indent is multiplier * indent = 1 * 2 102 | MemberExpression: 2, // indent is multiplier * indent = 2 * 2 103 | FunctionDeclaration: {parameters: 2}, 104 | FunctionExpression: {parameters: 2}, 105 | CallExpression: {arguments: 2}, 106 | }], 107 | 'block-spacing': ['error', 'always'], 108 | 'array-bracket-spacing': ['error', 'never'], 109 | 'brace-style': 'error', 110 | 'camelcase': 'error', 111 | 'comma-spacing': 'error', 112 | 'comma-style': 'error', 113 | 'computed-property-spacing': 'error', 114 | 'eol-last': 'error', 115 | 'func-call-spacing': 'error', 116 | 'key-spacing': 'error', 117 | 'keyword-spacing': 'error', 118 | 'linebreak-style': 'off', // check this in git 119 | 'new-cap': 'error', 120 | 'no-array-constructor': 'error', 121 | 'no-mixed-spaces-and-tabs': 'error', // eslint:recommended 122 | 'no-multiple-empty-lines': ['error', {max: 2}], 123 | 'no-new-object': 'error', 124 | 'no-trailing-spaces': 'error', 125 | 'object-curly-spacing': 'error', 126 | 'one-var': ['error', { 127 | var: 'never', 128 | let: 'never', 129 | const: 'never', 130 | }], 131 | 'padded-blocks': ['off', 'never'], 132 | 'quote-props': ['error', 'consistent'], 133 | 'semi-spacing': 'error', 134 | 'semi': 'error', 135 | 'space-in-parens': ['error', 'never'], 136 | 'space-before-blocks': 'error', 137 | 'space-before-function-paren': ['error', {named: 'never', anonymous: 'always'}], 138 | 'spaced-comment': ['error', 'always'], 139 | 'unicode-bom': 'warn', 140 | 'new-parens': 'error', 141 | 'no-nested-ternary': 'error', 142 | 'space-infix-ops': 'error', 143 | 'space-unary-ops': ['error', {words: true, nonwords: false}], 144 | 'yoda': ['error', 'never'], 145 | 146 | 'arrow-parens': ['error', 'always'], 147 | 148 | // parens are optional but recommended. 149 | // ESLint doesn't support a *consistent* 150 | // setting so "always" is used. 151 | 'constructor-super': 'error', // eslint:recommended 152 | 'generator-star-spacing': ['error', 'after'], 153 | 'no-new-symbol': 'error', // eslint:recommended 154 | 'no-this-before-super': 'error', // eslint:recommended 155 | 'no-var': 'error', 156 | 'prefer-rest-params': 'error', 157 | 'prefer-spread': 'error', 158 | 'rest-spread-spacing': 'error', 159 | 'yield-star-spacing': ['error', 'after'], 160 | 'object-shorthand': ['error', 'always', {'avoidQuotes': true}], 161 | 162 | 'comma-dangle': ['error', { 163 | 'arrays': 'never', 164 | 'objects': 'always-multiline', 165 | 'imports': 'never', 166 | 'exports': 'never', 167 | 'functions': 'never', 168 | }], 169 | 'quotes': ['error', 'single'], 170 | }, 171 | }; 172 | -------------------------------------------------------------------------------- /gulp/compileHtml.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import fileinclude from 'gulp-file-include'; 3 | import htmlbeautify from 'gulp-html-beautify'; 4 | 5 | const compileHtml = () => { 6 | return gulp.src(['source/html/*.html']) 7 | .pipe(fileinclude({ 8 | prefix: '@@', 9 | basepath: '@root', 10 | context: { // глобальные переменные для include 11 | test: 'text', 12 | }, 13 | })) 14 | .pipe(htmlbeautify({ 15 | 'indent_size': 2, 16 | 'preserve_newlines': true, 17 | 'max_preserve_newlines': 0, 18 | 'wrap_attributes': 'auto', 19 | })) 20 | .pipe(gulp.dest('build')); 21 | }; 22 | 23 | export default compileHtml; 24 | -------------------------------------------------------------------------------- /gulp/compileScripts.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import webpackStream from 'webpack-stream'; 3 | import webpackConfig from '../webpack.config.cjs'; 4 | 5 | const compileScripts = () => 6 | gulp.src(['source/js/main.js']) 7 | .pipe(webpackStream(webpackConfig)) 8 | .pipe(gulp.dest('build/js')); 9 | 10 | export default compileScripts; 11 | -------------------------------------------------------------------------------- /gulp/compileStyles.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import dartSass from 'sass'; 3 | import gulpSass from 'gulp-sass'; 4 | import postcss from 'gulp-postcss'; 5 | import autoprefixer from 'autoprefixer'; 6 | import csso from 'gulp-csso'; 7 | import gcmq from 'gulp-group-css-media-queries'; 8 | import rename from 'gulp-rename'; 9 | 10 | const sass = gulpSass(dartSass); 11 | 12 | const compileStyles = () => 13 | gulp.src('source/sass/style.scss', {sourcemaps: true}) 14 | .pipe(sass().on('error', sass.logError)) 15 | .pipe(postcss([autoprefixer({ 16 | grid: true, 17 | })])) 18 | .pipe(gcmq()) // выключите, если в проект импортятся шрифты через ссылку на внешний источник 19 | .pipe(gulp.dest('build/css')) 20 | .pipe(csso()) 21 | .pipe(rename('style.min.css')) 22 | .pipe(gulp.dest('build/css', {sourcemaps: '.'})); 23 | 24 | export default compileStyles; 25 | -------------------------------------------------------------------------------- /gulp/copyAssets.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | 3 | const copySvg = () => 4 | gulp.src('source/img/**/*.svg', {base: 'source'}) 5 | .pipe(gulp.dest('build')); 6 | 7 | const copyImages = () => 8 | gulp.src('source/img/**/*.{png,jpg,webp}', {base: 'source'}) 9 | .pipe(gulp.dest('build')); 10 | 11 | const copy = () => 12 | gulp.src([ 13 | 'source/**.html', 14 | 'source/fonts/**', 15 | 'source/img/**', 16 | 'source/favicon/**' 17 | ], { 18 | base: 'source', 19 | }) 20 | .pipe(gulp.dest('build')); 21 | 22 | export {copy, copyImages, copySvg}; 23 | -------------------------------------------------------------------------------- /gulp/optimizeImages.mjs: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import rename from 'gulp-rename'; 3 | import imagemin from 'gulp-imagemin'; 4 | import webp from 'gulp-webp'; 5 | import svgstore from 'gulp-svgstore'; 6 | 7 | const svgo = () => 8 | gulp 9 | .src('source/img/**/*.{svg}') 10 | .pipe( 11 | imagemin([ 12 | imagemin.svgo({ 13 | plugins: [ 14 | {removeViewBox: false}, 15 | {removeRasterImages: true}, 16 | {removeUselessStrokeAndFill: false} 17 | ], 18 | }) 19 | ]) 20 | ) 21 | .pipe(gulp.dest('source/img')); 22 | 23 | const sprite = () => 24 | gulp 25 | .src('source/img/sprite/*.svg') 26 | .pipe(svgstore({inlineSvg: true})) 27 | .pipe(rename('sprite_auto.svg')) 28 | .pipe(gulp.dest('build/img')); 29 | 30 | /* 31 | Optional tasks 32 | --------------------------------- 33 | 34 | Используйте отличное от дефолтного значение root, если нужно обработать отдельную папку в img, 35 | а не все изображения в img во всех папках. 36 | 37 | root = '' - по дефолту webp добавляются и обновляются во всех папках в source/img/ 38 | root = 'content/' - webp добавляются и обновляются только в source/img/content/ 39 | */ 40 | 41 | const createWebp = () => { 42 | const root = ''; 43 | return gulp 44 | .src(`source/img/${root}**/*.{png,jpg}`) 45 | .pipe(webp({quality: 90})) 46 | .pipe(gulp.dest(`source/img/${root}`)); 47 | }; 48 | 49 | const optimizeImages = () => 50 | gulp 51 | .src('build/img/**/*.{png,jpg}') 52 | .pipe( 53 | imagemin([ 54 | imagemin.optipng({optimizationLevel: 3}), 55 | imagemin.mozjpeg({quality: 75, progressive: true}) 56 | ]) 57 | ) 58 | .pipe(gulp.dest('build/img')); 59 | 60 | export {svgo, sprite, createWebp, optimizeImages}; 61 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import browserSync from 'browser-sync'; 3 | import del from 'del'; 4 | import styles from './gulp/compileStyles.mjs'; 5 | import { copy, copyImages, copySvg } from './gulp/copyAssets.mjs'; 6 | import js from './gulp/compileScripts.mjs'; 7 | import { svgo, sprite, createWebp, optimizeImages } from './gulp/optimizeImages.mjs'; 8 | import html from './gulp/compileHtml.mjs'; 9 | 10 | const server = browserSync.create(); 11 | const streamStyles = () => styles().pipe(server.stream()); 12 | const clean = () => del('build'); 13 | const refresh = (done) => { 14 | server.reload(); 15 | done(); 16 | }; 17 | 18 | 19 | const syncServer = () => { 20 | server.init({ 21 | server: 'build/', 22 | index: 'sitemap.html', 23 | notify: false, 24 | open: true, 25 | cors: true, 26 | ui: false, 27 | }); 28 | 29 | gulp.watch('source/pug/**/*.pug', gulp.series(html, refresh)); 30 | gulp.watch('source/sass/**/*.{scss,sass}', streamStyles); 31 | gulp.watch('source/js/**/*.{js,json}', gulp.series(js, refresh)); 32 | gulp.watch('source/data/**/*.{js,json}', gulp.series(copy, refresh)); 33 | gulp.watch('source/img/**/*.svg', gulp.series(copySvg, sprite, html, refresh)); 34 | gulp.watch('source/img/**/*.{png,jpg,webp}', gulp.series(copyImages, html, refresh)); 35 | 36 | gulp.watch('source/favicon/**', gulp.series(copy, refresh)); 37 | gulp.watch('source/video/**', gulp.series(copy, refresh)); 38 | gulp.watch('source/downloads/**', gulp.series(copy, refresh)); 39 | gulp.watch('source/*.php', gulp.series(copy, refresh)); 40 | }; 41 | 42 | 43 | const build = gulp.series(clean, svgo, copy, styles, sprite, js, html); 44 | const start = gulp.series(build, syncServer); 45 | 46 | export { optimizeImages as imagemin, createWebp as webp, build, start }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "browserslist": [ 5 | "last 3 versions", 6 | "IE 11", 7 | "Firefox ESR" 8 | ], 9 | "scripts": { 10 | "editorconfig": "editorconfig-cli", 11 | "stylelint": "stylelint \"source/sass/**/*.scss\" --syntax scss", 12 | "test": "npm run editorconfig & npm run stylelint & eslint source/js/", 13 | "build": "cross-env NODE_ENV=production gulp build & gulp imagemin", 14 | "start": "cross-env NODE_ENV=development gulp start", 15 | "imagemin": "gulp imagemin", 16 | "webp": "gulp webp" 17 | }, 18 | "editorconfig-cli": [ 19 | "*.json", 20 | "*.js", 21 | "source/**/*.html", 22 | "source/**/*.pug", 23 | "source/js/**/*.js", 24 | "source/img/**/*.svg", 25 | "source/sass/**/*.{sass,scss}" 26 | ], 27 | "devDependencies": { 28 | "@babel/preset-env": "7.18.6", 29 | "babel-loader": "8.2.5", 30 | "@htmlacademy/editorconfig-cli": "1.x", 31 | "eslint": "7.32.0", 32 | "stylelint": "13.13.1", 33 | "autoprefixer": "10.4.7", 34 | "browser-sync": "2.27.10", 35 | "cross-env": "7.0.3", 36 | "del": "6.1.1", 37 | "gulp": "4.0.2", 38 | "gulp-csso": "4.0.1", 39 | "gulp-file-include": "2.3.0", 40 | "gulp-group-css-media-queries": "1.2.2", 41 | "gulp-html-beautify": "1.0.1", 42 | "gulp-imagemin": "7.1.0", 43 | "gulp-plumber": "1.2.1", 44 | "gulp-postcss": "9.0.1", 45 | "gulp-rename": "1.4.0", 46 | "gulp-sass": "5.1.0", 47 | "gulp-svgstore": "7.0.1", 48 | "gulp-webp": "4.0.1", 49 | "postcss": "8.4.14", 50 | "sass": "1.53.0", 51 | "webpack": "4.42.0", 52 | "webpack-stream": "5.2.1", 53 | "circular-dependency-plugin": "5.2.2", 54 | "clean-webpack-plugin": "3.0.0", 55 | "duplicate-package-checker-webpack-plugin": "3.0.0" 56 | }, 57 | "engines": { 58 | "node": ">=16.15.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/fonts/rouble.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlonelove/liga-basic-template/910bea7921f8f930076ec36812bf6392f6c54d04/source/fonts/rouble.woff -------------------------------------------------------------------------------- /source/fonts/rouble.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlonelove/liga-basic-template/910bea7921f8f930076ec36812bf6392f6c54d04/source/fonts/rouble.woff2 -------------------------------------------------------------------------------- /source/html/base/footer.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /source/html/base/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /source/html/base/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | @@if (!context.index) { 4 | 7 | } 8 | @@if (context.index) { 9 | 12 | } 13 | 14 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /source/html/base/modal.html: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /source/html/components.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @@include("source/html/base/head.html") 6 | Компоненты 7 | 8 | 52 | 53 | 54 | 55 | 56 |
57 | @@include("build/img/sprite_auto.svg") 58 |
59 | 60 |
61 | @@include("source/html/base/header.html") 62 | 63 |
64 |
65 | 66 | 67 | вернуться на разводящую 68 | 69 |

Компоненты, используемые в вёрстке

70 | 71 |
72 |

Название компонента или группы компонентов

73 | 74 |

Класс компонента \ модификаторы

75 |

При необходимости, подсказки \ предупреждение \ важная информация

76 |
Компонент
77 |
78 | 79 | 80 | 85 |
86 |
87 | 88 | @@include("source/html/base/footer.html") 89 |
90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /source/html/components/modal-feedback.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /source/html/components/modal-success.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /source/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @@include("source/html/base/head.html") 6 | ProjectName — Главная 7 | 8 | 9 | 10 | 11 |
12 | @@include("build/img/sprite_auto.svg") 13 |
14 | 15 |
16 | <-- для внутренних страниц используйте "headerActiveLinkIndex": 0\1\2 --> 17 | @@include("source/html/base/header.html", { 18 | "index": true 19 | }) 20 | 21 |
22 | 26 |
27 | 28 | @@include("source/html/base/footer.html") 29 | 30 | @@include("source/html/base/modal.html", { 31 | "name": "feedback" 32 | }) 33 | 34 | @@include("source/html/base/modal.html", { 35 | "name": "success", 36 | "fitContent": true, 37 | "noScale": true, 38 | "mod": "some-mod" 39 | }) 40 |
41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /source/html/sitemap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @@include("source/html/base/head.html") 6 | Список страниц 7 | 8 | 52 | 53 | 54 | 55 | 56 |
57 |
58 | Логотип проекта 59 |

Список страниц и компонентов

60 |
    61 |
  1. 62 | Главная — index.html 63 |
  2. 64 | 67 |
68 |
69 |
70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /source/img/bg/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlonelove/liga-basic-template/910bea7921f8f930076ec36812bf6392f6c54d04/source/img/bg/placeholder.jpg -------------------------------------------------------------------------------- /source/img/content/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlonelove/liga-basic-template/910bea7921f8f930076ec36812bf6392f6c54d04/source/img/content/placeholder.jpg -------------------------------------------------------------------------------- /source/img/slides/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htmlonelove/liga-basic-template/910bea7921f8f930076ec36812bf6392f6c54d04/source/img/slides/placeholder.jpg -------------------------------------------------------------------------------- /source/img/sprite/icon-close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/img/svg/not-needed-in-sprite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/js/main.js: -------------------------------------------------------------------------------- 1 | import {iosVhFix} from './utils/ios-vh-fix'; 2 | import {initModals} from './modules/modals/init-modals'; 3 | 4 | // --------------------------------- 5 | 6 | window.addEventListener('DOMContentLoaded', () => { 7 | 8 | // Utils 9 | // --------------------------------- 10 | 11 | iosVhFix(); 12 | 13 | // Modules 14 | // --------------------------------- 15 | 16 | // все скрипты должны быть в обработчике 'DOMContentLoaded', но не все в 'load' 17 | // в load следует добавить скрипты, не участвующие в работе первого экрана 18 | window.addEventListener('load', () => { 19 | initModals(); 20 | }); 21 | }); 22 | 23 | // --------------------------------- 24 | 25 | // ❗❗❗ обязательно установите плагины eslint, stylelint, editorconfig в редактор кода. 26 | 27 | // привязывайте js не на классы, а на дата атрибуты (data-validate) 28 | 29 | // вместо модификаторов .block--active используем утилитарные классы 30 | // .is-active || .is-open || .is-invalid и прочие (обязателен нейминг в два слова) 31 | // .select.select--opened ❌ ---> [data-select].is-open ✅ 32 | 33 | // выносим все в дата атрибуты 34 | // url до иконок пинов карты, настройки автопрокрутки слайдера, url к json и т.д. 35 | 36 | // для адаптивного JS используейтся matchMedia и addListener 37 | // const breakpoint = window.matchMedia(`(min-width:1024px)`); 38 | // const breakpointChecker = () => { 39 | // if (breakpoint.matches) { 40 | // } else { 41 | // } 42 | // }; 43 | // breakpoint.addListener(breakpointChecker); 44 | // breakpointChecker(); 45 | 46 | // используйте .closest(el) 47 | -------------------------------------------------------------------------------- /source/js/modules/modals/init-modals.js: -------------------------------------------------------------------------------- 1 | import {Modals} from './modals'; 2 | 3 | let modals; 4 | 5 | // Здесь реализован пример открытия модалки через колбэк закрытия 6 | // const openModalInCloseCallback = (name, context = this) => { 7 | // context._enableScrolling = false; 8 | // context._setSettings('default'); 9 | // modals.open(name); 10 | // }; 11 | 12 | // closeCallback() { 13 | // openModalInCloseCallback('modal-5'); 14 | // }, 15 | 16 | const settings = { 17 | 'default': { 18 | preventDefault: true, 19 | stopPlay: true, 20 | lockFocus: true, 21 | startFocus: true, 22 | focusBack: true, 23 | eventTimeout: 400, 24 | openCallback: false, 25 | closeCallback: false, 26 | }, 27 | }; 28 | 29 | const initModals = () => { 30 | const modalElements = document.querySelectorAll('.modal'); 31 | if (modalElements.length) { 32 | modalElements.forEach((el) => { 33 | setTimeout(() => { 34 | el.classList.remove('modal--preload'); 35 | }, 100); 36 | }); 37 | } 38 | 39 | modals = new Modals(settings); 40 | // Используйте в разработке экспортируемую переменную modals, window сделан для бэкэнда 41 | window.modals = modals; 42 | }; 43 | 44 | export {modals, initModals}; 45 | -------------------------------------------------------------------------------- /source/js/modules/modals/modals.js: -------------------------------------------------------------------------------- 1 | import {ScrollLock} from '../../utils/scroll-lock'; 2 | import {FocusLock} from '../../utils/focus-lock'; 3 | 4 | export class Modals { 5 | constructor(settings = {}) { 6 | this._scrollLock = new ScrollLock(); 7 | this._focusLock = new FocusLock(); 8 | 9 | this._modalOpenElements = document.querySelectorAll('[data-open-modal]'); 10 | this._openedModalElement = null; 11 | this._modalName = null; 12 | this._enableScrolling = true; 13 | this._settingKey = 'default'; 14 | 15 | this._settings = settings; 16 | this._preventDefault = this._settings[this._settingKey].preventDefault; 17 | this._stopPlay = this._settings[this._settingKey].stopPlay; 18 | this._lockFocus = this._settings[this._settingKey].lockFocus; 19 | this._startFocus = this._settings[this._settingKey].startFocus; 20 | this._focusBack = this._settings[this._settingKey].focusBack; 21 | this._eventTimeout = this._settings[this._settingKey].eventTimeout; 22 | this._openCallback = this._settings[this._settingKey].openCallback; 23 | this._closeCallback = this._settings[this._settingKey].closeCallback; 24 | 25 | this._documentKeydownHandler = this._documentKeydownHandler.bind(this); 26 | this._documentClickHandler = this._documentClickHandler.bind(this); 27 | this._modalClickHandler = this._modalClickHandler.bind(this); 28 | 29 | this._init(); 30 | } 31 | 32 | _init() { 33 | if (this._modalOpenElements.length) { 34 | document.addEventListener('click', this._documentClickHandler); 35 | } 36 | } 37 | 38 | _setSettings(settingKey = this._settingKey) { 39 | if (!this._settings[settingKey]) { 40 | return; 41 | } 42 | 43 | this._preventDefault = 44 | typeof this._settings[settingKey].preventDefault === 'boolean' 45 | ? this._settings[settingKey].preventDefault 46 | : this._settings[this._settingKey].preventDefault; 47 | this._stopPlay = 48 | typeof this._settings[settingKey].stopPlay === 'boolean' 49 | ? this._settings[settingKey].stopPlay 50 | : this._settings[this._settingKey].stopPlay; 51 | this._lockFocus = 52 | typeof this._settings[settingKey].lockFocus === 'boolean' 53 | ? this._settings[settingKey].lockFocus 54 | : this._settings[this._settingKey].lockFocus; 55 | this._startFocus = 56 | typeof this._settings[settingKey].startFocus === 'boolean' 57 | ? this._settings[settingKey].startFocus 58 | : this._settings[this._settingKey].startFocus; 59 | this._focusBack = 60 | typeof this._settings[settingKey].lockFocus === 'boolean' 61 | ? this._settings[settingKey].focusBack 62 | : this._settings[this._settingKey].focusBack; 63 | this._eventTimeout = 64 | typeof this._settings[settingKey].eventTimeout === 'number' 65 | ? this._settings[settingKey].eventTimeout 66 | : this._settings[this._settingKey].eventTimeout; 67 | this._openCallback = this._settings[settingKey].openCallback || this._settings[this._settingKey].openCallback; 68 | this._closeCallback = this._settings[settingKey].closeCallback || this._settings[this._settingKey].closeCallback; 69 | } 70 | 71 | _documentClickHandler(evt) { 72 | const target = evt.target; 73 | 74 | if (!target.closest('[data-open-modal]')) { 75 | return; 76 | } 77 | 78 | evt.preventDefault(); 79 | 80 | this._modalName = target.closest('[data-open-modal]').dataset.openModal; 81 | 82 | if (!this._modalName) { 83 | return; 84 | } 85 | 86 | this.open(); 87 | } 88 | 89 | _documentKeydownHandler(evt) { 90 | const isEscKey = evt.key === 'Escape' || evt.key === 'Esc'; 91 | 92 | if (isEscKey) { 93 | evt.preventDefault(); 94 | this.close(document.querySelector('.modal.is-active').dataset.modal); 95 | } 96 | } 97 | 98 | _modalClickHandler(evt) { 99 | const target = evt.target; 100 | 101 | if (!target.closest('[data-close-modal]')) { 102 | return; 103 | } 104 | 105 | this.close(target.closest('[data-modal]').dataset.modal); 106 | } 107 | 108 | _addListeners(modal) { 109 | modal.addEventListener('click', this._modalClickHandler); 110 | document.addEventListener('keydown', this._documentKeydownHandler); 111 | } 112 | 113 | _removeListeners(modal) { 114 | modal.removeEventListener('click', this._modalClickHandler); 115 | document.removeEventListener('keydown', this._documentKeydownHandler); 116 | } 117 | 118 | _stopInteractive(modal) { 119 | if (this._stopPlay) { 120 | modal.querySelectorAll('video, audio').forEach((el) => el.pause()); 121 | modal.querySelectorAll('[data-iframe]').forEach((el) => { 122 | el.querySelector('iframe').contentWindow.postMessage('{"event": "command", "func": "pauseVideo", "args": ""}', '*'); 123 | }); 124 | } 125 | } 126 | 127 | _autoPlay(modal) { 128 | modal.querySelectorAll('[data-iframe]').forEach((el) => { 129 | const autoPlay = el.closest('[data-auto-play]'); 130 | if (autoPlay) { 131 | el.querySelector('iframe').contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*'); 132 | } 133 | }); 134 | } 135 | 136 | open(modalName = this._modalName) { 137 | const modal = document.querySelector(`[data-modal="${modalName}"]`); 138 | 139 | if (!modal || modal.classList.contains('is-active')) { 140 | return; 141 | } 142 | 143 | document.removeEventListener('click', this._documentClickHandler); 144 | 145 | this._openedModalElement = document.querySelector('.modal.is-active'); 146 | 147 | if (this._openedModalElement) { 148 | this._enableScrolling = false; 149 | this.close(this._openedModalElement.dataset.modal); 150 | } 151 | 152 | this._setSettings(modalName); 153 | modal.classList.add('is-active'); 154 | 155 | if (!this._openedModalElement) { 156 | this._scrollLock.disableScrolling(); 157 | } 158 | 159 | if (this._openCallback) { 160 | this._openCallback(); 161 | } 162 | 163 | if (this._lockFocus) { 164 | this._focusLock.lock('.modal.is-active', this._startFocus); 165 | } 166 | 167 | setTimeout(() => { 168 | this._addListeners(modal); 169 | this._autoPlay(modal); 170 | document.addEventListener('click', this._documentClickHandler); 171 | }, this._eventTimeout); 172 | } 173 | 174 | close(modalName = this._modalName) { 175 | const modal = document.querySelector(`[data-modal="${modalName}"]`); 176 | document.removeEventListener('click', this._documentClickHandler); 177 | 178 | if (!modal || !modal.classList.contains('is-active')) { 179 | return; 180 | } 181 | 182 | if (this._lockFocus) { 183 | this._focusLock.unlock(this._focusBack); 184 | } 185 | 186 | modal.classList.remove('is-active'); 187 | this._removeListeners(modal); 188 | this._stopInteractive(modal); 189 | 190 | if (this._closeCallback) { 191 | this._closeCallback(); 192 | } 193 | 194 | if (this._enableScrolling) { 195 | setTimeout(() => { 196 | this._scrollLock.enableScrolling(); 197 | }, this._eventTimeout); 198 | } 199 | 200 | setTimeout(() => { 201 | document.addEventListener('click', this._documentClickHandler); 202 | }, this._eventTimeout); 203 | 204 | this._setSettings('default'); 205 | this._enableScrolling = true; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /source/js/utils/focus-lock.js: -------------------------------------------------------------------------------- 1 | const SELECTORS = [ 2 | 'a[href]', 3 | 'area[href]', 4 | 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 5 | 'select:not([disabled]):not([aria-hidden])', 6 | 'textarea:not([disabled]):not([aria-hidden])', 7 | 'button:not([disabled]):not([aria-hidden])', 8 | 'iframe', 9 | 'object', 10 | 'embed', 11 | '[contenteditable]', 12 | '[tabindex]:not([tabindex^="-"])' 13 | ]; 14 | 15 | export class FocusLock { 16 | constructor() { 17 | this._lockedSelector = null; 18 | this._focusableElements = null; 19 | this._endElement = null; 20 | this._selectors = SELECTORS; 21 | 22 | this._documentKeydownHandler = this._documentKeydownHandler.bind(this); 23 | } 24 | 25 | _documentKeydownHandler(evt) { 26 | const activeElement = document.activeElement; 27 | if (evt.key === 'Tab') { 28 | if (!this._focusableElements.length) { 29 | evt.preventDefault(); 30 | activeElement.blur(); 31 | return; 32 | } 33 | if (this._focusableElements.length === 1) { 34 | evt.preventDefault(); 35 | this._focusableElements[0].focus(); 36 | return; 37 | } 38 | if (this._focusableElements.length > 1 && !activeElement.closest(this._lockedSelector)) { 39 | evt.preventDefault(); 40 | this._focusableElements[0].focus(); 41 | return; 42 | } 43 | } 44 | if (evt.key === 'Tab' && !evt.shiftKey && activeElement === this._focusableElements[this._focusableElements.length - 1]) { 45 | evt.preventDefault(); 46 | this._focusableElements[0].focus(); 47 | } 48 | if (evt.key === 'Tab' && evt.shiftKey && activeElement === this._focusableElements[0]) { 49 | evt.preventDefault(); 50 | this._focusableElements[this._focusableElements.length - 1].focus(); 51 | } 52 | } 53 | 54 | lock(lockedSelector, startFocus = true) { 55 | this.unlock(); 56 | this._lockedSelector = lockedSelector; 57 | const lockedElement = document.querySelector(this._lockedSelector); 58 | if (!lockedElement) { 59 | return; 60 | } 61 | this._focusableElements = lockedElement.querySelectorAll(this._selectors); 62 | this._endElement = document.activeElement; 63 | const startElement = lockedElement.querySelector('[data-focus]') || this._focusableElements[0]; 64 | if (this._endElement) { 65 | this._endElement.blur(); 66 | } 67 | if (startElement && startFocus) { 68 | startElement.focus(); 69 | } 70 | document.addEventListener('keydown', this._documentKeydownHandler); 71 | } 72 | 73 | unlock(returnFocus = true) { 74 | if (this._endElement && returnFocus) { 75 | this._endElement.focus(); 76 | } 77 | this._lockedSelector = null; 78 | this._focusableElements = null; 79 | this._endElement = null; 80 | document.removeEventListener('keydown', this._documentKeydownHandler); 81 | } 82 | } 83 | 84 | window.focusLock = new FocusLock(); 85 | -------------------------------------------------------------------------------- /source/js/utils/ios-checker.js: -------------------------------------------------------------------------------- 1 | export const iosChecker = () => { 2 | return [ 3 | 'iPad Simulator', 4 | 'iPhone Simulator', 5 | 'iPod Simulator', 6 | 'iPad', 7 | 'iPhone', 8 | 'iPod' 9 | ].includes(navigator.platform) 10 | // iPad on iOS 13 detection 11 | || (navigator.userAgent.includes('Mac') && 'ontouchend' in document); 12 | }; 13 | -------------------------------------------------------------------------------- /source/js/utils/ios-vh-fix.js: -------------------------------------------------------------------------------- 1 | import {iosChecker} from './ios-checker'; 2 | 3 | const iosVhFix = () => { 4 | if (!(!!window.MSInputMethodContext && !!document.documentMode)) { 5 | if (iosChecker()) { 6 | let vh = window.innerHeight * 0.01; 7 | document.documentElement.style.setProperty('--vh', `${vh}px`); 8 | 9 | window.addEventListener('resize', function () { 10 | vh = window.innerHeight * 0.01; 11 | document.documentElement.style.setProperty('--vh', `${vh}px`); 12 | }); 13 | } 14 | } 15 | }; 16 | 17 | export {iosVhFix}; 18 | -------------------------------------------------------------------------------- /source/js/utils/scroll-lock.js: -------------------------------------------------------------------------------- 1 | import {iosChecker} from './ios-checker'; 2 | 3 | export class ScrollLock { 4 | constructor() { 5 | this._iosChecker = iosChecker; 6 | this._lockClass = this._iosChecker() ? 'scroll-lock-ios' : 'scroll-lock'; 7 | this._scrollTop = null; 8 | this._fixedBlockElements = document.querySelectorAll('[data-fix-block]'); 9 | } 10 | 11 | _getScrollbarWidth() { 12 | return window.innerWidth - document.documentElement.clientWidth; 13 | } 14 | 15 | _getBodyScrollTop() { 16 | return ( 17 | self.pageYOffset || 18 | (document.documentElement && document.documentElement.ScrollTop) || 19 | (document.body && document.body.scrollTop) 20 | ); 21 | } 22 | 23 | disableScrolling() { 24 | this._scrollTop = document.body.dataset.scroll = document.body.dataset.scroll ? document.body.dataset.scroll : this._getBodyScrollTop(); 25 | if (this._getScrollbarWidth()) { 26 | document.body.style.paddingRight = `${this._getScrollbarWidth()}px`; 27 | this._fixedBlockElements.forEach((block) => { 28 | block.style.paddingRight = `${this._getScrollbarWidth()}px`; 29 | }); 30 | } 31 | document.body.style.top = `-${this._scrollTop}px`; 32 | document.body.classList.add(this._lockClass); 33 | } 34 | 35 | enableScrolling() { 36 | document.body.classList.remove(this._lockClass); 37 | window.scrollTo(0, +document.body.dataset.scroll); 38 | document.body.style.paddingRight = null; 39 | document.body.style.top = null; 40 | this._fixedBlockElements.forEach((block) => { 41 | block.style.paddingRight = null; 42 | }); 43 | document.body.removeAttribute('data-scroll'); 44 | this._scrollTop = null; 45 | } 46 | } 47 | 48 | window.scrollLock = new ScrollLock(); 49 | -------------------------------------------------------------------------------- /source/js/vendor.js: -------------------------------------------------------------------------------- 1 | // Swiper 7.4.1 2 | // import './vendor/swiper'; 3 | import './vendor/focus-visible-polyfill'; 4 | -------------------------------------------------------------------------------- /source/js/vendor/focus-visible-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Applies the :focus-visible polyfill at the given scope. 3 | * A scope in this case is either the top-level Document or a Shadow Root. 4 | * 5 | * @param {(Document|ShadowRoot)} scope 6 | * @see https://github.com/WICG/focus-visible 7 | */ 8 | function applyFocusVisiblePolyfill(scope) { 9 | var hadKeyboardEvent = true; 10 | var hadFocusVisibleRecently = false; 11 | var hadFocusVisibleRecentlyTimeout = null; 12 | 13 | var inputTypesAllowlist = { 14 | text: true, 15 | search: true, 16 | url: true, 17 | tel: true, 18 | email: true, 19 | password: true, 20 | number: true, 21 | date: true, 22 | month: true, 23 | week: true, 24 | time: true, 25 | datetime: true, 26 | 'datetime-local': true, 27 | }; 28 | 29 | /** 30 | * Helper function for legacy browsers and iframes which sometimes focus 31 | * elements like document, body, and non-interactive SVG. 32 | * @param {Element} el 33 | */ 34 | function isValidFocusTarget(el) { 35 | if ( 36 | el && 37 | el !== document && 38 | el.nodeName !== 'HTML' && 39 | el.nodeName !== 'BODY' && 40 | 'classList' in el && 41 | 'contains' in el.classList 42 | ) { 43 | return true; 44 | } 45 | return false; 46 | } 47 | 48 | /** 49 | * Computes whether the given element should automatically trigger the 50 | * `focus-visible` class being added, i.e. whether it should always match 51 | * `:focus-visible` when focused. 52 | * @param {Element} el 53 | * @return {boolean} 54 | */ 55 | function focusTriggersKeyboardModality(el) { 56 | var type = el.type; 57 | var tagName = el.tagName; 58 | 59 | if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) { 60 | return true; 61 | } 62 | 63 | if (tagName === 'TEXTAREA' && !el.readOnly) { 64 | return true; 65 | } 66 | 67 | if (el.isContentEditable) { 68 | return true; 69 | } 70 | 71 | return false; 72 | } 73 | 74 | /** 75 | * Add the `focus-visible` class to the given element if it was not added by 76 | * the author. 77 | * @param {Element} el 78 | */ 79 | function addFocusVisibleClass(el) { 80 | if (el.classList.contains('focus-visible')) { 81 | return; 82 | } 83 | el.classList.add('focus-visible'); 84 | el.setAttribute('data-focus-visible-added', ''); 85 | } 86 | 87 | /** 88 | * Remove the `focus-visible` class from the given element if it was not 89 | * originally added by the author. 90 | * @param {Element} el 91 | */ 92 | function removeFocusVisibleClass(el) { 93 | if (!el.hasAttribute('data-focus-visible-added')) { 94 | return; 95 | } 96 | el.classList.remove('focus-visible'); 97 | el.removeAttribute('data-focus-visible-added'); 98 | } 99 | 100 | /** 101 | * If the most recent user interaction was via the keyboard; 102 | * and the key press did not include a meta, alt/option, or control key; 103 | * then the modality is keyboard. Otherwise, the modality is not keyboard. 104 | * Apply `focus-visible` to any current active element and keep track 105 | * of our keyboard modality state with `hadKeyboardEvent`. 106 | * @param {KeyboardEvent} e 107 | */ 108 | function onKeyDown(e) { 109 | if (e.metaKey || e.altKey || e.ctrlKey) { 110 | return; 111 | } 112 | 113 | if (isValidFocusTarget(scope.activeElement)) { 114 | addFocusVisibleClass(scope.activeElement); 115 | } 116 | 117 | hadKeyboardEvent = true; 118 | } 119 | 120 | /** 121 | * If at any point a user clicks with a pointing device, ensure that we change 122 | * the modality away from keyboard. 123 | * This avoids the situation where a user presses a key on an already focused 124 | * element, and then clicks on a different element, focusing it with a 125 | * pointing device, while we still think we're in keyboard modality. 126 | * @param {Event} e 127 | */ 128 | function onPointerDown(e) { 129 | hadKeyboardEvent = false; 130 | } 131 | 132 | /** 133 | * On `focus`, add the `focus-visible` class to the target if: 134 | * - the target received focus as a result of keyboard navigation, or 135 | * - the event target is an element that will likely require interaction 136 | * via the keyboard (e.g. a text box) 137 | * @param {Event} e 138 | */ 139 | function onFocus(e) { 140 | // Prevent IE from focusing the document or HTML element. 141 | if (!isValidFocusTarget(e.target)) { 142 | return; 143 | } 144 | 145 | if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) { 146 | addFocusVisibleClass(e.target); 147 | } 148 | } 149 | 150 | /** 151 | * On `blur`, remove the `focus-visible` class from the target. 152 | * @param {Event} e 153 | */ 154 | function onBlur(e) { 155 | if (!isValidFocusTarget(e.target)) { 156 | return; 157 | } 158 | 159 | if (e.target.classList.contains('focus-visible') || e.target.hasAttribute('data-focus-visible-added')) { 160 | // To detect a tab/window switch, we look for a blur event followed 161 | // rapidly by a visibility change. 162 | // If we don't see a visibility change within 100ms, it's probably a 163 | // regular focus change. 164 | hadFocusVisibleRecently = true; 165 | window.clearTimeout(hadFocusVisibleRecentlyTimeout); 166 | hadFocusVisibleRecentlyTimeout = window.setTimeout(function () { 167 | hadFocusVisibleRecently = false; 168 | }, 100); 169 | removeFocusVisibleClass(e.target); 170 | } 171 | } 172 | 173 | /** 174 | * If the user changes tabs, keep track of whether or not the previously 175 | * focused element had .focus-visible. 176 | * @param {Event} e 177 | */ 178 | function onVisibilityChange(e) { 179 | if (document.visibilityState === 'hidden') { 180 | // If the tab becomes active again, the browser will handle calling focus 181 | // on the element (Safari actually calls it twice). 182 | // If this tab change caused a blur on an element with focus-visible, 183 | // re-apply the class when the user switches back to the tab. 184 | if (hadFocusVisibleRecently) { 185 | hadKeyboardEvent = true; 186 | } 187 | addInitialPointerMoveListeners(); 188 | } 189 | } 190 | 191 | /** 192 | * Add a group of listeners to detect usage of any pointing devices. 193 | * These listeners will be added when the polyfill first loads, and anytime 194 | * the window is blurred, so that they are active when the window regains 195 | * focus. 196 | */ 197 | function addInitialPointerMoveListeners() { 198 | document.addEventListener('mousemove', onInitialPointerMove); 199 | document.addEventListener('mousedown', onInitialPointerMove); 200 | document.addEventListener('mouseup', onInitialPointerMove); 201 | document.addEventListener('pointermove', onInitialPointerMove); 202 | document.addEventListener('pointerdown', onInitialPointerMove); 203 | document.addEventListener('pointerup', onInitialPointerMove); 204 | document.addEventListener('touchmove', onInitialPointerMove); 205 | document.addEventListener('touchstart', onInitialPointerMove); 206 | document.addEventListener('touchend', onInitialPointerMove); 207 | } 208 | 209 | function removeInitialPointerMoveListeners() { 210 | document.removeEventListener('mousemove', onInitialPointerMove); 211 | document.removeEventListener('mousedown', onInitialPointerMove); 212 | document.removeEventListener('mouseup', onInitialPointerMove); 213 | document.removeEventListener('pointermove', onInitialPointerMove); 214 | document.removeEventListener('pointerdown', onInitialPointerMove); 215 | document.removeEventListener('pointerup', onInitialPointerMove); 216 | document.removeEventListener('touchmove', onInitialPointerMove); 217 | document.removeEventListener('touchstart', onInitialPointerMove); 218 | document.removeEventListener('touchend', onInitialPointerMove); 219 | } 220 | 221 | /** 222 | * When the polfyill first loads, assume the user is in keyboard modality. 223 | * If any event is received from a pointing device (e.g. mouse, pointer, 224 | * touch), turn off keyboard modality. 225 | * This accounts for situations where focus enters the page from the URL bar. 226 | * @param {Event} e 227 | */ 228 | function onInitialPointerMove(e) { 229 | // Work around a Safari quirk that fires a mousemove on whenever the 230 | // window blurs, even if you're tabbing out of the page. ¯\_(ツ)_/¯ 231 | if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') { 232 | return; 233 | } 234 | 235 | hadKeyboardEvent = false; 236 | removeInitialPointerMoveListeners(); 237 | } 238 | 239 | // For some kinds of state, we are interested in changes at the global scope 240 | // only. For example, global pointer input, global key presses and global 241 | // visibility change should affect the state at every scope: 242 | document.addEventListener('keydown', onKeyDown, true); 243 | document.addEventListener('mousedown', onPointerDown, true); 244 | document.addEventListener('pointerdown', onPointerDown, true); 245 | document.addEventListener('touchstart', onPointerDown, true); 246 | document.addEventListener('visibilitychange', onVisibilityChange, true); 247 | 248 | addInitialPointerMoveListeners(); 249 | 250 | // For focus and blur, we specifically care about state changes in the local 251 | // scope. This is because focus / blur events that originate from within a 252 | // shadow root are not re-dispatched from the host element if it was already 253 | // the active element in its own scope: 254 | scope.addEventListener('focus', onFocus, true); 255 | scope.addEventListener('blur', onBlur, true); 256 | 257 | // We detect that a node is a ShadowRoot by ensuring that it is a 258 | // DocumentFragment and also has a host property. This check covers native 259 | // implementation and polyfill implementation transparently. If we only cared 260 | // about the native implementation, we could just check if the scope was 261 | // an instance of a ShadowRoot. 262 | if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) { 263 | // Since a ShadowRoot is a special kind of DocumentFragment, it does not 264 | // have a root element to add a class to. So, we add this attribute to the 265 | // host element instead: 266 | scope.host.setAttribute('data-js-focus-visible', ''); 267 | } else if (scope.nodeType === Node.DOCUMENT_NODE) { 268 | document.documentElement.classList.add('js-focus-visible'); 269 | document.documentElement.setAttribute('data-js-focus-visible', ''); 270 | } 271 | } 272 | 273 | // It is important to wrap all references to global window and document in 274 | // these checks to support server-side rendering use cases 275 | // @see https://github.com/WICG/focus-visible/issues/199 276 | if (typeof window !== 'undefined' && typeof document !== 'undefined') { 277 | // Make the polyfill helper globally available. This can be used as a signal 278 | // to interested libraries that wish to coordinate with the polyfill for e.g., 279 | // applying the polyfill to a shadow root: 280 | window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill; 281 | 282 | // Notify interested libraries of the polyfill's presence, in case the 283 | // polyfill was loaded lazily: 284 | var event; 285 | 286 | try { 287 | event = new CustomEvent('focus-visible-polyfill-ready'); 288 | } catch (error) { 289 | // IE11 does not support using CustomEvent as a constructor directly: 290 | event = document.createEvent('CustomEvent'); 291 | event.initCustomEvent('focus-visible-polyfill-ready', false, false, {}); 292 | } 293 | 294 | window.dispatchEvent(event); 295 | } 296 | 297 | if (typeof document !== 'undefined') { 298 | // Apply the polyfill to the global document, so that no JavaScript 299 | // coordination is required to use the polyfill in the top-level document: 300 | applyFocusVisiblePolyfill(document); 301 | } 302 | -------------------------------------------------------------------------------- /source/sass/blocks/btn.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | display: inline-flex; 3 | padding: 10px 20px; 4 | 5 | color: $color-default-white; 6 | 7 | background-color: $color-neon-blue; 8 | border: none; 9 | cursor: pointer; 10 | 11 | transition: opacity $trans-default; 12 | 13 | &--red { 14 | background-color: $color-torch-red; 15 | } 16 | 17 | &:disabled { 18 | opacity: 0.5; 19 | 20 | pointer-events: none; 21 | } 22 | 23 | @include hover-focus { 24 | opacity: 0.8; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /source/sass/blocks/footer.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 40px 0; 3 | 4 | background-color: $color-neon-blue; 5 | } 6 | -------------------------------------------------------------------------------- /source/sass/blocks/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 28px 0; 3 | 4 | background-color: $color-neon-blue; 5 | 6 | .container { 7 | display: flex; 8 | } 9 | 10 | &__logo { 11 | color: $color-default-white; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /source/sass/blocks/main-nav.scss: -------------------------------------------------------------------------------- 1 | .main-nav { 2 | &__list { 3 | display: flex; 4 | margin: 0; 5 | margin-right: 20px; 6 | margin-left: 60px; 7 | padding: 0; 8 | 9 | list-style: none; 10 | } 11 | 12 | &__item { 13 | margin-right: 20px; 14 | } 15 | 16 | &__link { 17 | color: $color-default-white; 18 | 19 | &.is-active { 20 | opacity: 0.6; 21 | } 22 | } 23 | 24 | &__toggle { 25 | display: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /source/sass/blocks/modal.scss: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | z-index: 1000; 8 | 9 | display: flex; 10 | flex-direction: column; 11 | overflow-y: auto; 12 | -webkit-overflow-scrolling: touch; 13 | 14 | background-color: rgba($color-default-black, 0.8); 15 | transform: scale(1.2); 16 | visibility: hidden; 17 | opacity: 0; 18 | 19 | transition: opacity $trans-modal, 20 | transform $trans-modal, 21 | visibility $trans-modal; 22 | 23 | pointer-events: none; 24 | 25 | -ms-overflow-style: none; 26 | scrollbar-width: none; 27 | 28 | &::-webkit-scrollbar { 29 | display: none; 30 | } 31 | 32 | &__wrapper { 33 | position: relative; 34 | 35 | display: flex; 36 | flex-shrink: 0; 37 | justify-content: center; 38 | align-items: center; 39 | width: 100%; 40 | min-height: 100%; 41 | padding: 60px 40px; 42 | 43 | @include vp-767 { 44 | padding: 60px 16px; 45 | } 46 | } 47 | 48 | &__overlay { 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | 53 | width: 100%; 54 | height: 100%; 55 | 56 | cursor: pointer; 57 | } 58 | 59 | &__content { 60 | position: relative; 61 | 62 | width: 600px; 63 | padding: 40px; 64 | 65 | background-color: $color-default-white; 66 | 67 | @include vp-767 { 68 | width: 100%; 69 | padding: 20px; 70 | } 71 | } 72 | 73 | &__close-btn { 74 | position: absolute; 75 | top: 22px; 76 | right: 22px; 77 | z-index: 1; 78 | 79 | width: 40px; 80 | height: 40px; 81 | padding: 0; 82 | 83 | background: $color-transparent; 84 | border: none; 85 | cursor: pointer; 86 | } 87 | 88 | &--responsive { 89 | .modal__content { 90 | width: 100%; 91 | } 92 | } 93 | 94 | &--fit-content { 95 | .modal__content { 96 | width: auto; 97 | 98 | @include vp-767 { 99 | width: 100%; 100 | } 101 | } 102 | } 103 | 104 | &--no-scale { 105 | transform: none; 106 | } 107 | 108 | &--preload { 109 | transition: none; 110 | } 111 | 112 | &.is-active { 113 | transform: scale(1); 114 | visibility: visible; 115 | opacity: 1; 116 | 117 | pointer-events: auto; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /source/sass/functions.scss: -------------------------------------------------------------------------------- 1 | // --------------------------------- 2 | // ❗ использование скейлинга должно быть согласовано с командой и заказчиком 3 | 4 | // 1) ремы в плюс 5 | 6 | // functions.scss 7 | // @function size($size-in-px) { 8 | // @return calc($size-in-px / $fz-default * 1rem); 9 | // } 10 | 11 | // @function size-desktop($size-in-px) { 12 | // @return calc($size-in-px / $vp-1920 * 100vw); 13 | // } 14 | 15 | // reboot.scss 16 | // html { 17 | // font-size: size-desktop($fz-default); 18 | 19 | // @include vp-1919 { 20 | // font-size: $fz-default; 21 | // } 22 | // } 23 | 24 | // --------------------------------- 25 | // 2) ремы в плюс от эталона и в минус, на планшете обычная верстка 26 | 27 | // functions.scss 28 | // @function size($size-in-px) { 29 | // @return calc($size-in-px / $fz-default * 1rem; 30 | // } 31 | 32 | // @function size-desktop($size-in-px) { 33 | // @return calc($size-in-px / $vp-1920 * 100vw; 34 | // } 35 | 36 | // reboot.scss 37 | // html { 38 | // font-size: size-desktop($fz-default); 39 | 40 | // @include vp-1023 { 41 | // font-size: $fz-default; 42 | // } 43 | // } 44 | 45 | // --------------------------------- 46 | // 3) ремы в плюс от эталона и в минус (с другим коэффициентом), на планшете обычная верстка 47 | // functions.scss 48 | // @function size($size-in-px) { 49 | // @return calc($size-in-px / $fz-default * 1rem); 50 | // } 51 | 52 | // @function size-desktop($size-in-px) { 53 | // @return calc($size-in-px / $vp-1920 * 100vw); 54 | // } 55 | 56 | // @function size-tablet($size-in-px) { 57 | // @return calc($size-in-px / $vp-1440 * 100vw); 58 | // } 59 | 60 | // reboot.scss 61 | // html { 62 | // font-size: size-desktop($fz-default); 63 | 64 | // @include vp-1439 { 65 | // font-size: size-tablet($fz-default); 66 | // } 67 | 68 | // @include vp-1023 { 69 | // font-size: $fz-default; 70 | // } 71 | // } 72 | 73 | // --------------------------------- 74 | // 4) ремы в коэффициентах 75 | // functions.scss 76 | // @function size($size-in-px) { 77 | // @return calc($size-in-px / $fz-default * 1rem); 78 | // } 79 | 80 | // @function size-coefficient($size-in-px) { 81 | // @return calc($size-in-px * 0.9); 82 | // } 83 | 84 | // @function size-coefficient-large($size-in-px) { 85 | // @return calc($size-in-px * 1.3); 86 | // } 87 | 88 | // reboot.scss 89 | // html { 90 | // font-size: $fz-default; 91 | 92 | // @media (min-width: 1921px) { 93 | // font-size: size-coefficient-large($fz-default); 94 | // } 95 | 96 | // @include vp-1439 { 97 | // font-size: size-coefficient($fz-default); 98 | // } 99 | 100 | // @include vp-1023 { 101 | // font-size: $fz-default; 102 | // } 103 | // } 104 | 105 | // --------------------------------- 106 | // 5) скейлинг отдельных элементов 107 | // functions.scss 108 | // @function size($size-in-px) { 109 | // @return calc($size-in-px / $fz-default * 1rem); 110 | // } 111 | 112 | // @function size-desktop($size-in-px) { 113 | // @return calc($size-in-px / $vp-1920 * 100vw); 114 | // } 115 | 116 | // @function no-scale($size-in-px) { 117 | // @return calc($size-in-px * (1921px / 1920px)); 118 | // } 119 | 120 | // reboot.scss 121 | // html { 122 | // font-size: size-desktop($fz-default); 123 | 124 | // @include vp-1439 { 125 | // font-size: $fz-default; 126 | // } 127 | // } 128 | 129 | // .block { 130 | // margin-bottom: size(40px); 131 | 132 | // @media (min-width: 1921px) { 133 | // margin-bottom: no-scale(40px); 134 | // } 135 | // } 136 | -------------------------------------------------------------------------------- /source/sass/global/container.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | max-width: 1920px; 4 | margin: 0 auto; 5 | padding: 0 40px; 6 | 7 | @include vp-1439 { 8 | padding: 0 32px; 9 | } 10 | 11 | @include vp-1023 { 12 | padding: 0 24px; 13 | } 14 | 15 | @include vp-767 { 16 | padding: 0 16px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /source/sass/global/fonts.scss: -------------------------------------------------------------------------------- 1 | // @font-face { 2 | // font-style: normal; 3 | // font-weight: 400; 4 | // font-family: "Rouble"; 5 | 6 | // font-display: swap; 7 | // src: 8 | // url("../fonts/rouble.woff2") format("woff2"), 9 | // url("../fonts/rouble.woff") format("woff"); 10 | // } 11 | 12 | @font-face { 13 | font-style: normal; 14 | font-weight: 400; 15 | font-family: ""; 16 | 17 | font-display: swap; 18 | src: 19 | url("../fonts/.woff2") format("woff2"), 20 | url("../fonts/.woff") format("woff"); 21 | } 22 | -------------------------------------------------------------------------------- /source/sass/global/reboot.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | margin: 0; 10 | padding: 0; 11 | min-height: 100vh; 12 | } 13 | 14 | html { 15 | font-style: normal; 16 | font-weight: 400; 17 | font-size: $fz-default; 18 | line-height: 24px; 19 | font-family: $ff-placeholder; 20 | color: $color-default-black; 21 | 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | // оптимизация выравнивания шрифта относительно центра строки 25 | text-rendering: optimizeLegibility; 26 | // если по прежнему есть проблемы с выравниванием 27 | // https://transfonter.org/ - включите настройку https://prnt.sc/12rnt6g и переконвертируйте шрифт 28 | } 29 | 30 | body { 31 | width: 100%; 32 | height: 100%; 33 | 34 | background-color: $color-default-white; 35 | } 36 | 37 | a { 38 | color: $color-default-black; 39 | text-decoration: none; 40 | } 41 | 42 | img, 43 | video { 44 | display: block; 45 | max-width: 100%; 46 | height: auto; 47 | } 48 | 49 | textarea { 50 | resize: none; 51 | } 52 | 53 | // chrome autofill background removal 54 | // если на проекте у инпутов используются разные цвета фонов\текста - 55 | // удалите это из глобала и используйте локально с нужными цветами 56 | // rgba не подойдет, сконвертируйте цвет в hex без прозрачности 57 | // если в стилях уже используется box-shadow есть другое решение - 58 | // задать к списку транзишенов `background-color 10000000s ease-out` 59 | input:-webkit-autofill { 60 | box-shadow: inset 0 0 0 1000px $color-default-white; 61 | 62 | -webkit-text-fill-color: $color-default-black; 63 | } 64 | 65 | // firefox placeholder \ invalid fix + ios bdrs 66 | input, 67 | textarea { 68 | border-radius: 0; 69 | 70 | &::placeholder { 71 | opacity: 1; 72 | } 73 | 74 | &:invalid { 75 | box-shadow: none; 76 | } 77 | } 78 | 79 | select { 80 | border-radius: 0; 81 | } 82 | 83 | // ie11 X removal 84 | input { 85 | &::-ms-clear, 86 | &::-ms-reveal { 87 | display: none; 88 | } 89 | } 90 | 91 | // chrome search X removal 92 | input[type="search"]::-webkit-search-decoration, 93 | input[type="search"]::-webkit-search-cancel-button, 94 | input[type="search"]::-webkit-search-results-button, 95 | input[type="search"]::-webkit-search-results-decoration { 96 | appearance: none; 97 | } 98 | 99 | // input[number] arrows removal 100 | input::-webkit-outer-spin-button, 101 | input::-webkit-inner-spin-button { 102 | margin: 0; 103 | 104 | appearance: none; 105 | } 106 | 107 | input[type="number"] { 108 | appearance: textfield; 109 | } 110 | 111 | // ios button \ inputs reset 112 | select, 113 | textarea, 114 | input:matches([type="email"], 115 | [type="number"], 116 | [type="password"], 117 | [type="search"], 118 | [type="tel"], 119 | [type="text"], 120 | [type="url"]) { 121 | appearance: none; 122 | } 123 | 124 | button, 125 | [type="button"], 126 | [type="reset"], 127 | [type="submit"] { 128 | appearance: none; 129 | } 130 | -------------------------------------------------------------------------------- /source/sass/global/utils.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: relative; 3 | 4 | display: flex; 5 | flex-direction: column; 6 | min-height: 100vh; 7 | /* stylelint-disable-next-line */ 8 | min-height: calc(100 * var(--vh, 1vh)); 9 | 10 | .header, 11 | .footer { 12 | flex-shrink: 0; 13 | } 14 | 15 | main { 16 | flex-grow: 1; 17 | } 18 | } 19 | 20 | .rouble { 21 | font-family: $ff-rouble; 22 | /* stylelint-disable */ 23 | text-transform: lowercase !important; 24 | font-weight: 400 !important; 25 | /* stylelint-enable */ 26 | } 27 | 28 | .visually-hidden { 29 | position: absolute; 30 | 31 | width: 1px; 32 | height: 1px; 33 | margin: -1px; 34 | padding: 0; 35 | overflow: hidden; 36 | 37 | white-space: nowrap; 38 | 39 | border: 0; 40 | 41 | clip: rect(0 0 0 0); 42 | clip-path: inset(100%); 43 | } 44 | 45 | .scroll-lock-ios { 46 | position: fixed; 47 | 48 | overflow: hidden; 49 | } 50 | 51 | .scroll-lock { 52 | overflow: hidden; 53 | } 54 | 55 | .no-scrollbar { 56 | -ms-overflow-style: none; 57 | scrollbar-width: none; 58 | 59 | &::-webkit-scrollbar { 60 | display: none; 61 | } 62 | } 63 | 64 | .no-transition { 65 | /* stylelint-disable-next-line */ 66 | transition: none !important; 67 | } 68 | -------------------------------------------------------------------------------- /source/sass/keyframes.scss: -------------------------------------------------------------------------------- 1 | // используйте для скрытия нежелательных анимаций-скачков при поворотах экрана 2 | @keyframes disableBreakpointAnimation { 3 | 0% { 4 | visibility: hidden; 5 | opacity: 0; 6 | } 7 | 8 | 100% { 9 | visibility: hidden; 10 | opacity: 0; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /source/sass/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin retina { 2 | @media (min-resolution: $retina-dpi), (min-resolution: $retina-dppx) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin hover { 8 | @media (hover: hover) { 9 | &:hover:not(.focus-visible) { 10 | @content; 11 | } 12 | } 13 | } 14 | 15 | @mixin focus { 16 | &.focus-visible:focus { 17 | @content; 18 | } 19 | } 20 | 21 | @mixin active { 22 | &.focus-visible:active { 23 | @content; 24 | } 25 | } 26 | 27 | @mixin hover-focus { 28 | @media (hover: hover) { 29 | &:hover:not(.focus-visible) { 30 | @content; 31 | } 32 | } 33 | 34 | &.focus-visible:focus { 35 | @content; 36 | } 37 | } 38 | 39 | // Desktop first 40 | 41 | @mixin vp-1919 { 42 | @media (max-width: ($vp-1920 - 1px)) { 43 | @content; 44 | } 45 | } 46 | 47 | @mixin vp-1439 { 48 | @media (max-width: ($vp-1440 - 1px)) { 49 | @content; 50 | } 51 | } 52 | 53 | @mixin vp-1279 { 54 | @media (max-width: ($vp-1280 - 1px)) { 55 | @content; 56 | } 57 | } 58 | 59 | @mixin vp-1023 { 60 | @media (max-width: ($vp-1024 - 1px)) { 61 | @content; 62 | } 63 | } 64 | 65 | @mixin vp-767 { 66 | @media (max-width: ($vp-768 - 1px)) { 67 | @content; 68 | } 69 | } 70 | 71 | @mixin vp-374 { 72 | @media (max-width: ($vp-375 - 1px)) { 73 | @content; 74 | } 75 | } 76 | 77 | // Mobile first 78 | 79 | // @mixin vp-375 { 80 | // @media (min-width: $vp-375) { 81 | // @content; 82 | // } 83 | // } 84 | 85 | // @mixin vp-768 { 86 | // @media (min-width: $vp-768) { 87 | // @content; 88 | // } 89 | // } 90 | 91 | // @mixin vp-1024 { 92 | // @media (min-width: $vp-1024) { 93 | // @content; 94 | // } 95 | // } 96 | 97 | // @mixin vp-1280 { 98 | // @media (min-width: $vp-1280) { 99 | // @content; 100 | // } 101 | // } 102 | 103 | // @mixin vp-1440 { 104 | // @media (min-width: $vp-1440) { 105 | // @content; 106 | // } 107 | // } 108 | 109 | // @mixin vp-1920 { 110 | // @media (min-width: $vp-1920) { 111 | // @content; 112 | // } 113 | // } 114 | 115 | // --------------------------------- 116 | 117 | // не нужно добавлять свои миксины для ресета списков, обнуления кнопок и прочие 118 | // этим вы усложняете работу команды, проверку вашего кода и будущую поддержку проекта 119 | -------------------------------------------------------------------------------- /source/sass/style.scss: -------------------------------------------------------------------------------- 1 | // Vendor 2 | // --------------------------------- 3 | 4 | @import "vendor/normalize"; 5 | // Swiper 7.4.1 6 | // @import "vendor/swiper"; 7 | 8 | // Global 9 | // --------------------------------- 10 | 11 | @import "variables"; 12 | @import "mixins"; 13 | @import "functions"; 14 | @import "global/fonts"; 15 | @import "global/reboot"; 16 | @import "global/utils"; 17 | @import "global/container"; 18 | 19 | // Blocks 20 | // --------------------------------- 21 | 22 | @import "blocks/header"; 23 | @import "blocks/main-nav"; 24 | @import "blocks/footer"; 25 | @import "blocks/modal"; 26 | @import "blocks/btn"; 27 | 28 | // --------------------------------- 29 | 30 | // ❗❗❗ запрещено использовать изображения в css, касается как jpg \ png, так и svg 31 | 32 | // ❗ обязательно используйте mixin hover-focus для отключения ховеров на тач устройствах 33 | /* @mixin hover-focus { 34 | @media (hover: hover) { 35 | &:hover:not(.focus-visible) { 36 | @content; 37 | } 38 | } 39 | 40 | &.focus-visible:focus { 41 | @content; 42 | } 43 | } 44 | */ 45 | /* @include hover-focus { 46 | opacity: 0.8; 47 | } 48 | */ 49 | // но не используем для текстовых полей ( input, textarea ) 50 | // так же в сборке есть отдельный миксин для hover 51 | /*@mixin hover { 52 | @media (hover: hover) { 53 | &:hover:not(.focus-visible) { 54 | @content; 55 | } 56 | } 57 | } 58 | */ 59 | // для focus 60 | /*@mixin focus { 61 | &.focus-visible:focus { 62 | @content; 63 | } 64 | } 65 | */ 66 | // и для active 67 | /*@mixin active { 68 | &.focus-visible:active { 69 | @content; 70 | } 71 | } 72 | */ 73 | // адаптив пишем внутри каждого класса (смотрим container.scss) 74 | 75 | // для фикса проблем с vh на iOS в сборке подключен скрипт 76 | // используя vh на проекте задавайте их также как в примере в utils.scss 77 | 78 | // для любых transition обязательно указывайте transition-property 79 | // transition: $trans-default ❌ ---> transition: color $trans-default ✅ 80 | -------------------------------------------------------------------------------- /source/sass/variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | // --------------------------------- 3 | 4 | // Default 5 | 6 | $color-default-black: #000000; 7 | $color-default-white: #ffffff; 8 | $color-transparent: rgba(255, 255, 255, 0); 9 | 10 | // Project palette 11 | // для нейминга цветов используем https://www.htmlcsscolor.com/hex/334482 12 | 13 | $color-neon-blue: #2c39f2; 14 | $color-torch-red: #ff1553; 15 | 16 | // Gradient 17 | 18 | $black-to-right: linear-gradient(90deg, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0) 75%); 19 | 20 | // Typography 21 | // --------------------------------- 22 | 23 | // переменная используемая в html для подключения скейлинга 24 | $fz-default: 16px; 25 | 26 | // у некоторых шрифтов в ios возникают проблемы с символом рубля 27 | // https://www.artlebedev.ru/kovodstvo/sections/159/#13 28 | $ff-rouble: "Rouble", "Arial", sans-serif; 29 | 30 | $ff-placeholder: "Placeholder", "Arial", sans-serif; 31 | 32 | // Animation 33 | // --------------------------------- 34 | 35 | $tf-default: ease; 36 | $trans-default: 0.3s $tf-default; 37 | 38 | // если требуется изинг отличный от $tf-default, то переменную называем не цифрой 39 | $trans-modal: 0.6s cubic-bezier(0.55, 0, 0.1, 1); 40 | 41 | $trans-600: 0.6s $tf-default; 42 | 43 | // Viewports 44 | // --------------------------------- 45 | 46 | $vp-320: 320px; 47 | $vp-375: 375px; 48 | $vp-768: 768px; 49 | $vp-1024: 1024px; 50 | $vp-1280: 1280px; 51 | $vp-1440: 1440px; 52 | $vp-1920: 1920px; 53 | 54 | // Retina 55 | // --------------------------------- 56 | 57 | $retina-dpi: 144dpi; 58 | $retina-dppx: 1.5dppx; 59 | -------------------------------------------------------------------------------- /source/sass/vendor/normalize.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 3 | 4 | /* Document 5 | ========================================================================== */ 6 | 7 | /** 8 | * 1. Correct the line height in all browsers. 9 | * 2. Prevent adjustments of font size after orientation changes in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers. 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Render the `main` element consistently in IE. 31 | */ 32 | 33 | main { 34 | display: block; 35 | } 36 | 37 | /** 38 | * Correct the font size and margin on `h1` elements within `section` and 39 | * `article` contexts in Chrome, Firefox, and Safari. 40 | */ 41 | 42 | h1 { 43 | margin: 0.67em 0; 44 | 45 | font-size: 2em; 46 | } 47 | 48 | /* Grouping content 49 | ========================================================================== */ 50 | 51 | /** 52 | * 1. Add the correct box sizing in Firefox. 53 | * 2. Show the overflow in Edge and IE. 54 | */ 55 | 56 | hr { 57 | box-sizing: content-box; /* 1 */ 58 | height: 0; /* 1 */ 59 | overflow: visible; /* 2 */ 60 | } 61 | 62 | /** 63 | * 1. Correct the inheritance and scaling of font size in all browsers. 64 | * 2. Correct the odd `em` font sizing in all browsers. 65 | */ 66 | 67 | pre { 68 | font-size: 1em; /* 2 */ 69 | font-family: monospace, monospace; /* 1 */ 70 | } 71 | 72 | /* Text-level semantics 73 | ========================================================================== */ 74 | 75 | /** 76 | * Remove the gray background on active links in IE 10. 77 | */ 78 | 79 | a { 80 | background-color: transparent; 81 | } 82 | 83 | /** 84 | * 1. Remove the bottom border in Chrome 57- 85 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 86 | */ 87 | 88 | abbr[title] { 89 | text-decoration: underline; /* 2 */ 90 | text-decoration: underline dotted; /* 2 */ 91 | 92 | border-bottom: none; /* 1 */ 93 | } 94 | 95 | /** 96 | * Add the correct font weight in Chrome, Edge, and Safari. 97 | */ 98 | 99 | b, 100 | strong { 101 | font-weight: bolder; 102 | } 103 | 104 | /** 105 | * 1. Correct the inheritance and scaling of font size in all browsers. 106 | * 2. Correct the odd `em` font sizing in all browsers. 107 | */ 108 | 109 | code, 110 | kbd, 111 | samp { 112 | font-size: 1em; /* 2 */ 113 | font-family: monospace, monospace; /* 1 */ 114 | } 115 | 116 | /** 117 | * Add the correct font size in all browsers. 118 | */ 119 | 120 | small { 121 | font-size: 80%; 122 | } 123 | 124 | /** 125 | * Prevent `sub` and `sup` elements from affecting the line height in 126 | * all browsers. 127 | */ 128 | 129 | sub, 130 | sup { 131 | position: relative; 132 | 133 | font-size: 75%; 134 | line-height: 0; 135 | vertical-align: baseline; 136 | } 137 | 138 | sub { 139 | bottom: -0.25em; 140 | } 141 | 142 | sup { 143 | top: -0.5em; 144 | } 145 | 146 | /* Embedded content 147 | ========================================================================== */ 148 | 149 | /** 150 | * Remove the border on images inside links in IE 10. 151 | */ 152 | 153 | img { 154 | border-style: none; 155 | } 156 | 157 | /* Forms 158 | ========================================================================== */ 159 | 160 | /** 161 | * 1. Change the font styles in all browsers. 162 | * 2. Remove the margin in Firefox and Safari. 163 | */ 164 | 165 | button, 166 | input, 167 | optgroup, 168 | select, 169 | textarea { 170 | margin: 0; /* 2 */ 171 | 172 | font-size: 100%; /* 1 */ 173 | line-height: 1.15; /* 1 */ 174 | font-family: inherit; /* 1 */ 175 | } 176 | 177 | /** 178 | * Show the overflow in IE. 179 | * 1. Show the overflow in Edge. 180 | */ 181 | 182 | button, 183 | input { 184 | /* 1 */ 185 | overflow: visible; 186 | } 187 | 188 | /** 189 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 190 | * 1. Remove the inheritance of text transform in Firefox. 191 | */ 192 | 193 | button, 194 | select { 195 | /* 1 */ 196 | text-transform: none; 197 | } 198 | 199 | /** 200 | * Correct the inability to style clickable types in iOS and Safari. 201 | */ 202 | 203 | button, 204 | [type="button"], 205 | [type="reset"], 206 | [type="submit"] { 207 | -webkit-appearance: button; 208 | } 209 | 210 | /** 211 | * Remove the inner border and padding in Firefox. 212 | */ 213 | 214 | button::-moz-focus-inner, 215 | [type="button"]::-moz-focus-inner, 216 | [type="reset"]::-moz-focus-inner, 217 | [type="submit"]::-moz-focus-inner { 218 | padding: 0; 219 | 220 | border-style: none; 221 | } 222 | 223 | /** 224 | * Restore the focus styles unset by the previous rule. 225 | */ 226 | 227 | button:-moz-focusring, 228 | [type="button"]:-moz-focusring, 229 | [type="reset"]:-moz-focusring, 230 | [type="submit"]:-moz-focusring { 231 | outline: 1px dotted ButtonText; 232 | } 233 | 234 | /** 235 | * Correct the padding in Firefox. 236 | */ 237 | 238 | fieldset { 239 | padding: 0.35em 0.75em 0.625em; 240 | } 241 | 242 | /** 243 | * 1. Correct the text wrapping in Edge and IE. 244 | * 2. Correct the color inheritance from `fieldset` elements in IE. 245 | * 3. Remove the padding so developers are not caught out when they zero out 246 | * `fieldset` elements in all browsers. 247 | */ 248 | 249 | legend { 250 | display: table; /* 1 */ 251 | box-sizing: border-box; /* 1 */ 252 | max-width: 100%; /* 1 */ 253 | padding: 0; /* 3 */ 254 | 255 | color: inherit; /* 2 */ 256 | white-space: normal; /* 1 */ 257 | } 258 | 259 | /** 260 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 261 | */ 262 | 263 | progress { 264 | vertical-align: baseline; 265 | } 266 | 267 | /** 268 | * Remove the default vertical scrollbar in IE 10+. 269 | */ 270 | 271 | textarea { 272 | overflow: auto; 273 | } 274 | 275 | /** 276 | * 1. Add the correct box sizing in IE 10. 277 | * 2. Remove the padding in IE 10. 278 | */ 279 | 280 | [type="checkbox"], 281 | [type="radio"] { 282 | box-sizing: border-box; /* 1 */ 283 | padding: 0; /* 2 */ 284 | } 285 | 286 | /** 287 | * Correct the cursor style of increment and decrement buttons in Chrome. 288 | */ 289 | 290 | [type="number"]::-webkit-inner-spin-button, 291 | [type="number"]::-webkit-outer-spin-button { 292 | height: auto; 293 | } 294 | 295 | /** 296 | * 1. Correct the odd appearance in Chrome and Safari. 297 | * 2. Correct the outline style in Safari. 298 | */ 299 | 300 | [type="search"] { 301 | outline-offset: -2px; /* 2 */ 302 | 303 | -webkit-appearance: textfield; /* 1 */ 304 | } 305 | 306 | /** 307 | * Remove the inner padding in Chrome and Safari on macOS. 308 | */ 309 | 310 | [type="search"]::-webkit-search-decoration { 311 | -webkit-appearance: none; 312 | } 313 | 314 | /** 315 | * 1. Correct the inability to style clickable types in iOS and Safari. 316 | * 2. Change font properties to `inherit` in Safari. 317 | */ 318 | 319 | ::-webkit-file-upload-button { 320 | font: inherit; /* 2 */ 321 | 322 | -webkit-appearance: button; /* 1 */ 323 | } 324 | 325 | /* Interactive 326 | ========================================================================== */ 327 | 328 | /* 329 | * Add the correct display in Edge, IE 10+, and Firefox. 330 | */ 331 | 332 | details { 333 | display: block; 334 | } 335 | 336 | /* 337 | * Add the correct display in all browsers. 338 | */ 339 | 340 | summary { 341 | display: list-item; 342 | } 343 | 344 | /* Misc 345 | ========================================================================== */ 346 | 347 | /** 348 | * Add the correct display in IE 10+. 349 | */ 350 | 351 | template { 352 | display: none; 353 | } 354 | 355 | /** 356 | * Add the correct display in IE 10. 357 | */ 358 | 359 | [hidden] { 360 | display: none; 361 | } 362 | -------------------------------------------------------------------------------- /source/sass/vendor/swiper.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | /** 3 | * Swiper 7.4.1 4 | * Most modern mobile touch slider and framework with hardware accelerated transitions 5 | * https://swiperjs.com 6 | * 7 | * Copyright 2014-2021 Vladimir Kharlampidi 8 | * 9 | * Released under the MIT License 10 | * 11 | * Released on: December 24, 2021 12 | */ 13 | 14 | @font-face { 15 | font-family: 'swiper-icons'; 16 | src: url('data:application/font-woff;charset=utf-8;base64, d09GRgABAAAAAAZgABAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAGRAAAABoAAAAci6qHkUdERUYAAAWgAAAAIwAAACQAYABXR1BPUwAABhQAAAAuAAAANuAY7+xHU1VCAAAFxAAAAFAAAABm2fPczU9TLzIAAAHcAAAASgAAAGBP9V5RY21hcAAAAkQAAACIAAABYt6F0cBjdnQgAAACzAAAAAQAAAAEABEBRGdhc3AAAAWYAAAACAAAAAj//wADZ2x5ZgAAAywAAADMAAAD2MHtryVoZWFkAAABbAAAADAAAAA2E2+eoWhoZWEAAAGcAAAAHwAAACQC9gDzaG10eAAAAigAAAAZAAAArgJkABFsb2NhAAAC0AAAAFoAAABaFQAUGG1heHAAAAG8AAAAHwAAACAAcABAbmFtZQAAA/gAAAE5AAACXvFdBwlwb3N0AAAFNAAAAGIAAACE5s74hXjaY2BkYGAAYpf5Hu/j+W2+MnAzMYDAzaX6QjD6/4//Bxj5GA8AuRwMYGkAPywL13jaY2BkYGA88P8Agx4j+/8fQDYfA1AEBWgDAIB2BOoAeNpjYGRgYNBh4GdgYgABEMnIABJzYNADCQAACWgAsQB42mNgYfzCOIGBlYGB0YcxjYGBwR1Kf2WQZGhhYGBiYGVmgAFGBiQQkOaawtDAoMBQxXjg/wEGPcYDDA4wNUA2CCgwsAAAO4EL6gAAeNpj2M0gyAACqxgGNWBkZ2D4/wMA+xkDdgAAAHjaY2BgYGaAYBkGRgYQiAHyGMF8FgYHIM3DwMHABGQrMOgyWDLEM1T9/w8UBfEMgLzE////P/5//f/V/xv+r4eaAAeMbAxwIUYmIMHEgKYAYjUcsDAwsLKxc3BycfPw8jEQA/gZBASFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTQZBgMAAMR+E+gAEQFEAAAAKgAqACoANAA+AEgAUgBcAGYAcAB6AIQAjgCYAKIArAC2AMAAygDUAN4A6ADyAPwBBgEQARoBJAEuATgBQgFMAVYBYAFqAXQBfgGIAZIBnAGmAbIBzgHsAAB42u2NMQ6CUAyGW568x9AneYYgm4MJbhKFaExIOAVX8ApewSt4Bic4AfeAid3VOBixDxfPYEza5O+Xfi04YADggiUIULCuEJK8VhO4bSvpdnktHI5QCYtdi2sl8ZnXaHlqUrNKzdKcT8cjlq+rwZSvIVczNiezsfnP/uznmfPFBNODM2K7MTQ45YEAZqGP81AmGGcF3iPqOop0r1SPTaTbVkfUe4HXj97wYE+yNwWYxwWu4v1ugWHgo3S1XdZEVqWM7ET0cfnLGxWfkgR42o2PvWrDMBSFj/IHLaF0zKjRgdiVMwScNRAoWUoH78Y2icB/yIY09An6AH2Bdu/UB+yxopYshQiEvnvu0dURgDt8QeC8PDw7Fpji3fEA4z/PEJ6YOB5hKh4dj3EvXhxPqH/SKUY3rJ7srZ4FZnh1PMAtPhwP6fl2PMJMPDgeQ4rY8YT6Gzao0eAEA409DuggmTnFnOcSCiEiLMgxCiTI6Cq5DZUd3Qmp10vO0LaLTd2cjN4fOumlc7lUYbSQcZFkutRG7g6JKZKy0RmdLY680CDnEJ+UMkpFFe1RN7nxdVpXrC4aTtnaurOnYercZg2YVmLN/d/gczfEimrE/fs/bOuq29Zmn8tloORaXgZgGa78yO9/cnXm2BpaGvq25Dv9S4E9+5SIc9PqupJKhYFSSl47+Qcr1mYNAAAAeNptw0cKwkAAAMDZJA8Q7OUJvkLsPfZ6zFVERPy8qHh2YER+3i/BP83vIBLLySsoKimrqKqpa2hp6+jq6RsYGhmbmJqZSy0sraxtbO3sHRydnEMU4uR6yx7JJXveP7WrDycAAAAAAAH//wACeNpjYGRgYOABYhkgZgJCZgZNBkYGLQZtIJsFLMYAAAw3ALgAeNolizEKgDAQBCchRbC2sFER0YD6qVQiBCv/H9ezGI6Z5XBAw8CBK/m5iQQVauVbXLnOrMZv2oLdKFa8Pjuru2hJzGabmOSLzNMzvutpB3N42mNgZGBg4GKQYzBhYMxJLMlj4GBgAYow/P/PAJJhLM6sSoWKfWCAAwDAjgbRAAB42mNgYGBkAIIbCZo5IPrmUn0hGA0AO8EFTQAA'); 17 | font-weight: 400; 18 | font-style: normal; 19 | } 20 | :root { 21 | --swiper-theme-color: #007aff; 22 | } 23 | .swiper { 24 | margin-left: auto; 25 | margin-right: auto; 26 | position: relative; 27 | overflow: hidden; 28 | list-style: none; 29 | padding: 0; 30 | /* Fix of Webkit flickering */ 31 | z-index: 1; 32 | } 33 | .swiper-vertical > .swiper-wrapper { 34 | flex-direction: column; 35 | } 36 | .swiper-wrapper { 37 | position: relative; 38 | width: 100%; 39 | height: 100%; 40 | z-index: 1; 41 | display: flex; 42 | transition-property: transform; 43 | box-sizing: content-box; 44 | } 45 | .swiper-android .swiper-slide, 46 | .swiper-wrapper { 47 | transform: translate3d(0px, 0, 0); 48 | } 49 | .swiper-pointer-events { 50 | touch-action: pan-y; 51 | } 52 | .swiper-pointer-events.swiper-vertical { 53 | touch-action: pan-x; 54 | } 55 | .swiper-slide { 56 | flex-shrink: 0; 57 | width: 100%; 58 | height: 100%; 59 | position: relative; 60 | transition-property: transform; 61 | } 62 | .swiper-slide-invisible-blank { 63 | visibility: hidden; 64 | } 65 | /* Auto Height */ 66 | .swiper-autoheight, 67 | .swiper-autoheight .swiper-slide { 68 | height: auto; 69 | } 70 | .swiper-autoheight .swiper-wrapper { 71 | align-items: flex-start; 72 | transition-property: transform, height; 73 | } 74 | /* 3D Effects */ 75 | .swiper-3d, 76 | .swiper-3d.swiper-css-mode .swiper-wrapper { 77 | perspective: 1200px; 78 | } 79 | .swiper-3d .swiper-wrapper, 80 | .swiper-3d .swiper-slide, 81 | .swiper-3d .swiper-slide-shadow, 82 | .swiper-3d .swiper-slide-shadow-left, 83 | .swiper-3d .swiper-slide-shadow-right, 84 | .swiper-3d .swiper-slide-shadow-top, 85 | .swiper-3d .swiper-slide-shadow-bottom, 86 | .swiper-3d .swiper-cube-shadow { 87 | transform-style: preserve-3d; 88 | } 89 | .swiper-3d .swiper-slide-shadow, 90 | .swiper-3d .swiper-slide-shadow-left, 91 | .swiper-3d .swiper-slide-shadow-right, 92 | .swiper-3d .swiper-slide-shadow-top, 93 | .swiper-3d .swiper-slide-shadow-bottom { 94 | position: absolute; 95 | left: 0; 96 | top: 0; 97 | width: 100%; 98 | height: 100%; 99 | pointer-events: none; 100 | z-index: 10; 101 | } 102 | .swiper-3d .swiper-slide-shadow { 103 | background: rgba(0, 0, 0, 0.15); 104 | } 105 | .swiper-3d .swiper-slide-shadow-left { 106 | background-image: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)); 107 | } 108 | .swiper-3d .swiper-slide-shadow-right { 109 | background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)); 110 | } 111 | .swiper-3d .swiper-slide-shadow-top { 112 | background-image: linear-gradient(to top, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)); 113 | } 114 | .swiper-3d .swiper-slide-shadow-bottom { 115 | background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)); 116 | } 117 | /* CSS Mode */ 118 | .swiper-css-mode > .swiper-wrapper { 119 | overflow: auto; 120 | scrollbar-width: none; 121 | /* For Firefox */ 122 | -ms-overflow-style: none; 123 | /* For Internet Explorer and Edge */ 124 | } 125 | .swiper-css-mode > .swiper-wrapper::-webkit-scrollbar { 126 | display: none; 127 | } 128 | .swiper-css-mode > .swiper-wrapper > .swiper-slide { 129 | scroll-snap-align: start start; 130 | } 131 | .swiper-horizontal.swiper-css-mode > .swiper-wrapper { 132 | scroll-snap-type: x mandatory; 133 | } 134 | .swiper-vertical.swiper-css-mode > .swiper-wrapper { 135 | scroll-snap-type: y mandatory; 136 | } 137 | .swiper-centered > .swiper-wrapper::before { 138 | content: ''; 139 | flex-shrink: 0; 140 | order: 9999; 141 | } 142 | .swiper-centered.swiper-horizontal > .swiper-wrapper > .swiper-slide:first-child { 143 | margin-inline-start: var(--swiper-centered-offset-before); 144 | } 145 | .swiper-centered.swiper-horizontal > .swiper-wrapper::before { 146 | height: 100%; 147 | min-height: 1px; 148 | width: var(--swiper-centered-offset-after); 149 | } 150 | .swiper-centered.swiper-vertical > .swiper-wrapper > .swiper-slide:first-child { 151 | margin-block-start: var(--swiper-centered-offset-before); 152 | } 153 | .swiper-centered.swiper-vertical > .swiper-wrapper::before { 154 | width: 100%; 155 | min-width: 1px; 156 | height: var(--swiper-centered-offset-after); 157 | } 158 | .swiper-centered > .swiper-wrapper > .swiper-slide { 159 | scroll-snap-align: center center; 160 | } 161 | .swiper-virtual.swiper-css-mode .swiper-wrapper::after { 162 | content: ''; 163 | position: absolute; 164 | left: 0; 165 | top: 0; 166 | pointer-events: none; 167 | } 168 | .swiper-virtual.swiper-css-mode.swiper-horizontal .swiper-wrapper::after { 169 | height: 1px; 170 | width: var(--swiper-virtual-size); 171 | } 172 | .swiper-virtual.swiper-css-mode.swiper-vertical .swiper-wrapper::after { 173 | width: 1px; 174 | height: var(--swiper-virtual-size); 175 | } 176 | :root { 177 | --swiper-navigation-size: 44px; 178 | /* 179 | --swiper-navigation-color: var(--swiper-theme-color); 180 | */ 181 | } 182 | .swiper-button-prev, 183 | .swiper-button-next { 184 | position: absolute; 185 | top: 50%; 186 | width: calc(var(--swiper-navigation-size) / 44 * 27); 187 | height: var(--swiper-navigation-size); 188 | margin-top: calc(0px - (var(--swiper-navigation-size) / 2)); 189 | z-index: 10; 190 | cursor: pointer; 191 | display: flex; 192 | align-items: center; 193 | justify-content: center; 194 | color: var(--swiper-navigation-color, var(--swiper-theme-color)); 195 | } 196 | .swiper-button-prev.swiper-button-disabled, 197 | .swiper-button-next.swiper-button-disabled { 198 | opacity: 0.35; 199 | cursor: auto; 200 | pointer-events: none; 201 | } 202 | .swiper-button-prev:after, 203 | .swiper-button-next:after { 204 | font-family: swiper-icons; 205 | font-size: var(--swiper-navigation-size); 206 | text-transform: none !important; 207 | letter-spacing: 0; 208 | text-transform: none; 209 | font-variant: initial; 210 | line-height: 1; 211 | } 212 | .swiper-button-prev, 213 | .swiper-rtl .swiper-button-next { 214 | left: 10px; 215 | right: auto; 216 | } 217 | .swiper-button-prev:after, 218 | .swiper-rtl .swiper-button-next:after { 219 | content: 'prev'; 220 | } 221 | .swiper-button-next, 222 | .swiper-rtl .swiper-button-prev { 223 | right: 10px; 224 | left: auto; 225 | } 226 | .swiper-button-next:after, 227 | .swiper-rtl .swiper-button-prev:after { 228 | content: 'next'; 229 | } 230 | .swiper-button-lock { 231 | display: none; 232 | } 233 | :root { 234 | /* 235 | --swiper-pagination-color: var(--swiper-theme-color); 236 | --swiper-pagination-bullet-size: 8px; 237 | --swiper-pagination-bullet-width: 8px; 238 | --swiper-pagination-bullet-height: 8px; 239 | --swiper-pagination-bullet-inactive-color: #000; 240 | --swiper-pagination-bullet-inactive-opacity: 0.2; 241 | --swiper-pagination-bullet-opacity: 1; 242 | --swiper-pagination-bullet-horizontal-gap: 4px; 243 | --swiper-pagination-bullet-vertical-gap: 6px; 244 | */ 245 | } 246 | .swiper-pagination { 247 | position: absolute; 248 | text-align: center; 249 | transition: 300ms opacity; 250 | transform: translate3d(0, 0, 0); 251 | z-index: 10; 252 | } 253 | .swiper-pagination.swiper-pagination-hidden { 254 | opacity: 0; 255 | } 256 | /* Common Styles */ 257 | .swiper-pagination-fraction, 258 | .swiper-pagination-custom, 259 | .swiper-horizontal > .swiper-pagination-bullets, 260 | .swiper-pagination-bullets.swiper-pagination-horizontal { 261 | bottom: 10px; 262 | left: 0; 263 | width: 100%; 264 | } 265 | /* Bullets */ 266 | .swiper-pagination-bullets-dynamic { 267 | overflow: hidden; 268 | font-size: 0; 269 | } 270 | .swiper-pagination-bullets-dynamic .swiper-pagination-bullet { 271 | transform: scale(0.33); 272 | position: relative; 273 | } 274 | .swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active { 275 | transform: scale(1); 276 | } 277 | .swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-main { 278 | transform: scale(1); 279 | } 280 | .swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev { 281 | transform: scale(0.66); 282 | } 283 | .swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-prev-prev { 284 | transform: scale(0.33); 285 | } 286 | .swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next { 287 | transform: scale(0.66); 288 | } 289 | .swiper-pagination-bullets-dynamic .swiper-pagination-bullet-active-next-next { 290 | transform: scale(0.33); 291 | } 292 | .swiper-pagination-bullet { 293 | width: var(--swiper-pagination-bullet-width, var(--swiper-pagination-bullet-size, 8px)); 294 | height: var(--swiper-pagination-bullet-height, var(--swiper-pagination-bullet-size, 8px)); 295 | display: inline-block; 296 | border-radius: 50%; 297 | background: var(--swiper-pagination-bullet-inactive-color, #000); 298 | opacity: var(--swiper-pagination-bullet-inactive-opacity, 0.2); 299 | } 300 | button.swiper-pagination-bullet { 301 | border: none; 302 | margin: 0; 303 | padding: 0; 304 | box-shadow: none; 305 | -webkit-appearance: none; 306 | appearance: none; 307 | } 308 | .swiper-pagination-clickable .swiper-pagination-bullet { 309 | cursor: pointer; 310 | } 311 | .swiper-pagination-bullet:only-child { 312 | display: none !important; 313 | } 314 | .swiper-pagination-bullet-active { 315 | opacity: var(--swiper-pagination-bullet-opacity, 1); 316 | background: var(--swiper-pagination-color, var(--swiper-theme-color)); 317 | } 318 | .swiper-vertical > .swiper-pagination-bullets, 319 | .swiper-pagination-vertical.swiper-pagination-bullets { 320 | right: 10px; 321 | top: 50%; 322 | transform: translate3d(0px, -50%, 0); 323 | } 324 | .swiper-vertical > .swiper-pagination-bullets .swiper-pagination-bullet, 325 | .swiper-pagination-vertical.swiper-pagination-bullets .swiper-pagination-bullet { 326 | margin: var(--swiper-pagination-bullet-vertical-gap, 6px) 0; 327 | display: block; 328 | } 329 | .swiper-vertical > .swiper-pagination-bullets.swiper-pagination-bullets-dynamic, 330 | .swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic { 331 | top: 50%; 332 | transform: translateY(-50%); 333 | width: 8px; 334 | } 335 | .swiper-vertical > .swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet, 336 | .swiper-pagination-vertical.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet { 337 | display: inline-block; 338 | transition: 200ms transform, 200ms top; 339 | } 340 | .swiper-horizontal > .swiper-pagination-bullets .swiper-pagination-bullet, 341 | .swiper-pagination-horizontal.swiper-pagination-bullets .swiper-pagination-bullet { 342 | margin: 0 var(--swiper-pagination-bullet-horizontal-gap, 4px); 343 | } 344 | .swiper-horizontal > .swiper-pagination-bullets.swiper-pagination-bullets-dynamic, 345 | .swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic { 346 | left: 50%; 347 | transform: translateX(-50%); 348 | white-space: nowrap; 349 | } 350 | .swiper-horizontal > .swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet, 351 | .swiper-pagination-horizontal.swiper-pagination-bullets.swiper-pagination-bullets-dynamic .swiper-pagination-bullet { 352 | transition: 200ms transform, 200ms left; 353 | } 354 | .swiper-horizontal.swiper-rtl > .swiper-pagination-bullets-dynamic .swiper-pagination-bullet { 355 | transition: 200ms transform, 200ms right; 356 | } 357 | /* Progress */ 358 | .swiper-pagination-progressbar { 359 | background: rgba(0, 0, 0, 0.25); 360 | position: absolute; 361 | } 362 | .swiper-pagination-progressbar .swiper-pagination-progressbar-fill { 363 | background: var(--swiper-pagination-color, var(--swiper-theme-color)); 364 | position: absolute; 365 | left: 0; 366 | top: 0; 367 | width: 100%; 368 | height: 100%; 369 | transform: scale(0); 370 | transform-origin: left top; 371 | } 372 | .swiper-rtl .swiper-pagination-progressbar .swiper-pagination-progressbar-fill { 373 | transform-origin: right top; 374 | } 375 | .swiper-horizontal > .swiper-pagination-progressbar, 376 | .swiper-pagination-progressbar.swiper-pagination-horizontal, 377 | .swiper-vertical > .swiper-pagination-progressbar.swiper-pagination-progressbar-opposite, 378 | .swiper-pagination-progressbar.swiper-pagination-vertical.swiper-pagination-progressbar-opposite { 379 | width: 100%; 380 | height: 4px; 381 | left: 0; 382 | top: 0; 383 | } 384 | .swiper-vertical > .swiper-pagination-progressbar, 385 | .swiper-pagination-progressbar.swiper-pagination-vertical, 386 | .swiper-horizontal > .swiper-pagination-progressbar.swiper-pagination-progressbar-opposite, 387 | .swiper-pagination-progressbar.swiper-pagination-horizontal.swiper-pagination-progressbar-opposite { 388 | width: 4px; 389 | height: 100%; 390 | left: 0; 391 | top: 0; 392 | } 393 | .swiper-pagination-lock { 394 | display: none; 395 | } 396 | /* Scrollbar */ 397 | .swiper-scrollbar { 398 | border-radius: 10px; 399 | position: relative; 400 | -ms-touch-action: none; 401 | background: rgba(0, 0, 0, 0.1); 402 | } 403 | .swiper-horizontal > .swiper-scrollbar { 404 | position: absolute; 405 | left: 1%; 406 | bottom: 3px; 407 | z-index: 50; 408 | height: 5px; 409 | width: 98%; 410 | } 411 | .swiper-vertical > .swiper-scrollbar { 412 | position: absolute; 413 | right: 3px; 414 | top: 1%; 415 | z-index: 50; 416 | width: 5px; 417 | height: 98%; 418 | } 419 | .swiper-scrollbar-drag { 420 | height: 100%; 421 | width: 100%; 422 | position: relative; 423 | background: rgba(0, 0, 0, 0.5); 424 | border-radius: 10px; 425 | left: 0; 426 | top: 0; 427 | } 428 | .swiper-scrollbar-cursor-drag { 429 | cursor: move; 430 | } 431 | .swiper-scrollbar-lock { 432 | display: none; 433 | } 434 | .swiper-zoom-container { 435 | width: 100%; 436 | height: 100%; 437 | display: flex; 438 | justify-content: center; 439 | align-items: center; 440 | text-align: center; 441 | } 442 | .swiper-zoom-container > img, 443 | .swiper-zoom-container > svg, 444 | .swiper-zoom-container > canvas { 445 | max-width: 100%; 446 | max-height: 100%; 447 | object-fit: contain; 448 | } 449 | .swiper-slide-zoomed { 450 | cursor: move; 451 | } 452 | /* Preloader */ 453 | :root { 454 | /* 455 | --swiper-preloader-color: var(--swiper-theme-color); 456 | */ 457 | } 458 | .swiper-lazy-preloader { 459 | width: 42px; 460 | height: 42px; 461 | position: absolute; 462 | left: 50%; 463 | top: 50%; 464 | margin-left: -21px; 465 | margin-top: -21px; 466 | z-index: 10; 467 | transform-origin: 50%; 468 | animation: swiper-preloader-spin 1s infinite linear; 469 | box-sizing: border-box; 470 | border: 4px solid var(--swiper-preloader-color, var(--swiper-theme-color)); 471 | border-radius: 50%; 472 | border-top-color: transparent; 473 | } 474 | .swiper-lazy-preloader-white { 475 | --swiper-preloader-color: #fff; 476 | } 477 | .swiper-lazy-preloader-black { 478 | --swiper-preloader-color: #000; 479 | } 480 | @keyframes swiper-preloader-spin { 481 | 100% { 482 | transform: rotate(360deg); 483 | } 484 | } 485 | /* a11y */ 486 | .swiper .swiper-notification { 487 | position: absolute; 488 | left: 0; 489 | top: 0; 490 | pointer-events: none; 491 | opacity: 0; 492 | z-index: -1000; 493 | } 494 | .swiper-free-mode > .swiper-wrapper { 495 | transition-timing-function: ease-out; 496 | margin: 0 auto; 497 | } 498 | .swiper-grid > .swiper-wrapper { 499 | flex-wrap: wrap; 500 | } 501 | .swiper-grid-column > .swiper-wrapper { 502 | flex-wrap: wrap; 503 | flex-direction: column; 504 | } 505 | .swiper-fade.swiper-free-mode .swiper-slide { 506 | transition-timing-function: ease-out; 507 | } 508 | .swiper-fade .swiper-slide { 509 | pointer-events: none; 510 | transition-property: opacity; 511 | } 512 | .swiper-fade .swiper-slide .swiper-slide { 513 | pointer-events: none; 514 | } 515 | .swiper-fade .swiper-slide-active, 516 | .swiper-fade .swiper-slide-active .swiper-slide-active { 517 | pointer-events: auto; 518 | } 519 | .swiper-cube { 520 | overflow: visible; 521 | } 522 | .swiper-cube .swiper-slide { 523 | pointer-events: none; 524 | -webkit-backface-visibility: hidden; 525 | backface-visibility: hidden; 526 | z-index: 1; 527 | visibility: hidden; 528 | transform-origin: 0 0; 529 | width: 100%; 530 | height: 100%; 531 | } 532 | .swiper-cube .swiper-slide .swiper-slide { 533 | pointer-events: none; 534 | } 535 | .swiper-cube.swiper-rtl .swiper-slide { 536 | transform-origin: 100% 0; 537 | } 538 | .swiper-cube .swiper-slide-active, 539 | .swiper-cube .swiper-slide-active .swiper-slide-active { 540 | pointer-events: auto; 541 | } 542 | .swiper-cube .swiper-slide-active, 543 | .swiper-cube .swiper-slide-next, 544 | .swiper-cube .swiper-slide-prev, 545 | .swiper-cube .swiper-slide-next + .swiper-slide { 546 | pointer-events: auto; 547 | visibility: visible; 548 | } 549 | .swiper-cube .swiper-slide-shadow-top, 550 | .swiper-cube .swiper-slide-shadow-bottom, 551 | .swiper-cube .swiper-slide-shadow-left, 552 | .swiper-cube .swiper-slide-shadow-right { 553 | z-index: 0; 554 | -webkit-backface-visibility: hidden; 555 | backface-visibility: hidden; 556 | } 557 | .swiper-cube .swiper-cube-shadow { 558 | position: absolute; 559 | left: 0; 560 | bottom: 0px; 561 | width: 100%; 562 | height: 100%; 563 | opacity: 0.6; 564 | z-index: 0; 565 | } 566 | .swiper-cube .swiper-cube-shadow:before { 567 | content: ''; 568 | background: #000; 569 | position: absolute; 570 | left: 0; 571 | top: 0; 572 | bottom: 0; 573 | right: 0; 574 | filter: blur(50px); 575 | } 576 | .swiper-flip { 577 | overflow: visible; 578 | } 579 | .swiper-flip .swiper-slide { 580 | pointer-events: none; 581 | -webkit-backface-visibility: hidden; 582 | backface-visibility: hidden; 583 | z-index: 1; 584 | } 585 | .swiper-flip .swiper-slide .swiper-slide { 586 | pointer-events: none; 587 | } 588 | .swiper-flip .swiper-slide-active, 589 | .swiper-flip .swiper-slide-active .swiper-slide-active { 590 | pointer-events: auto; 591 | } 592 | .swiper-flip .swiper-slide-shadow-top, 593 | .swiper-flip .swiper-slide-shadow-bottom, 594 | .swiper-flip .swiper-slide-shadow-left, 595 | .swiper-flip .swiper-slide-shadow-right { 596 | z-index: 0; 597 | -webkit-backface-visibility: hidden; 598 | backface-visibility: hidden; 599 | } 600 | .swiper-creative .swiper-slide { 601 | -webkit-backface-visibility: hidden; 602 | backface-visibility: hidden; 603 | overflow: hidden; 604 | transition-property: transform, opacity, height; 605 | } 606 | .swiper-cards { 607 | overflow: visible; 608 | } 609 | .swiper-cards .swiper-slide { 610 | transform-origin: center bottom; 611 | -webkit-backface-visibility: hidden; 612 | backface-visibility: hidden; 613 | overflow: hidden; 614 | } 615 | -------------------------------------------------------------------------------- /stylelintconfig.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'font-family-no-duplicate-names': true, 4 | 'font-family-no-missing-generic-family-keyword': true, 5 | 'no-descending-specificity': null, 6 | 7 | 'color-hex-case': 'lower', 8 | 'color-hex-length': 'long', 9 | 10 | 'comment-no-empty': true, 11 | 12 | 'font-family-name-quotes': 'always-unless-keyword', 13 | 14 | 'function-comma-newline-after': 'always-multi-line', 15 | 'function-comma-newline-before': 'never-multi-line', 16 | 'function-comma-space-after': 'always-single-line', 17 | 'function-comma-space-before': 'never', 18 | 'function-max-empty-lines': 0, 19 | 'function-name-case': 'lower', 20 | 'function-parentheses-space-inside': 'never-single-line', 21 | 'function-url-quotes': 'always', 22 | 'function-whitespace-after': 'always', 23 | 'function-calc-no-invalid': true, 24 | 'function-calc-no-unspaced-operator': true, 25 | 'function-linear-gradient-no-nonstandard-direction': true, 26 | 27 | 'number-leading-zero': 'always', 28 | 'number-no-trailing-zeros': true, 29 | 'length-zero-no-unit': true, 30 | 31 | 'string-quotes': 'double', 32 | 33 | 'unit-case': 'lower', 34 | 35 | 'value-keyword-case': 'lower', 36 | 'value-list-comma-newline-after': 'always-multi-line', 37 | 'value-list-comma-newline-before': 'never-multi-line', 38 | 'value-list-comma-space-after': 'always-single-line', 39 | 'value-list-comma-space-before': 'never', 40 | 'value-list-max-empty-lines': 0, 41 | 'value-no-vendor-prefix': true, 42 | 43 | 'property-case': 'lower', 44 | 'property-no-vendor-prefix': true, 45 | 46 | 'keyframe-declaration-no-important': true, 47 | 'declaration-no-important': true, 48 | 49 | 'declaration-bang-space-after': 'never', 50 | 'declaration-bang-space-before': 'always', 51 | 'declaration-colon-space-after': 'always-single-line', 52 | 'declaration-colon-space-before': 'never', 53 | 54 | 'declaration-block-no-duplicate-custom-properties': true, 55 | 'declaration-block-no-shorthand-property-overrides': true, 56 | 'declaration-block-no-duplicate-properties': true, 57 | 'declaration-block-semicolon-newline-after': 'always', 58 | 'declaration-block-semicolon-newline-before': 'never-multi-line', 59 | 'declaration-block-semicolon-space-after': 'always-single-line', 60 | 'declaration-block-semicolon-space-before': 'never', 61 | 'declaration-block-trailing-semicolon': 'always', 62 | 63 | 'block-closing-brace-empty-line-before': 'never', 64 | 'block-closing-brace-newline-after': 'always', 65 | 'block-closing-brace-newline-before': 'always', 66 | 'block-closing-brace-space-after': 'always-single-line', 67 | 'block-closing-brace-space-before': 'always-single-line', 68 | 'block-opening-brace-newline-after': 'always', 69 | 'block-opening-brace-space-after': 'always-single-line', 70 | 'block-opening-brace-space-before': 'always', 71 | 'block-no-empty': true, 72 | 73 | 'selector-attribute-brackets-space-inside': 'never', 74 | 'selector-attribute-operator-space-after': 'never', 75 | 'selector-attribute-operator-space-before': 'never', 76 | 'selector-attribute-quotes': 'always', 77 | 'selector-combinator-space-after': 'always', 78 | 'selector-combinator-space-before': 'always', 79 | 'selector-descendant-combinator-no-non-space': true, 80 | 'selector-max-id': 0, 81 | 'selector-pseudo-class-case': 'lower', 82 | 'selector-pseudo-class-parentheses-space-inside': 'never', 83 | 'selector-pseudo-element-case': 'lower', 84 | 'selector-pseudo-element-colon-notation': 'double', 85 | 'selector-type-case': 'lower', 86 | 'selector-type-no-unknown': true, 87 | 'selector-max-empty-lines': 0, 88 | 89 | 'selector-list-comma-newline-after': 'always', 90 | 'selector-list-comma-newline-before': 'never-multi-line', 91 | 'selector-list-comma-space-after': 'always-single-line', 92 | 'selector-list-comma-space-before': 'never', 93 | 94 | 'rule-empty-line-before': [ 95 | 'always', 96 | { 97 | 'except': ['first-nested'], 98 | 'ignore': ['after-comment'], 99 | } 100 | ], 101 | 102 | 'media-feature-colon-space-after': 'always', 103 | 'media-feature-colon-space-before': 'never', 104 | 'media-feature-name-case': 'lower', 105 | 'media-feature-name-no-vendor-prefix': true, 106 | 'media-feature-parentheses-space-inside': 'never', 107 | 'media-feature-range-operator-space-after': 'always', 108 | 'media-feature-range-operator-space-before': 'always', 109 | 'media-feature-name-no-unknown': true, 110 | 111 | 'media-query-list-comma-newline-after': 'always-multi-line', 112 | 'media-query-list-comma-newline-before': 'never-multi-line', 113 | 'media-query-list-comma-space-after': 'always-single-line', 114 | 'media-query-list-comma-space-before': 'never-single-line', 115 | 116 | 'at-rule-empty-line-before': [ 117 | 'always', 118 | { 119 | 'except': ['first-nested', 'blockless-after-blockless'], 120 | 'ignore': ['after-comment'], 121 | } 122 | ], 123 | 'at-rule-name-case': 'lower', 124 | 'at-rule-name-space-after': 'always', 125 | 'at-rule-no-unknown': [ 126 | true, 127 | { 128 | 'ignoreAtRules': ['mixin', 'define-mixin', 'include', 'content', 'rules', 'each'], 129 | } 130 | ], 131 | 'at-rule-no-vendor-prefix': true, 132 | 'at-rule-semicolon-newline-after': 'always', 133 | 134 | 'indentation': [2, {'ignore': ['inside-parens']}], 135 | 136 | 'max-nesting-depth': [4, {'ignoreAtRules': ['media', 'include']}], 137 | 138 | 'max-empty-lines': 2, 139 | 'no-eol-whitespace': true, 140 | 'no-missing-end-of-source-newline': true, 141 | 142 | 'no-duplicate-at-import-rules': true, 143 | 'no-invalid-position-at-import-rule': true, 144 | 145 | 'named-grid-areas-no-invalid': true, 146 | 'no-duplicate-selectors': true, 147 | 'no-empty-source': true, 148 | 'no-extra-semicolons': true, 149 | 'no-invalid-double-slash-comments': true, 150 | 'no-irregular-whitespace': true, 151 | 'property-no-unknown': true, 152 | 'selector-pseudo-class-no-unknown': true, 153 | 'selector-pseudo-element-no-unknown': true, 154 | 155 | 'string-no-newline': true, 156 | 'unit-no-unknown': true, 157 | 158 | 'color-no-invalid-hex': true, 159 | 'color-no-hex': null, 160 | 'color-named': 'never', 161 | 'number-max-precision': 2, 162 | }, 163 | }; 164 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CircularDependencyPlugin = require('circular-dependency-plugin') 3 | const DuplicatePackageCheckerPlugin = require('duplicate-package-checker-webpack-plugin'); 4 | const {CleanWebpackPlugin} = require('clean-webpack-plugin'); 5 | const isProd = process.env.NODE_ENV === 'production'; 6 | const isDev = !isProd; 7 | 8 | module.exports = { 9 | context: path.resolve(__dirname, 'source'), 10 | mode: 'development', 11 | entry: { 12 | main: './js/main.js', 13 | vendor: './js/vendor.js', 14 | }, 15 | devtool: isDev ? 'source-map' : false, 16 | output: { 17 | filename: '[name].min.js', 18 | path: path.resolve(__dirname, 'build/js'), 19 | }, 20 | optimization: { 21 | minimize: isDev ? false : true, 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | loader: 'babel-loader', 29 | options: { 30 | presets: ['@babel/preset-env'], 31 | }, 32 | }, 33 | ], 34 | }, 35 | plugins: [ 36 | new CleanWebpackPlugin(), 37 | new DuplicatePackageCheckerPlugin(), 38 | new CircularDependencyPlugin(), 39 | ], 40 | }; 41 | --------------------------------------------------------------------------------