├── .ecrc
├── .editorconfig
├── .gitignore
├── .htmlhintrc
├── .stylelintignore
├── .stylelintrc.json
├── .vscode
├── extensions.json
└── snippets
│ └── html.json
├── README.md
├── gulp
├── config
│ └── paths.js
└── tasks
│ ├── cache.js
│ ├── clean.js
│ ├── html-include.js
│ ├── html-minify.js
│ ├── images.js
│ ├── resources.js
│ ├── rewrite.js
│ ├── scripts-backend.js
│ ├── scripts.js
│ ├── sprite.js
│ ├── styles-backend.js
│ ├── styles.js
│ ├── webp.js
│ └── zip.js
├── gulpfile.js
├── package.json
└── src
├── img
├── cover.jpg
└── svg
│ └── icon.svg
├── index.html
├── js
├── _components.js
├── _functions.js
├── _vars.js
├── components
│ └── ex.js
├── functions
│ ├── burger.js
│ ├── check-viewport.js
│ ├── disable-scroll.js
│ ├── enable-scroll.js
│ ├── fix-fullheight.js
│ ├── header-height.js
│ ├── mobile-check.js
│ ├── throttle.js
│ └── validate-forms.js
└── main.js
├── partials
├── footer.html
├── head.html
└── header.html
├── resources
├── favicon.ico
├── fonts
│ └── MullerRegular.woff2
├── mail.php
└── phpmailer
│ ├── Exception.php
│ ├── PHPMailer.php
│ └── SMTP.php
└── scss
├── _fonts.scss
├── _mixins.scss
├── _settings.scss
├── _vars.scss
├── components
└── _header.scss
├── main.scss
├── mixins
├── _breakpoint.scss
├── _burger.scss
├── _checkbox.scss
├── _disable-mob-hover.scss
├── _flex.scss
├── _font-face.scss
├── _layout.scss
├── _mini.scss
└── _tabs.scss
├── vendor.scss
└── vendor
└── normalize.css
/.ecrc:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": ["node_modules", "app", "vendor", "gulpfile.js", "package.json", ".stylelintrc", "\\.md$", "\\.php$"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /app
3 | package-lock.json
4 |
--------------------------------------------------------------------------------
/.htmlhintrc:
--------------------------------------------------------------------------------
1 | {
2 | "attr-lowercase": false,
3 | "doctype-first": false
4 | }
5 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | app/**/*.css
2 | src/scss/vendor/**/*
3 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard-scss",
3 | "plugins": [
4 | "stylelint-order"
5 | ],
6 | "rules": {
7 | "order/order": [
8 | "custom-properties",
9 | "dollar-variables",
10 | "at-rules"
11 | ],
12 | "order/properties-alphabetical-order": null,
13 | "order/properties-order": [
14 | [
15 | "all",
16 | "print-color-adjust",
17 | "appearance",
18 | "counter-increment",
19 | "counter-reset",
20 | "content",
21 | "quotes",
22 | "position",
23 | "left",
24 | "right",
25 | "top",
26 | "bottom",
27 | "inset",
28 | "z-index",
29 | "display",
30 | "columns",
31 | "column-width",
32 | "column-count",
33 | "column-fill",
34 | "column-gap",
35 | "column-rule",
36 | "column-rule-style",
37 | "column-rule-width",
38 | "column-rule-color",
39 | "column-span",
40 | "break-after",
41 | "break-before",
42 | "break-inside",
43 | "page-break-after",
44 | "page-break-before",
45 | "page-break-inside",
46 | "orphans",
47 | "widows",
48 | "flex",
49 | "flex-grow",
50 | "flex-shrink",
51 | "flex-basis",
52 | "flex-flow",
53 | "flex-direction",
54 | "flex-wrap",
55 | "place-content",
56 | "place-items",
57 | "place-self",
58 | "align-content",
59 | "align-items",
60 | "align-self",
61 | "justify-content",
62 | "justify-items",
63 | "justify-self",
64 | "order",
65 | "clear",
66 | "float",
67 | "grid",
68 | "grid-area",
69 | "grid-auto-columns",
70 | "grid-auto-flow",
71 | "grid-auto-rows",
72 | "grid-column",
73 | "grid-column-end",
74 | "grid-column-gap",
75 | "grid-column-start",
76 | "grid-gap",
77 | "grid-row",
78 | "grid-row-end",
79 | "grid-row-gap",
80 | "grid-row-start",
81 | "grid-template",
82 | "grid-template-areas",
83 | "grid-template-columns",
84 | "grid-template-rows",
85 | "list-style",
86 | "list-style-type",
87 | "list-style-position",
88 | "list-style-image",
89 | "caption-side",
90 | "empty-cells",
91 | "table-layout",
92 | "vertical-align",
93 | "clip-path",
94 | "mask",
95 | "mask-clip",
96 | "mask-composite",
97 | "mask-image",
98 | "mask-mode",
99 | "mask-origin",
100 | "mask-position",
101 | "mask-position-x",
102 | "mask-position-y",
103 | "mask-repeat",
104 | "mask-repeat-x",
105 | "mask-repeat-y",
106 | "mask-size",
107 | "mask-type",
108 | "shape-image-threshold",
109 | "shape-margin",
110 | "shape-outside",
111 | "contain",
112 | "overflow",
113 | "overflow-x",
114 | "overflow-y",
115 | "overflow-anchor",
116 | "overflow-wrap",
117 | "margin",
118 | "margin-top",
119 | "margin-right",
120 | "margin-bottom",
121 | "margin-left",
122 | "margin-before",
123 | "margin-end",
124 | "margin-after",
125 | "margin-start",
126 | "margin-collapse",
127 | "margin-top-collapse",
128 | "margin-bottom-collapse",
129 | "margin-before-collapse",
130 | "margin-after-collapse",
131 | "outline",
132 | "outline-style",
133 | "outline-width",
134 | "outline-color",
135 | "outline-offset",
136 | "outline-radius",
137 | "outline-radius-topleft",
138 | "outline-radius-topright",
139 | "outline-radius-bottomright",
140 | "outline-radius-bottomleft",
141 | "border",
142 | "border-style",
143 | "border-width",
144 | "border-color",
145 | "border-top",
146 | "border-top-style",
147 | "border-top-width",
148 | "border-top-color",
149 | "border-right",
150 | "border-right-style",
151 | "border-right-width",
152 | "border-right-color",
153 | "border-bottom",
154 | "border-bottom-style",
155 | "border-bottom-width",
156 | "border-bottom-color",
157 | "border-left",
158 | "border-left-style",
159 | "border-left-width",
160 | "border-left-color",
161 | "border-before",
162 | "border-before-style",
163 | "border-before-width",
164 | "border-before-color",
165 | "border-end",
166 | "border-end-style",
167 | "border-end-width",
168 | "border-end-color",
169 | "border-after",
170 | "border-after-style",
171 | "border-after-width",
172 | "border-after-color",
173 | "border-start",
174 | "border-start-style",
175 | "border-start-width",
176 | "border-start-color",
177 | "border-collapse",
178 | "border-image",
179 | "border-image-source",
180 | "border-image-slice",
181 | "border-image-width",
182 | "border-image-outset",
183 | "border-image-repeat",
184 | "border-radius",
185 | "border-top-left-radius",
186 | "border-top-right-radius",
187 | "border-bottom-right-radius",
188 | "border-bottom-left-radius",
189 | "border-spacing",
190 | "padding",
191 | "padding-top",
192 | "padding-right",
193 | "padding-bottom",
194 | "padding-left",
195 | "padding-before",
196 | "padding-end",
197 | "padding-after",
198 | "padding-start",
199 | "width",
200 | "height",
201 | "min-width",
202 | "min-height",
203 | "max-width",
204 | "max-height",
205 | "box-decoration-break",
206 | "box-shadow",
207 | "box-sizing",
208 | "src",
209 | "font",
210 | "font-family",
211 | "font-weight",
212 | "font-style",
213 | "font-display",
214 | "font-feature-settings",
215 | "font-kerning",
216 | "font-smoothing",
217 | "font-stretch",
218 | "font-synthesis",
219 | "font-variant",
220 | "font-variant-alternates",
221 | "font-variant-caps",
222 | "font-variant-east-asian",
223 | "font-variant-ligatures",
224 | "font-variant-numeric",
225 | "font-variant-position",
226 | "font-size",
227 | "font-size-adjust",
228 | "unicode-bidi",
229 | "unicode-range",
230 | "line-break",
231 | "line-height",
232 | "letter-spacing",
233 | "word-break",
234 | "word-spacing",
235 | "word-wrap",
236 | "white-space",
237 | "hyphens",
238 | "tab-size",
239 | "text-align",
240 | "text-align-last",
241 | "text-combine-upright",
242 | "text-decoration",
243 | "text-decoration-style",
244 | "text-decoration-line",
245 | "text-decoration-color",
246 | "text-decoration-skip",
247 | "text-emphasis",
248 | "text-emphasis-style",
249 | "text-emphasis-color",
250 | "text-emphasis-position",
251 | "text-fill-color",
252 | "text-indent",
253 | "text-justify",
254 | "text-orientation",
255 | "text-overflow",
256 | "text-rendering",
257 | "text-security",
258 | "text-shadow",
259 | "text-size-adjust",
260 | "text-stroke",
261 | "text-stroke-width",
262 | "text-stroke-color",
263 | "text-transform",
264 | "text-underline-position",
265 | "direction",
266 | "writing-mode",
267 | "ruby-align",
268 | "ruby-position",
269 | "color",
270 | "caret-color",
271 | "tap-highlight-color",
272 | "d",
273 | "x",
274 | "y",
275 | "cx",
276 | "cy",
277 | "r",
278 | "rx",
279 | "ry",
280 | "fill",
281 | "fill-opacity",
282 | "fill-rule",
283 | "stroke",
284 | "stroke-dasharray",
285 | "stroke-dashoffset",
286 | "stroke-linecap",
287 | "stroke-linejoin",
288 | "stroke-miterlimit",
289 | "stroke-opacity",
290 | "stroke-width",
291 | "alignment-baseline",
292 | "baseline-shift",
293 | "dominant-baseline",
294 | "clip-rule",
295 | "color-interpolation",
296 | "color-interpolation-filters",
297 | "color-rendering",
298 | "flood-color",
299 | "flood-opacity",
300 | "lighting-color",
301 | "marker",
302 | "marker-end",
303 | "marker-mid",
304 | "marker-start",
305 | "paint-order",
306 | "shape-rendering",
307 | "stop-color",
308 | "stop-opacity",
309 | "text-anchor",
310 | "offset",
311 | "offset-position",
312 | "offset-path",
313 | "offset-distance",
314 | "offset-anchor",
315 | "offset-rotate",
316 | "background",
317 | "background-image",
318 | "background-position",
319 | "background-position-x",
320 | "background-position-y",
321 | "background-size",
322 | "background-repeat",
323 | "background-repeat-x",
324 | "background-repeat-y",
325 | "background-origin",
326 | "background-clip",
327 | "background-attachment",
328 | "background-color",
329 | "background-blend-mode",
330 | "image-orientation",
331 | "image-rendering",
332 | "object-fit",
333 | "object-position",
334 | "opacity",
335 | "visibility",
336 | "filter",
337 | "isolation",
338 | "mix-blend-mode",
339 | "zoom",
340 | "backface-visibility",
341 | "perspective",
342 | "perspective-origin",
343 | "perspective-origin-x",
344 | "perspective-origin-y",
345 | "transform",
346 | "transform-box",
347 | "transform-origin",
348 | "transform-origin-x",
349 | "transform-origin-y",
350 | "transform-origin-z",
351 | "transform-style",
352 | "transition",
353 | "transition-property",
354 | "transition-duration",
355 | "transition-delay",
356 | "transition-timing-function",
357 | "animation",
358 | "animation-name",
359 | "animation-duration",
360 | "animation-delay",
361 | "animation-timing-function",
362 | "animation-iteration-count",
363 | "animation-direction",
364 | "animation-fill-mode",
365 | "animation-play-state",
366 | "will-change",
367 | "cursor",
368 | "pointer-events",
369 | "touch-action",
370 | "user-drag",
371 | "user-focus",
372 | "user-select",
373 | "user-zoom",
374 | "resize",
375 | "scroll-behavior",
376 | "scroll-snap-coordinate",
377 | "scroll-snap-destination",
378 | "scroll-snap-type",
379 | "scroll-snap-type-x",
380 | "scroll-snap-type-y"
381 | ],
382 | {
383 | "unspecified": "bottomAlphabetical"
384 | }
385 | ],
386 | "at-rule-empty-line-before": [
387 | "always",
388 | {
389 | "except": [
390 | "blockless-after-same-name-blockless",
391 | "first-nested"
392 | ],
393 | "ignore": [
394 | "after-comment"
395 | ],
396 | "ignoreAtRules": [
397 | "else"
398 | ]
399 | }
400 | ],
401 | "at-rule-no-unknown": null,
402 | "at-rule-no-vendor-prefix": true,
403 | "declaration-no-important": true,
404 | "color-function-notation": null,
405 | "alpha-value-notation": null,
406 | "property-no-vendor-prefix": null,
407 | "custom-property-pattern": null,
408 | "selector-class-pattern": null,
409 | "selector-id-pattern": null,
410 | "color-named": "never",
411 | "scss/double-slash-comment-inline": null,
412 | "scss/double-slash-comment-whitespace-inside": "always",
413 | "scss/media-feature-value-dollar-variable": null,
414 | "scss/operator-no-newline-after": null,
415 | "scss/operator-no-newline-before": null,
416 | "scss/operator-no-unspaced": true,
417 | "scss/partial-no-import": null,
418 | "scss/percent-placeholder-pattern": null,
419 | "scss/selector-no-redundant-nesting-selector": true,
420 | "scss/dollar-variable-no-missing-interpolation": null,
421 | "scss/dollar-variable-pattern": null,
422 | "scss/double-slash-comment-empty-line-before": [
423 | "always",
424 | {
425 | "except": [
426 | "first-nested"
427 | ],
428 | "ignore": [
429 | "between-comments",
430 | "stylelint-commands"
431 | ]
432 | }
433 | ],
434 | "value-keyword-case": "lower",
435 | "value-no-vendor-prefix": true,
436 | "scss/at-else-closing-brace-newline-after": "always-last-in-chain",
437 | "scss/at-else-closing-brace-space-after": "always-intermediate",
438 | "scss/at-else-empty-line-before": "never",
439 | "scss/at-else-if-parentheses-space-before": "always",
440 | "scss/at-extend-no-missing-placeholder": null,
441 | "scss/at-function-parentheses-space-before": "never",
442 | "scss/at-function-pattern": null,
443 | "scss/at-if-closing-brace-newline-after": "always-last-in-chain",
444 | "scss/at-if-closing-brace-space-after": "always-intermediate",
445 | "scss/at-import-partial-extension-blacklist": null,
446 | "scss/at-mixin-argumentless-call-parentheses": "never",
447 | "scss/at-mixin-parentheses-space-before": "never",
448 | "scss/at-mixin-pattern": null,
449 | "scss/at-rule-no-unknown": true,
450 | "scss/declaration-nested-properties": "never",
451 | "scss/declaration-nested-properties-no-divided-groups": null,
452 | "scss/dollar-variable-colon-newline-after": "always-multi-line",
453 | "scss/dollar-variable-colon-space-after": "always-single-line",
454 | "scss/dollar-variable-colon-space-before": "never",
455 | "scss/dollar-variable-empty-line-before": [
456 | "always",
457 | {
458 | "except": [
459 | "after-dollar-variable",
460 | "first-nested"
461 | ],
462 | "ignore": [
463 | "after-comment",
464 | "inside-single-line-block"
465 | ]
466 | }
467 | ],
468 | "selector-attribute-quotes": "always",
469 | "selector-max-attribute": null,
470 | "selector-max-class": null,
471 | "selector-max-combinators": null,
472 | "selector-max-compound-selectors": null,
473 | "selector-max-id": 1,
474 | "selector-max-specificity": null,
475 | "selector-max-type": null,
476 | "selector-max-universal": null,
477 | "selector-nested-pattern": null,
478 | "selector-no-qualifying-type": null,
479 | "selector-no-vendor-prefix": true,
480 | "selector-pseudo-class-no-unknown": true,
481 | "selector-pseudo-element-colon-notation": "double",
482 | "selector-pseudo-element-no-unknown": true,
483 | "selector-type-case": "lower",
484 | "selector-type-no-unknown": true,
485 | "shorthand-property-no-redundant-values": true,
486 | "string-no-newline": true,
487 | "time-min-milliseconds": 10,
488 | "block-no-empty": [
489 | true,
490 | {
491 | "severity": "warning"
492 | }
493 | ],
494 | "color-hex-length": "short",
495 | "color-no-hex": null,
496 | "color-no-invalid-hex": true,
497 | "comment-empty-line-before": [
498 | "always",
499 | {
500 | "except": [
501 | "first-nested"
502 | ],
503 | "ignore": [
504 | "after-comment",
505 | "stylelint-commands"
506 | ]
507 | }
508 | ],
509 | "comment-no-empty": null,
510 | "comment-whitespace-inside": "always",
511 | "custom-media-pattern": null,
512 | "custom-property-empty-line-before": [
513 | "always",
514 | {
515 | "except": [
516 | "after-custom-property",
517 | "first-nested"
518 | ],
519 | "ignore": [
520 | "after-comment",
521 | "inside-single-line-block"
522 | ]
523 | }
524 | ],
525 | "font-family-name-quotes": "always-unless-keyword",
526 | "font-family-no-duplicate-names": true,
527 | "font-weight-notation": "numeric",
528 | "keyframe-declaration-no-important": true,
529 | "length-zero-no-unit": true,
530 | "max-nesting-depth": null,
531 | "no-descending-specificity": null,
532 | "no-duplicate-selectors": true,
533 | "no-empty-source": [
534 | true,
535 | {
536 | "severity": "warning"
537 | }
538 | ],
539 | "number-max-precision": 5,
540 | "property-no-unknown": [
541 | true,
542 | {
543 | "checkPrefixed": true
544 | }
545 | ],
546 | "rule-empty-line-before": [
547 | "always-multi-line",
548 | {
549 | "except": [
550 | "first-nested"
551 | ],
552 | "ignore": [
553 | "after-comment"
554 | ]
555 | }
556 | ]
557 | },
558 | "overrides": [{
559 | "files": ["src/scss/*/**.scss"],
560 | "customSyntax": "postcss-scss"
561 | }]
562 | }
563 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Syler.sass-indented","stylelint.vscode-stylelint","jerryhong.autofilename","EditorConfig.EditorConfig", "htmlhint.vscode-htmlhint", "ritwickdey.LiveServer", "dbaeumer.vscode-eslint", "rebornix.project-snippets"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/snippets/html.json:
--------------------------------------------------------------------------------
1 | {
2 | "svguse": {
3 | "scope": "html",
4 | "prefix": "g-use",
5 | "body": [
6 | "",
7 | " ",
8 | " "
9 | ],
10 | "description": "Snippet for use svg sprites"
11 | },
12 | "burger": {
13 | "scope": "html",
14 | "prefix": "g-burger",
15 | "body": [
16 | "",
17 | " ",
18 | " "
19 | ],
20 | "description": "Snippet for burger btn"
21 | },
22 | "checkbox": {
23 | "scope": "html",
24 | "prefix": "g-checkbox",
25 | "body": [
26 | "",
27 | " ",
28 | " $1 ",
29 | " "
30 | ],
31 | "description": "Snippet for custom checkbox"
32 | },
33 | "navigation": {
34 | "scope": "html",
35 | "prefix": "g-nav",
36 | "body": [
37 | "",
38 | " ",
39 | " ",
40 | " ",
41 | " ",
42 | " ",
43 | " "
44 | ],
45 | "description": "Snippet for navigation"
46 | },
47 | "social": {
48 | "scope": "html",
49 | "prefix": "g-social",
50 | "body": [
51 | "
",
52 | " ",
53 | " ",
54 | " ",
55 | " "
56 | ],
57 | "description": "Snippet for social links"
58 | },
59 | "basicform": {
60 | "prefix": "g-form",
61 | "body": [
62 | ""
68 | ],
69 | "description": "Snippet for basic form"
70 | },
71 | "basicswiper": {
72 | "prefix": "g-swiper",
73 | "body": [
74 | "",
75 | "
",
76 | "
",
77 | "
",
78 | "
",
79 | "
"
80 | ],
81 | "description": "Snippet for basic swiper structure"
82 | },
83 | "graph modal Btn": {
84 | "prefix": "g-graph-btn",
85 | "body": [
86 | "$3 "
87 | ],
88 | "description": "Snippet for graph-modal btn"
89 | },
90 | "graph modal Structure": {
91 | "prefix": "g-graph-modal",
92 | "body": [
93 | "",
94 | "
",
95 | "
",
96 | "
$2
",
97 | "
",
98 | "
"
99 | ],
100 | "description": "Snippet for graph-modal basic structure"
101 | },
102 | "picture": {
103 | "prefix": "g-picture",
104 | "body": [
105 | "",
106 | " ",
107 | " ",
108 | " "
109 | ],
110 | "description": "Snippet for basic logo structure"
111 | },
112 | "svgimg": {
113 | "scope": "html",
114 | "prefix": "g-svgimg",
115 | "body": [
116 | " "
117 | ],
118 | "description": "Snippet for simple html svg images"
119 | },
120 | "tooltip": {
121 | "prefix": "g-tooltip",
122 | "body": [
123 | "",
124 | " $2 ",
125 | " ",
126 | " Тултип: ",
127 | " $3",
128 | " ",
129 | " "
130 | ],
131 | "description": "Snippet for tooltip"
132 | },
133 | "tabs html sctructure": {
134 | "prefix": "g-tabs",
135 | "body": [
136 | "",
137 | "
",
138 | " $2 ",
139 | " $3 ",
140 | " $4 ",
141 | " ",
142 | "
",
143 | "
$5
",
144 | "
$6
",
145 | "
$7
",
146 | "
",
147 | "
"
148 | ],
149 | "description": "Snippet for basic tabs structure"
150 | },
151 | "transparent video (need special video-file)": {
152 | "prefix": "g-tr-video",
153 | "body": [
154 | "",
155 | " ",
156 | " ",
157 | " "
158 | ],
159 | "description": "Snippet for transparent video"
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gulp — сборка MaxGraph
2 |
3 | > Используется Gulp 5. Тестировалось на node.js 20.12.2
4 |
5 | ## Начало работы
6 |
7 | Для работы с данной сборкой в новом проекте, склонируйте все содержимое репозитория
8 | `git clone `
9 | Затем, находясь в корне проекта, запустите команду `npm i`, которая установит все находящиеся в package.json зависимости.
10 | После этого вы можете использовать любую из предложенных команд сборки (подробнее - ниже, в разделе __npm-скрипты__).
11 |
12 | ## Структура папок и файлов
13 |
14 | ```
15 | ├── gulp/ # Все настройки Gulp-сборки, разделенные по отдельным файлам
16 | ├── src/ # Исходники
17 | │ ├── js # Скрипты
18 | │ │ └── main.js # Главный скрипт
19 | │ │ ├── _vars.js # файл с переменными проекта
20 | │ │ ├── _functions.js # файл с готовыми функциями на js
21 | │ │ ├── _components.js # файл с подключениями компонентов
22 | │ │ ├── components # js-компоненты
23 | │ ├── scss # Стили сайта (препроцессор sass в scss-синтаксисе)
24 | │ │ └── main.scss # Главный файл стилей
25 | │ │ └── vendor.scss # Файл для подключения стилей библиотек из папки vendor
26 | │ │ └── _fonts.scss # Файл для подключения шрифтов (можно использовать миксин)
27 | │ │ └── _mixins.scss # Файл для подключения миксинов из папки mixins
28 | │ │ └── _vars.scss # Файл для написания css- или scss-переменных
29 | │ │ └── _settings.scss # Файл для написания глобальных стилей
30 | │ │ ├── components # scss-компоненты
31 | │ │ ├── mixins # папка для сохранения готовых scss-компонентов
32 | │ │ ├── vendor # папка для хранения локальных css-стилей библиотек
33 | │ ├── partials # папка для хранения html-частей страницы
34 | │ ├── img # папка для хранения картинок
35 | │ │ ├── svg # специальная папка для преобразования svg в спрайт
36 | │ ├── resources # папка для хранения иных ассетов - php, видео-файлы, favicon и т.д.
37 | │ │ ├── fonts # папка для хранения шрифтов в формате woff2
38 | │ └── index.html # Главный html-файл
39 | └── gulpfile.js # Результирующий файл с настройками Gulp
40 | └── package.json # файл с настройками сборки и установленными пакетами
41 | └── .editorconfig # файл с настройками форматирования кода
42 | └── .ecrc # файл с настройками пакета editorconfig-checker (исключает ненужные папки)
43 | └── .stylelintrc.json # файл с настройками stylelint
44 | └── .stylelintignore # файл с исключениями для stylelint
45 | └── .htmlhintrc # файл с настройками htmlhint
46 | └── README.md # документация сборки
47 | ```
48 |
49 | ## Оглавление
50 | 1. [npm-скрипты](#npm-скрипты)
51 | 2. [Работа с html](#работа-с-html)
52 | 3. [Работа с CSS](#работа-с-css)
53 | 4. [Работа с JavaScript](#работа-с-javascript)
54 | 5. [Работа со шрифтами](#работа-со-шрифтами)
55 | 6. [Работа с изображениями](#работа-с-изображениями)
56 | 7. [Работа с иными ресурсами](#работа-с-иными-ресурсами)
57 | 8. [Типограф](#типограф)
58 | 9. [Рекомендуемые плагины VS Code](#рекомендуемые-плагины-для-vs-code)
59 | 10. [Локальные сниппеты](#локальные-сниппеты)
60 | 11. [Готовые модули](#готовые-модули)
61 | 12. [Изменения в версии 3.0.0 (от 21.04.2024)](#изменения-в-версии-300-от-21042024)
62 | 13. [Заключение](#заключение)
63 |
64 | ## npm-скрипты
65 |
66 | Вызывать различные Gulp-задачи нужно __только__ через npm-команды, т.к. обычные Gulp-команды работают неполноценно.
67 |
68 | * `npm run stylelint` — команда, запускающая проверку всех scss-файлов на соответствие stylelint.
69 | * `npm run style-fix` — проверка и одновременный фикс scss-файлов на соответствие stylelint. Лично я сам все исправляю вручную, боясь, что автофикс что-то сломает.
70 | * `npm run code` — команда, запускающая проверку всех файлов на соответствие editorconfig.
71 | * `npm run dev` — базовая команда, которая запускает Gulp в режиме разработки.
72 | * `npm run build` — команда, запускающая продакшн-версию сборки. В эту команду также включена проверка stylelint и editorconfig, и если файлы не соответсвуют правилам - ваш проект не соберется.
73 | * `npm run cache` — команда, которую стоит запускать после npm run build__, если вам нужно загрузить новые файлы на хостинг без кэширования.
74 | * `npm run backend` — команда для бэкенд-сборки проекта. Она лишена ненужных вещей из dev-сборки, но не сжата, для удобства бэкендера.
75 | * `npm run zip` — команда собирает ваш готовый код в zip-архив.
76 |
77 | ## Работа с html
78 |
79 | Благодаря плагину __gulp-file-include__ вы можете разделять html-файл на различные шаблоны, которые должны храниться в папке __partials__. Удобно делить html-страницу на секции.
80 |
81 | > Для вставки html-частей в главный файл используйте `@include('partials/filename.html')`
82 |
83 | Если вы хотите создать многостраничный сайт - копируйте __index.html__, переименовывайте как вам нужно, и используйте.
84 |
85 | При использовании команды `npm run build`, вы получите минифицированный html-код в одну строку для всех html-файлов.
86 |
87 | ## Работа с CSS
88 |
89 | В сборке используется препроцессор __sass__ в синтаксисе __scss__.
90 |
91 | Стили, написанные в __components__, следует подключать в __main.scss__.
92 | __ВАЖНО:__ Обязательно удалить стили, которые написаны в __main.scss__ для `.page__body`.
93 |
94 | Чтобы подключить сторонние css-файлы (библиотеки) - положите их в папку __vendor__ и подключите в файле ___vendor.scss__
95 |
96 | Если вы хотите создать свой миксин - делайте это в папке __mixins__, а затем подключайте в файл ___mixins.scss__.
97 |
98 | Если вы хотите использовать scss-переменные - подключите ___vars.scss__ также в main.scss или в любое другое место, где он нужен, но обязательно удалите __:root__.
99 |
100 | > Для подключения css-файлов используйте директиву `@import`
101 |
102 | В итоговой папке __app/css__ создаются два файла: __main.css__ - для стилей страницы, __vendor.css__ - для стилей всех библиотек, использующихся в проекте.
103 |
104 | При использовании команды `npm run build`, вы получите минифицированный css-код в одну строку для всех css-файлов.
105 |
106 | ## Работа с JavaScript
107 |
108 | Для сборки JS-кода используется webpack.
109 |
110 | JS-код лучше делить на компоненты - небольшие js-файлы, которые содержат свою, изолированную друг от друга реализацию. Такие файлы помещайте в папку __components__, а потом импортируйте в файл ___components.js__
111 |
112 | В файле __vars.js__ должны храниться базовые переменные проекта, вроде нахождения элементов и т.д.
113 |
114 | В файле __main.js__ ничего менять не нужно, он сделан просто как результирующий.
115 |
116 | Подключать библиотеки в сборку можно только с помощью npm. Для этого установите нужный пакет с помощью его команды, создайте в папке __components__ файлик, импортируйте туда библиотеку и работайте с ней. Не забудьте импортировать этот файл в файл ___components.js__.
117 |
118 | При использовании команды `npm run build`, вы получите минифицированный js-код в одну строку для всех js-файлов.
119 |
120 | ## Работа со шрифтами
121 |
122 | В сборке реализована поддержка только формата __woff2__, т.к. другие форматы шрифтов не актуальны (это значит, что в миксине подключения шрифтов используется только данный формат).
123 |
124 | Загружайте файлы __woff2__ в папку __resources/fonts__, а затем вызывайте миксин `@font-face` в файле ___fonts.scss__.
125 |
126 | Также не забудьте прописать эти же шрифты в ` ` в html.
127 |
128 | ## Работа с изображениями
129 |
130 | Любые изображения, кроме __favicon__ кладите в папку __img__.
131 |
132 | Если вам нужно сделать svg-спрайт, кладите нужные для спрайта svg-файлы в папку __img/svg__. При этом, такие атрибуты как fill, stroke, style будут автоматически удаляться. Иные svg-файлы просто оставляйте в папке __img__.
133 |
134 | При использовании команды `npm run build`, вы получите минифицированные изображения в итоговой папке __img__.
135 |
136 | В сборке доступна поддержка __webp__ формата. Подключить его вы можете через тег `picture`. Для background можно использовать обычные __jpg__ или __png__, либо использовать `image-set` там, где это возможно.
137 |
138 | ## Работа с иными ресурсами
139 |
140 | Любые ресурсы (ассеты) проекта, под которые не отведена соответствующая папка, должны храниться в папке __resources__. Это могут быть видео-файлы, php-файлы (как, например, файл отправки формы), favicon и прочие.
141 |
142 | ## Типограф
143 |
144 | Для корректного отображения текста на странице был подключен плагин типограф, которые автоматически добавит неразрывные пробелы и иные символы, чтобы текст везде отображался по всем правилам русского языка.
145 |
146 | ## Рекомендуемые плагины для VS Code
147 |
148 | Я рекомендую использовать именно VS Code, и в сборке реализовано взаимодействие именно с этим редактором. Так, при открытии папки со сборкой в VS Code, редактор предложит вам ранее не установленные плагины, которые подойдут для корректной работы сборки.
149 |
150 | Самый важный из них – __projects snippets__, этот плагин "включает" локально написанные сниппеты для сборки. Данный плагин не всегда работает корректно, в этом случае просто перезапустите VS Code.
151 |
152 | ## Локальные сниппеты
153 |
154 | Для удобства и быстроты разработки были добавление локальные сниппеты (находятся в папке .vscode/snippets), которые работают благодаря плагину, описанному выше. Все сниппеты начинаются с префикса __g-__. В сниппетах пока только html (быстрое создание навигации, соцсетей, корректного тега picture с webp и avif и так далее).
155 |
156 | Некоторые сниппеты тесно связаны с scss-миксинами, например кнопка-бургер. Сниппет __g-burger__ создаст вам html-разметку бургера, а подключение миксина __@include burger__ добавит для него стили, что крайне удобно.
157 |
158 | ## Готовые модули
159 |
160 | В сборке присутствуют готовые, часто-используемые модули под различные задачи. Ниже будет перечислен уже добавленный функционал.
161 |
162 | __Внимание!__ В файле _functions.js_ описаны лишь подключения всех нужных модулей. Рекомендуется использовать все это в отдельных файлах. Например, если вам нужно создать модальное окно, создаете файл _modal.js_ в папке components, подключаете его в файл components.js и уже в файле _modal.js_ используете код подключения.
163 |
164 | ### Бургер меню
165 |
166 | Вы можете очень быстро добавить рабочий бургер к себе на страницу, для этого нужно:
167 |
168 | 1. В html вызвать сниппет `g-burger`
169 | 2. На ваше потенциальное меню в html добавить атрибут `data-menu`
170 | 3. В scss вызвать миксин `burger`
171 |
172 | ```scss
173 | .burger { @include burger }
174 | ```
175 |
176 | 4. Зайти в файл js/_functions.js и скопировать строку с подключением js-файла бургера, после подключить в вашем файле для бургера.
177 | 5. Настроить стили показа меню под себя с помощью класса `menu--active`
178 |
179 | ### Модальное окно
180 |
181 | Вы можете очень быстро добавить рабочее модальное окно к себе на страницу, для этого нужно:
182 |
183 | 1. В html вызвать сниппет `g-graph-btn`. Он создаст кнопку для модального окна, ваша задача лишь заполнить атрибут `data-graph-path`
184 | 2. Далее вызвать сниппет `g-graph-modal`. Он создаст базовую разметку окна. Ваша задача - сделать окно по макету, заполнить контент и обязательно обозначить атрибут `data-graph-target` с тем же значением, что и у `data-graph-path`
185 | 3. Зайти в файл vendor.scss и раскомментировать строку с подключением файла graph-modal.min.css
186 | 4. Зайти в файл js/_functions.js и скопировать строку с импортом и подключением библиотеки `GraphModal`, после подключить в вашем файле для модального окна.
187 |
188 | ### Управление скроллом
189 |
190 | Вы можете отключать\включать скролл на странице (работает даже на iPhone). Для этого нужно:
191 |
192 | 1. Зайти в файл js/_functions.js и скопировать строку с импортом функций `disableScroll`, `enableScroll`. После подключить в вашем файле для отключения/включения скролла.
193 | __Важно!__. Если на странице присутствуют блоки с фиксированным позиционированием (например, шапка), добавьте ей класс `fixed-block`, чтобы этот блок не прыгал при отключении скролла.
194 |
195 | ### Табы
196 |
197 | Вы можете очень быстро добавить рабочие табы к себе на страницу, для этого нужно:
198 |
199 | 1. В html вызвать сниппет `g-tabs`. Он создаст разметку для табов, ваша задача лишь заполнить атрибут `data-tabs`
200 | 2. Для класса `.tabs` вызвать миксин `tabs` в scss (или же использовать подключение скрипта библиотеки из npm в файле vendor.scss)
201 | 4. Зайти в файл js/_functions.js и скопировать строку с импортом и подключением библиотеки `GraphTabs`, после подключить в вашем файле для табов.
202 |
203 | ### Валидация и отправка данных на почту
204 |
205 | Вы можете быстро настроить валидацию и отправку данных на почту. Как это использовать:
206 |
207 | 1. Создать форму, указав у нее уникальный класс. Также указать уникальные классы для полей ввода.
208 | 2. Создать массив, в котором будут переданы правила плагина just-validate , например:
209 | ```js
210 | const rules1 = [
211 | {
212 | ruleSelector: '.input-name',
213 | rules: [
214 | {
215 | rule: 'minLength',
216 | value: 3
217 | },
218 | {
219 | rule: 'required',
220 | value: true,
221 | errorMessage: 'Заполните имя!'
222 | }
223 | ]
224 | },
225 | {
226 | ruleSelector: '.input-tel',
227 | tel: true,
228 | telError: 'Введите корректный телефон',
229 | rules: [
230 | {
231 | rule: 'required',
232 | value: true,
233 | errorMessage: 'Заполните телефон!'
234 | }
235 | ]
236 | },
237 | ];
238 | ```
239 | __ВАЖНО__. Если в вашей форме есть поле с телефоном, обязательно пропишите в массиве с правилами новые поля: `tel: true, telError: 'Ошибка при вводе телефона'`.
240 | 3. Подключить функцию `validateForms`, она находится в _functions.js_, передав туда три параметра:
241 | 3.1. Строку с классом формы
242 | 3.2. Массив правил
243 | 3.3. Если нужно, можно создать свою функцию, которая выполнится после отправки, тогда ее тоже нужно будет передать как аргумент функции `validateForms`.
244 | 4. Также эта функция поддерживает работы с множественными чекбоксами/радиокнопками. Третьим параметром в функцию можно передать массив с настройками:
245 | ```js
246 | const checks = [
247 | {
248 | selector: ".checkbox-group",
249 | errorMessage: "Выберите чекбоксы",
250 | }
251 | ];
252 | ```
253 | Пример кода:
254 | ```js
255 | import { validateForms } from './functions/validate-forms';
256 | const checks = [
257 | {
258 | selector: ".checkbox-group",
259 | errorMessage: "Выберите чекбоксы",
260 | }
261 | ];
262 | const rules1 = [
263 | {
264 | ruleSelector: '.input-name',
265 | rules: [
266 | {
267 | rule: 'minLength',
268 | value: 3
269 | },
270 | {
271 | rule: 'required',
272 | value: true,
273 | errorMessage: 'Заполните имя!'
274 | }
275 | ]
276 | },
277 | {
278 | ruleSelector: '.input-tel',
279 | tel: true,
280 | telError: 'Введите корректный телефон',
281 | rules: [
282 | {
283 | rule: 'required',
284 | value: true,
285 | errorMessage: 'Заполните телефон!'
286 | }
287 | ]
288 | },
289 | ];
290 |
291 | const afterForm = () => {
292 | console.log('Произошла отправка, тут можно писать любые действия');
293 | };
294 |
295 | validateForms('.form-1', rules1, checks, afterForm);
296 | ```
297 |
298 | ### Throttle-функция
299 |
300 | Чтобы сгладить управление частоиспользуемыми событиями, вы можете использовать готовую функцию __throttle__. Для этого нужно:
301 |
302 | 1. В нужном месте импортировать функцию __throttle()__
303 | 2. Написать нужную вам функцию, например, __func()__
304 | 3. Создать переменную, в которую поместить вызов вашей фукнции внутри throttle, например: `let f = throttle(func)`
305 | 4. Использовать эту переменную как функцию в вызове, например: `window.addEventListener('resize', f)`
306 |
307 | ### Фикс фулскрин блоков
308 |
309 | Нередко блоки с высотой 100vh вызывают проблемы в мобильных браузерах. Решить это поможет готовый модуль fix-fullheight:
310 |
311 | 1. Раскомментируйте строку с импортом файла __fix-fullheight.js__
312 | 2. Назначьте на нужный вам блок высоту не 100vh, а `var(--vh)`
313 |
314 | Для этой функции используется ранее упомянутый throttle. Вы можете убрать его, либо изменить время внутри файла __fix-fullheight.js__.
315 |
316 | ### Получение высоты шапки
317 |
318 | Иногда требуется получить точную высоту шапки, если она сделана абсолютным или фиксированным позиционированием, и для этого есть функция `getHeaderHeght`. Как ее использовать:
319 |
320 | 1. Раскомментируйте строку с импортом файла __header-height.js__
321 | 2. Используйте css-переменную `--header-height` в нужном вам месте
322 |
323 | Необязательно использовать функции именно в файле __functions__, делайте как удобно вам.
324 |
325 | ### Кастомный скролл
326 |
327 | Для реализации кастомного скролла в сборку установлен плагин __simplebar.js__. Как его использовать:
328 |
329 | 1. Раскомментируйте строку с импортом плагина __simplebar__
330 | 2. Добавьте нужному блоку максимальную высоту и атрибут `data-simplebar`
331 |
332 | ### Функции определения вьюпорта
333 |
334 | Вы можете запускать скрипты на определенной ширине (пока что поддержка ресайза не реализована) с помощью готовых функций `isMobile()`, `isTablet()`, `isDesktop()`. Для этого нужно лишь подключить нужную из них из файла, а затем использовать внутри условия `if`.
335 |
336 | ### Тултипы
337 |
338 | Вы можете быстро создать рабочий, доступный тултип, который к тому же будет сам рассчитывать отступы с помощью js. Как это использовать:
339 |
340 | 1. В html вызвать сниппет `g-tooltip`. Он создаст кнопку для модального окна, ваша задача лишь заполнить атрибуты `aria-describedby` и `id`.
341 | 2. Далее нужно подключить тултипы (код в файле _functions.js_), и вместо el передать id или class кнопки тултипа, а вместо tooltip передать id или class самого тултипа.
342 | 3. После этого можете стилизовать тултип как вам угодно.
343 |
344 | ### Слайдер
345 |
346 | Вы можете быстро создать рабочий swiper-слайдер. Как это использовать:
347 |
348 | 1. В html вызвать сниппет `g-swiper`. Он создаст базовую структуру свайпер-слайдера, вам нужно добавить свой класс для свайпер-контейнера.
349 | 2. Раскомментировать строку с подключением стилей в файле _vendor.scss_
350 | 3. Подключить сам свайпер (код в файле _functions.js_) и использовать его, следуя документации.
351 |
352 | ### Анимации по скроллу
353 |
354 | Вы можете быстро набросать анимаций по скроллу с помощью плагина. Как это использовать:
355 |
356 | 1. Подключить код библиотеки AOS.js (код в файле _functions.js_) и инициализировать его.
357 | 2. С помощью атрибутов из документации плагина вызывать те или иные анимации, или написать свою.
358 |
359 | ### Параллакс по скроллу
360 |
361 | Вы можете быстро набросать параллакс элементов по скроллу с помощью плагина. Как это использовать:
362 |
363 | 1. Подключить код библиотеки rellax.js (код в файле _functions.js_) и инициализировать его, передав класс элемента (элементов).
364 | 2. Задать этот класс нужным элементам, а также использовать атрибуты из документации для кастомизации анимаций.
365 |
366 | ### Свайпы на мобильных устройствах
367 |
368 | Вы можете реализовывать различные взаимодействия со страницей через свайпы на мобильных устройствах с помощью плагина. Как это использовать:
369 |
370 | 1. Подключить код библиотеки swiped-events.js (код в файле _functions.js_).
371 | 2. Использовать различные события из библиотеки плагина.
372 |
373 | ### Миксин для flex-расстановки элементов.
374 |
375 | В последней версии сборки я добавил миксин flex-layout (можно найти в папке mixins), в котором реализована типичная сетка для карточек. Вы можете выбирать нужные вам настройки, чтобы сделать сетку быстро и без проблем. Например:
376 |
377 | ```html
378 |
379 |
Текст
380 |
Текст
381 |
Текст
382 |
Текст
383 |
Текст
384 |
Текст
385 |
386 |
387 | $options: (
388 | parentClass: "cards",
389 | itemsClass: "cards__item",
390 | desktopGap: 30px,
391 | desktopElems: 3,
392 | tablet: "1024px",
393 | tabletElems: 2,
394 | tabletGap: 30px,
395 | mobile: "600px",
396 | mobileElems: 1,
397 | mobileGap: 20px
398 | );
399 |
400 | @include flex-layout($options);
401 | ```
402 |
403 | В опциях можно выбрать класс-родитель (или же флекс-контейнер, класс для потомков, какой у них будет отступ, на каких разрешениях будет меняться кол-во элементов).
404 |
405 | ## Изменения в версии 3.0.0 (от 21.04.2024)
406 |
407 | 1. Версия Gulp изменена на пятую.
408 | 2. Обновлены все нужные для работы пакеты.
409 | 3. Теперь сборка разделена на отдельные файлы, которые хранятся в папке __gulp__.
410 | 4. Удалены некоторые пакеты, такие как __smooth-scroll__ или __js-focus-visible__, т.к. уже не нужны в 2024 году.
411 | 5. Обновлен конфиг-файл __stylelint__, т.к. старый не работал с новой версией.
412 | 6. Немного обновлены базовые стили сборки:
413 | 6.1. min-width по умолчанию теперь 360, т.к. телефонов меньше почти нет.
414 | 6.2. __box-sizing: border-box__ задан глобально (без inherit), т.к. за все время использования сборки понял, что это лишено смысла.
415 | 6.3. по умолчанию у .page добавлено свойство __scroll-behavior__.
416 | 7. Немного обновлены скрипты и модули:
417 | 7.1. Изменился метод подключения __swiper__.
418 | 7.2. Изменился метод подключения __Inputmask__.
419 | 7.3. Изменилась функция валидации.
420 | 8. Все команды сборки теперь должны запускаться только через npm-команды.
421 | 9. Прочие мелочи.
422 |
423 | Тестировал сборку на своих рабочих проектах, все запускалось без проблем.
424 |
425 | ## Заключение
426 |
427 | Спасибо всем, кто использует сборку! Если вы заметили какую-либо ошибку, пришлите пожалуйста issue с подробным описанием проблемы, я все смотрю и постараюсь решить. Спасибо!
428 |
--------------------------------------------------------------------------------
/gulp/config/paths.js:
--------------------------------------------------------------------------------
1 | const srcFolder = './src';
2 | const buildFolder = './app';
3 |
4 | export const paths = {
5 | base: {
6 | src: srcFolder,
7 | build: buildFolder,
8 | },
9 | srcSvg: `${srcFolder}/img/svg/**.svg`,
10 | srcImgFolder: `${srcFolder}/img`,
11 | buildImgFolder: `${buildFolder}/img`,
12 | srcScss: `${srcFolder}/scss/**/*.scss`,
13 | buildCssFolder: `${buildFolder}/css`,
14 | srcFullJs: `${srcFolder}/js/**/*.js`,
15 | srcMainJs: `${srcFolder}/js/main.js`,
16 | buildJsFolder: `${buildFolder}/js`,
17 | srcPartialsFolder: `${srcFolder}/partials`,
18 | resourcesFolder: `${srcFolder}/resources`,
19 | };
20 |
--------------------------------------------------------------------------------
/gulp/tasks/cache.js:
--------------------------------------------------------------------------------
1 | import rev from "gulp-rev";
2 | import revDel from "gulp-rev-delete-original";
3 |
4 | export const cacheTask = () => {
5 | return app.gulp.src(`${app.paths.base.build}/**/*.{css,js,svg,png,jpg,jpeg,webp,woff2}`, {
6 | base: app.paths.base.build,
7 | encoding: false,
8 | })
9 | .pipe(rev())
10 | .pipe(revDel())
11 | .pipe(app.gulp.dest(app.paths.base.build))
12 | .pipe(rev.manifest('rev.json'))
13 | .pipe(app.gulp.dest(app.paths.base.build));
14 | };
15 |
--------------------------------------------------------------------------------
/gulp/tasks/clean.js:
--------------------------------------------------------------------------------
1 | import { deleteAsync } from 'del';
2 |
3 | export const clean = () => {
4 | return deleteAsync(app.paths.base.build);
5 | }
6 |
--------------------------------------------------------------------------------
/gulp/tasks/html-include.js:
--------------------------------------------------------------------------------
1 | import browserSync from 'browser-sync';
2 | import fileInclude from "gulp-file-include";
3 | import typograf from "gulp-typograf";
4 |
5 | export const htmlInclude = () => {
6 | return app.gulp.src([`${app.paths.base.src}/*.html`])
7 | .pipe(fileInclude({
8 | prefix: '@',
9 | basepath: '@file',
10 | maxRecursion: 100
11 | }))
12 | .pipe(typograf({
13 | locale: ['ru', 'en-US']
14 | }))
15 | .pipe(app.gulp.dest(app.paths.base.build))
16 | .pipe(browserSync.stream());
17 | }
18 |
--------------------------------------------------------------------------------
/gulp/tasks/html-minify.js:
--------------------------------------------------------------------------------
1 | import htmlmin from "gulp-htmlmin";
2 |
3 | export const htmlMinify = () => {
4 | return app.gulp.src(`${app.paths.base.build}/**/*.html`)
5 | .pipe(htmlmin({
6 | collapseWhitespace: true
7 | }))
8 | .pipe(app.gulp.dest(app.paths.base.build));
9 | }
10 |
--------------------------------------------------------------------------------
/gulp/tasks/images.js:
--------------------------------------------------------------------------------
1 | import gulpif from 'gulp-if';
2 | import imagemin, { gifsicle, mozjpeg, optipng, svgo } from 'gulp-imagemin';
3 | import newer from "gulp-newer";
4 |
5 | export const images = () => {
6 | return app.gulp.src([`${app.paths.srcImgFolder}/**/**.{jpg,jpeg,png,svg}`], { encoding: false })
7 | // .pipe(newer(app.paths.buildImgFolder))
8 | // .pipe(gulpif(app.isProd, imagemin([
9 | // gifsicle({ interlaced: true }),
10 | // mozjpeg({ quality: 75, progressive: true }),
11 | // optipng({ optimizationLevel: 2 }),
12 | // ])))
13 | .pipe(app.gulp.dest(app.paths.buildImgFolder))
14 | };
15 |
--------------------------------------------------------------------------------
/gulp/tasks/resources.js:
--------------------------------------------------------------------------------
1 | export const resources = () => {
2 | return app.gulp.src(`${app.paths.resourcesFolder}/**`, { encoding: false })
3 | .pipe(app.gulp.dest(app.paths.base.build))
4 | }
5 |
--------------------------------------------------------------------------------
/gulp/tasks/rewrite.js:
--------------------------------------------------------------------------------
1 | import revRewrite from "gulp-rev-rewrite";
2 | import { readFileSync } from "fs";
3 |
4 | export const rewrite = () => {
5 | const manifest = readFileSync('app/rev.json');
6 |
7 | app.gulp.src(`${app.paths.buildCssFolder}/*.css`)
8 | .pipe(revRewrite({
9 | manifest
10 | }))
11 | .pipe(app.gulp.dest(app.paths.buildCssFolder));
12 | return app.gulp.src(`${app.paths.base.build}/**/*.html`)
13 | .pipe(revRewrite({
14 | manifest
15 | }))
16 | .pipe(app.gulp.dest(app.paths.base.build));
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/gulp/tasks/scripts-backend.js:
--------------------------------------------------------------------------------
1 | import browserSync from 'browser-sync';
2 | import webpackStream from 'webpack-stream';
3 | import plumber from 'gulp-plumber';
4 | import notify from 'gulp-notify';
5 |
6 | export const scriptsBackend = () => {
7 | return app.gulp.src(app.paths.srcMainJs)
8 | .pipe(plumber(
9 | notify.onError({
10 | title: "JS",
11 | message: "Error: <%= error.message %>"
12 | })
13 | ))
14 | .pipe(webpackStream({
15 | mode: 'development',
16 | output: {
17 | filename: 'main.js',
18 | },
19 | module: {
20 | rules: [{
21 | test: /\.m?js$/,
22 | exclude: /node_modules/,
23 | use: {
24 | loader: 'babel-loader',
25 | options: {
26 | presets: [
27 | ['@babel/preset-env', {
28 | targets: "defaults"
29 | }]
30 | ]
31 | }
32 | }
33 | }]
34 | },
35 | devtool: false
36 | }))
37 | .on('error', function (err) {
38 | console.error('WEBPACK ERROR', err);
39 | this.emit('end');
40 | })
41 | .pipe(app.gulp.dest(app.paths.buildJsFolder))
42 | .pipe(browserSync.stream());
43 | }
44 |
--------------------------------------------------------------------------------
/gulp/tasks/scripts.js:
--------------------------------------------------------------------------------
1 | import browserSync from 'browser-sync';
2 | import webpackStream from 'webpack-stream';
3 | import plumber from 'gulp-plumber';
4 | import notify from 'gulp-notify';
5 |
6 | export const scripts = () => {
7 | return app.gulp.src(app.paths.srcMainJs)
8 | .pipe(plumber(
9 | notify.onError({
10 | title: "JS",
11 | message: "Error: <%= error.message %>"
12 | })
13 | ))
14 | .pipe(webpackStream({
15 | mode: app.isProd ? 'production' : 'development',
16 | output: {
17 | filename: 'main.js',
18 | },
19 | module: {
20 | rules: [{
21 | test: /\.m?js$/,
22 | exclude: /node_modules/,
23 | use: {
24 | loader: 'babel-loader',
25 | options: {
26 | presets: [
27 | ['@babel/preset-env', {
28 | targets: "defaults"
29 | }]
30 | ]
31 | }
32 | }
33 | }]
34 | },
35 | devtool: !app.isProd ? 'source-map' : false
36 | }))
37 | .on('error', function (err) {
38 | console.error('WEBPACK ERROR', err);
39 | this.emit('end');
40 | })
41 | .pipe(app.gulp.dest(app.paths.buildJsFolder))
42 | .pipe(browserSync.stream());
43 | }
44 |
--------------------------------------------------------------------------------
/gulp/tasks/sprite.js:
--------------------------------------------------------------------------------
1 | import svgSprite from "gulp-svg-sprite";
2 | import svgmin from "gulp-svgmin";
3 | import cheerio from 'gulp-cheerio';
4 | import replace from 'gulp-replace';
5 |
6 | export const svgSprites = () => {
7 | return app.gulp.src(app.paths.srcSvg, { encoding: false })
8 | .pipe(
9 | svgmin({
10 | js2svg: {
11 | pretty: true,
12 | },
13 | })
14 | )
15 | .pipe(
16 | cheerio({
17 | run: function ($) {
18 | $('[fill]').removeAttr('fill');
19 | $('[stroke]').removeAttr('stroke');
20 | $('[style]').removeAttr('style');
21 | },
22 | parserOptions: {
23 | xmlMode: true
24 | },
25 | })
26 | )
27 | .pipe(replace('>', '>'))
28 | .pipe(svgSprite({
29 | mode: {
30 | stack: {
31 | sprite: "../sprite.svg"
32 | }
33 | },
34 | }))
35 | .pipe(app.gulp.dest(app.paths.buildImgFolder));
36 | }
37 |
--------------------------------------------------------------------------------
/gulp/tasks/styles-backend.js:
--------------------------------------------------------------------------------
1 | import browserSync from 'browser-sync';
2 | import * as dartSass from 'sass';
3 | import gulpSass from 'gulp-sass';
4 | import plumber from 'gulp-plumber';
5 | import autoprefixer from 'gulp-autoprefixer';
6 | import notify from 'gulp-notify';
7 |
8 | const sass = gulpSass(dartSass);
9 |
10 | export const stylesBackend = () => {
11 | return app.gulp.src(app.paths.srcScss)
12 | .pipe(plumber(
13 | notify.onError({
14 | title: "SCSS",
15 | message: "Error: <%= error.message %>"
16 | })
17 | ))
18 | .pipe(sass())
19 | .pipe(autoprefixer({
20 | cascade: false,
21 | grid: true,
22 | overrideBrowserslist: ["last 5 versions"]
23 | }))
24 | .pipe(app.gulp.dest(app.paths.buildCssFolder))
25 | .pipe(browserSync.stream());
26 | };
27 |
--------------------------------------------------------------------------------
/gulp/tasks/styles.js:
--------------------------------------------------------------------------------
1 | import gulpif from 'gulp-if';
2 | import browserSync from 'browser-sync';
3 | import cleanCSS from 'gulp-clean-css';
4 | import * as dartSass from 'sass';
5 | import gulpSass from 'gulp-sass';
6 | import plumber from 'gulp-plumber';
7 | import autoprefixer from 'gulp-autoprefixer';
8 | import notify from 'gulp-notify';
9 |
10 | const sass = gulpSass(dartSass);
11 |
12 | export const styles = () => {
13 | return app.gulp.src(app.paths.srcScss, { sourcemaps: !app.isProd })
14 | .pipe(plumber(
15 | notify.onError({
16 | title: "SCSS",
17 | message: "Error: <%= error.message %>"
18 | })
19 | ))
20 | .pipe(sass())
21 | .pipe(autoprefixer({
22 | cascade: false,
23 | grid: true,
24 | overrideBrowserslist: ["last 5 versions"]
25 | }))
26 | .pipe(gulpif(app.isProd, cleanCSS({
27 | level: 2
28 | })))
29 | .pipe(app.gulp.dest(app.paths.buildCssFolder, { sourcemaps: '.' }))
30 | .pipe(browserSync.stream());
31 | };
32 |
--------------------------------------------------------------------------------
/gulp/tasks/webp.js:
--------------------------------------------------------------------------------
1 | import webp from 'gulp-webp';
2 |
3 | export const webpImages = () => {
4 | return app.gulp.src([`${app.paths.srcImgFolder}/**/**.{jpg,jpeg,png}`], { encoding: false })
5 | .pipe(webp())
6 | .pipe(app.gulp.dest(app.paths.buildImgFolder))
7 | };
8 |
--------------------------------------------------------------------------------
/gulp/tasks/zip.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import zip from 'gulp-zip';
3 | import { deleteAsync } from 'del';
4 | import notify from 'gulp-notify';
5 | import plumber from 'gulp-plumber';
6 |
7 | const rootFolder = path.basename(path.resolve());
8 |
9 | export const zipFiles = () => {
10 | deleteAsync([`${app.paths.base.build}/*.zip`]);
11 | return app.gulp.src(`${app.paths.base.build}/**/*.*`, { encoding: false })
12 | .pipe(plumber(
13 | notify.onError({
14 | title: "ZIP",
15 | message: "Error: <%= error.message %>"
16 | })
17 | ))
18 | .pipe(zip(`${rootFolder}.zip`))
19 | .pipe(app.gulp.dest(app.paths.base.build));
20 | }
21 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import browserSync from 'browser-sync';
3 |
4 | import { paths } from './gulp/config/paths.js';
5 | import { clean } from './gulp/tasks/clean.js';
6 | import { svgSprites } from './gulp/tasks/sprite.js';
7 | import { styles } from './gulp/tasks/styles.js';
8 | import { stylesBackend } from './gulp/tasks/styles-backend.js';
9 | import { scripts } from './gulp/tasks/scripts.js';
10 | import { scriptsBackend } from './gulp/tasks/scripts-backend.js';
11 | import { resources } from './gulp/tasks/resources.js';
12 | import { images } from './gulp/tasks/images.js';
13 | import { webpImages } from './gulp/tasks/webp.js';
14 | import { htmlInclude } from './gulp/tasks/html-include.js';
15 | import { cacheTask } from './gulp/tasks/cache.js';
16 | import { rewrite } from './gulp/tasks/rewrite.js';
17 | import { htmlMinify } from './gulp/tasks/html-minify.js';
18 | import { zipFiles } from './gulp/tasks/zip.js';
19 |
20 | global.app = {
21 | gulp,
22 | isProd: process.argv.includes('--build'),
23 | paths,
24 | }
25 |
26 | const watcher = () => {
27 | browserSync.init({
28 | server: {
29 | baseDir: `${app.paths.base.build}`
30 | },
31 | notify: false,
32 | port: 3000,
33 | });
34 |
35 | gulp.watch(app.paths.srcScss, styles);
36 | gulp.watch(app.paths.srcFullJs, scripts);
37 | gulp.watch(`${app.paths.srcPartialsFolder}/*.html`, htmlInclude);
38 | gulp.watch(`${app.paths.base.src}/*.html`, htmlInclude);
39 | gulp.watch(`${app.paths.resourcesFolder}/**`, resources);
40 | gulp.watch(`${app.paths.srcImgFolder}/**/**.{jpg,jpeg,png,svg}`, images);
41 | gulp.watch(`${app.paths.srcImgFolder}/**/**.{jpg,jpeg,png}`, webpImages);
42 | gulp.watch(app.paths.srcSvg, svgSprites);
43 | }
44 |
45 | const dev = gulp.series(clean, htmlInclude, scripts, styles, resources, images, webpImages, svgSprites, watcher);
46 | const backend = gulp.series(clean, htmlInclude, scriptsBackend, stylesBackend, resources, images, webpImages, svgSprites);
47 | const build = gulp.series(clean, htmlInclude, scripts, styles, resources, images, webpImages, svgSprites, htmlMinify);
48 | const cache = gulp.series(cacheTask, rewrite);
49 | const zip = zipFiles;
50 |
51 | export { dev }
52 | export { build }
53 | export { backend }
54 | export { cache }
55 | export { zip }
56 |
57 | gulp.task('default', dev);
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gulp-builder",
3 | "version": "3.0.0",
4 | "description": "gulp-builder for weblayout",
5 | "main": "gulpfile.js",
6 | "type": "module",
7 | "scripts": {
8 | "stylelint": "npx stylelint **/*.scss",
9 | "style-fix": "npx stylelint **/*.scss --fix",
10 | "code": "editorconfig-checker",
11 | "dev": "gulp",
12 | "build": "npm run stylelint && npm run code && gulp build --build",
13 | "cache": "gulp cache",
14 | "backend": "gulp backend",
15 | "zip": "gulp zip --build"
16 | },
17 | "author": "MaxGraph",
18 | "license": "ISC",
19 | "devDependencies": {
20 | "@babel/core": "^7.24.4",
21 | "@babel/preset-env": "^7.24.4",
22 | "babel-loader": "^9.1.3",
23 | "browser-sync": "^3.0.2",
24 | "del": "^7.1.0",
25 | "editorconfig-checker": "^5.1.5",
26 | "gulp": "^5.0.0",
27 | "gulp-autoprefixer": "^9.0.0",
28 | "gulp-cheerio": "^1.0.0",
29 | "gulp-clean-css": "^4.3.0",
30 | "gulp-cli": "^3.0.0",
31 | "gulp-file-include": "^2.3.0",
32 | "gulp-htmlmin": "^5.0.1",
33 | "gulp-if": "^3.0.0",
34 | "gulp-imagemin": "^9.0.0",
35 | "gulp-newer": "^1.4.0",
36 | "gulp-notify": "^4.0.0",
37 | "gulp-plumber": "^1.2.1",
38 | "gulp-replace": "^1.1.4",
39 | "gulp-rev": "^11.0.0",
40 | "gulp-rev-delete-original": "^0.2.3",
41 | "gulp-rev-rewrite": "^6.0.0",
42 | "gulp-sass": "^5.1.0",
43 | "gulp-svg-sprite": "^2.0.3",
44 | "gulp-svgmin": "^4.1.0",
45 | "gulp-typograf": "^4.1.0",
46 | "gulp-webp": "^5.0.0",
47 | "gulp-zip": "^6.0.0",
48 | "postcss": "^8.4.38",
49 | "postcss-scss": "^4.0.9",
50 | "sass": "^1.75.0",
51 | "stylelint": "^16.3.1",
52 | "stylelint-config-standard-scss": "^13.1.0",
53 | "stylelint-order": "^6.0.4",
54 | "typograf": "^7.4.0",
55 | "webpack": "^5.91.0",
56 | "webpack-stream": "^7.0.0"
57 | },
58 | "dependencies": {
59 | "@popperjs/core": "^2.11.8",
60 | "aos": "^2.3.4",
61 | "graph-modal": "^1.0.7",
62 | "graph-tabs": "^1.0.2",
63 | "inputmask": "^5.0.8",
64 | "just-validate": "^4.3.0",
65 | "rellax": "^1.12.1",
66 | "simplebar": "^6.2.5",
67 | "swiped-events": "^1.1.9",
68 | "swiper": "^11.1.1"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/img/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxdenaro/gulp-maxgraph/2dc43cdb23ed1726c30df61325ccae381846eeda/src/img/cover.jpg
--------------------------------------------------------------------------------
/src/img/svg/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
9 |
10 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | @include('partials/head.html')
5 |
6 |
7 |
8 | @include('partials/header.html')
9 |
10 |
11 |
12 |
13 |
14 |
15 | @include('partials/footer.html')
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/js/_components.js:
--------------------------------------------------------------------------------
1 | console.log('components');
2 |
--------------------------------------------------------------------------------
/src/js/_functions.js:
--------------------------------------------------------------------------------
1 | // Данный файл - лишь собрание подключений готовых компонентов.
2 | // Рекомендуется создавать отдельный файл в папке components и подключать все там
3 |
4 | // Определение операционной системы на мобильных
5 | // import { mobileCheck } from "./functions/mobile-check.js";
6 | // console.log(mobileCheck())
7 |
8 | // Определение ширины экрана
9 | // import { isMobile, isTablet, isDesktop } from './functions/check-viewport';
10 | // if (isDesktop()) {
11 | // console.log('...')
12 | // }
13 |
14 | // Троттлинг функции (для ресайза, ввода в инпут, скролла и т.д.)
15 | // import { throttle } from './functions/throttle';
16 | // let yourFunc = () => { console.log('throttle') };
17 | // let func = throttle(yourFunc);
18 | // window.addEventListener('resize', func);
19 |
20 | // Фикс фулскрин-блоков
21 | // import './functions/fix-fullheight';
22 |
23 | // Реализация бургер-меню
24 | // import { burger } from './functions/burger';
25 |
26 | // Реализация остановки скролла (не забудьте вызвать функцию)
27 | // import { disableScroll } from './functions/disable-scroll';
28 |
29 | // Реализация включения скролла (не забудьте вызвать функцию)
30 | // import { enableScroll } from './functions/enable-scroll';
31 |
32 | // Реализация модального окна
33 | // import GraphModal from 'graph-modal';
34 | // const modal = new GraphModal();
35 |
36 | // Реализация табов
37 | // import GraphTabs from 'graph-tabs';
38 | // const tabs = new GraphTabs('tab');
39 |
40 | // Получение высоты шапки сайта (не забудьте вызвать функцию)
41 | // import { getHeaderHeight } from './functions/header-height';
42 |
43 | // Подключение плагина кастом-скролла
44 | // import 'simplebar';
45 |
46 | // Подключение плагина для позиционирования тултипов
47 | // import { createPopper, right} from '@popperjs/core';
48 | // createPopper(el, tooltip, {
49 | // placement: 'right'
50 | // });
51 |
52 | // Подключение свайпера
53 | // import Swiper from 'swiper';
54 | // import { Navigation, Pagination } from 'swiper/modules';
55 | // Swiper.use([Navigation, Pagination]);
56 | // const swiper = new Swiper(el, {
57 | // slidesPerView: 'auto',
58 | // });
59 |
60 | // Подключение анимаций по скроллу
61 | // import AOS from 'aos';
62 | // AOS.init();
63 |
64 | // Подключение параллакса блоков при скролле
65 | // import Rellax from 'rellax';
66 | // const rellax = new Rellax('.rellax');
67 |
68 | // Подключение плавной прокрутки к якорям
69 | // import SmoothScroll from 'smooth-scroll';
70 | // const scroll = new SmoothScroll('a[href*="#"]');
71 |
72 | // Подключение событий свайпа на мобильных
73 | // import 'swiped-events';
74 | // document.addEventListener('swiped', function(e) {
75 | // console.log(e.target);
76 | // console.log(e.detail);
77 | // console.log(e.detail.dir);
78 | // });
79 |
80 | // import { validateForms } from './functions/validate-forms';
81 | // const rules1 = [...];
82 |
83 | // const afterForm = () => {
84 | // console.log('Произошла отправка, тут можно писать любые действия');
85 | // };
86 |
87 | // validateForms('.form-1', rules1, afterForm);
88 |
--------------------------------------------------------------------------------
/src/js/_vars.js:
--------------------------------------------------------------------------------
1 | export default {
2 | windowEl: window,
3 | documentEl: document,
4 | htmlEl: document.documentElement,
5 | bodyEl: document.body,
6 | }
7 |
--------------------------------------------------------------------------------
/src/js/components/ex.js:
--------------------------------------------------------------------------------
1 | console.log('maxgraph');
2 |
--------------------------------------------------------------------------------
/src/js/functions/burger.js:
--------------------------------------------------------------------------------
1 | import { disableScroll } from '../functions/disable-scroll.js';
2 | import { enableScroll } from '../functions/enable-scroll.js';
3 |
4 | (function(){
5 | const burger = document?.querySelector('[data-burger]');
6 | const menu = document?.querySelector('[data-menu]');
7 | const menuItems = document?.querySelectorAll('[data-menu-item]');
8 | const overlay = document?.querySelector('[data-menu-overlay]');
9 |
10 | burger?.addEventListener('click', (e) => {
11 | burger?.classList.toggle('burger--active');
12 | menu?.classList.toggle('menu--active');
13 |
14 | if (menu?.classList.contains('menu--active')) {
15 | burger?.setAttribute('aria-expanded', 'true');
16 | burger?.setAttribute('aria-label', 'Закрыть меню');
17 | disableScroll();
18 | } else {
19 | burger?.setAttribute('aria-expanded', 'false');
20 | burger?.setAttribute('aria-label', 'Открыть меню');
21 | enableScroll();
22 | }
23 | });
24 |
25 | overlay?.addEventListener('click', () => {
26 | burger?.setAttribute('aria-expanded', 'false');
27 | burger?.setAttribute('aria-label', 'Открыть меню');
28 | burger.classList.remove('burger--active');
29 | menu.classList.remove('menu--active');
30 | enableScroll();
31 | });
32 |
33 | menuItems?.forEach(el => {
34 | el.addEventListener('click', () => {
35 | burger?.setAttribute('aria-expanded', 'false');
36 | burger?.setAttribute('aria-label', 'Открыть меню');
37 | burger.classList.remove('burger--active');
38 | menu.classList.remove('menu--active');
39 | enableScroll();
40 | });
41 | });
42 | })();
43 |
--------------------------------------------------------------------------------
/src/js/functions/check-viewport.js:
--------------------------------------------------------------------------------
1 | export const isMobile = () => {
2 | if (window.innerWidth < 768) {
3 | return true;
4 | }
5 |
6 | return false;
7 | };
8 |
9 | export const isTablet = () => {
10 | if (window.innerWidth >= 769 && window.innerWidth <= 1024) {
11 | return true;
12 | }
13 |
14 | return false;
15 | };
16 |
17 | export const isDesktop = () => {
18 | if (window.innerWidth > 1025) {
19 | return true;
20 | }
21 |
22 | return false;
23 | };
24 |
--------------------------------------------------------------------------------
/src/js/functions/disable-scroll.js:
--------------------------------------------------------------------------------
1 | import vars from '../_vars.js';
2 |
3 | export const disableScroll = () => {
4 | const fixBlocks = document?.querySelectorAll('.fixed-block');
5 | const pagePosition = window.scrollY;
6 | const paddingOffset = `${(window.innerWidth - vars.bodyEl.offsetWidth)}px`;
7 |
8 | vars.htmlEl.style.scrollBehavior = 'none';
9 | fixBlocks.forEach(el => { el.style.paddingRight = paddingOffset; });
10 | vars.bodyEl.style.paddingRight = paddingOffset;
11 | vars.bodyEl.classList.add('dis-scroll');
12 | vars.bodyEl.dataset.position = pagePosition;
13 | vars.bodyEl.style.top = `-${pagePosition}px`;
14 | }
15 |
--------------------------------------------------------------------------------
/src/js/functions/enable-scroll.js:
--------------------------------------------------------------------------------
1 | import vars from '../_vars.js';
2 |
3 | export const enableScroll = () => {
4 | const fixBlocks = document?.querySelectorAll('.fixed-block');
5 | const body = document.body;
6 | const pagePosition = parseInt(vars.bodyEl.dataset.position, 10);
7 | fixBlocks.forEach(el => { el.style.paddingRight = '0px'; });
8 | vars.bodyEl.style.paddingRight = '0px';
9 |
10 | vars.bodyEl.style.top = 'auto';
11 | vars.bodyEl.classList.remove('dis-scroll');
12 | window.scroll({
13 | top: pagePosition,
14 | left: 0
15 | });
16 | vars.bodyEl.removeAttribute('data-position');
17 | vars.htmlEl.style.scrollBehavior = 'smooth';
18 | }
19 |
--------------------------------------------------------------------------------
/src/js/functions/fix-fullheight.js:
--------------------------------------------------------------------------------
1 | import { throttle } from './throttle.js';
2 |
3 | const fixFullheight = () => {
4 | let vh = window.innerHeight;
5 | document.documentElement.style.setProperty('--vh', `${vh}px`);
6 | };
7 |
8 | let fixHeight = throttle(fixFullheight);
9 |
10 | fixHeight();
11 |
12 | window.addEventListener('resize', fixHeight);
13 |
--------------------------------------------------------------------------------
/src/js/functions/header-height.js:
--------------------------------------------------------------------------------
1 | export const getHeaderHeight = () => {
2 | const headerHeight = document?.querySelector('.header').offsetHeight;
3 | document.querySelector(':root').style.setProperty('--header-height', `${headerHeight}px`);
4 | }
5 |
--------------------------------------------------------------------------------
/src/js/functions/mobile-check.js:
--------------------------------------------------------------------------------
1 | import vars from '../_vars.js';
2 |
3 | export const mobileCheck = () => {
4 | const userAgent = navigator.userAgent || navigator.vendor || window.opera;
5 |
6 | if (/android/i.test(userAgent)) {
7 | vars.htmlEl.classList.add('page--android');
8 | return "Android";
9 | }
10 |
11 | if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
12 | vars.htmlEl.classList.add('page--ios');
13 | return "iOS";
14 | }
15 |
16 | return "unknown";
17 | };
18 |
--------------------------------------------------------------------------------
/src/js/functions/throttle.js:
--------------------------------------------------------------------------------
1 | export const throttle = (func, delay = 250) => {
2 | let isThrottled = false;
3 | let savedArgs = null;
4 | let savedThis = null;
5 |
6 | return function wrap(...args) {
7 | if (isThrottled) {
8 | savedArgs = args,
9 | savedThis = this;
10 | return;
11 | }
12 |
13 | func.apply(this, args);
14 | isThrottled = true;
15 |
16 | setTimeout(() => {
17 | isThrottled = false;
18 |
19 | if (savedThis) {
20 | wrap.apply(savedThis, savedArgs);
21 | savedThis = null;
22 | savedArgs = null;
23 | }
24 |
25 | }, delay);
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/js/functions/validate-forms.js:
--------------------------------------------------------------------------------
1 | import JustValidate from 'just-validate';
2 | import Inputmask from "../../../node_modules/inputmask/dist/inputmask.es6.js";
3 |
4 | export const validateForms = (selector, rules, checkboxes = [], afterSend) => {
5 | const form = document?.querySelector(selector);
6 | const telSelector = form?.querySelector('input[type="tel"]');
7 |
8 | if (!form) {
9 | console.error('Нет такого селектора!');
10 | return false;
11 | }
12 |
13 | if (!rules) {
14 | console.error('Вы не передали правила валидации!');
15 | return false;
16 | }
17 |
18 | if (telSelector) {
19 | const inputMask = new Inputmask('+7 (999) 999-99-99');
20 | inputMask.mask(telSelector);
21 |
22 | for (let item of rules) {
23 | if (item.tel) {
24 | item.rules.push({
25 | rule: 'function',
26 | validator: function() {
27 | const phone = telSelector.inputmask.unmaskedvalue();
28 | return phone.length === 10;
29 | },
30 | errorMessage: item.telError
31 | });
32 | }
33 | }
34 | }
35 |
36 | const validation = new JustValidate(selector);
37 |
38 | for (let item of rules) {
39 | validation
40 | .addField(item.ruleSelector, item.rules);
41 | }
42 |
43 | if (checkboxes.length) {
44 | for (let item of checkboxes) {
45 | validation
46 | .addRequiredGroup(
47 | `${item.selector}`,
48 | `${item.errorMessage}`
49 | )
50 | }
51 | }
52 |
53 | validation.onSuccess((ev) => {
54 | let formData = new FormData(ev.target);
55 |
56 | let xhr = new XMLHttpRequest();
57 |
58 | xhr.onreadystatechange = function () {
59 | if (xhr.readyState === 4) {
60 | if (xhr.status === 200) {
61 | if (afterSend) {
62 | afterSend();
63 | }
64 | console.log('Отправлено');
65 | }
66 | }
67 | }
68 |
69 | xhr.open('POST', 'mail.php', true);
70 | xhr.send(formData);
71 |
72 | ev.target.reset();
73 | })
74 |
75 | };
76 |
--------------------------------------------------------------------------------
/src/js/main.js:
--------------------------------------------------------------------------------
1 | import './_components.js';
2 |
--------------------------------------------------------------------------------
/src/partials/footer.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/partials/head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | MaxGraph New Site
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/partials/header.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/resources/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxdenaro/gulp-maxgraph/2dc43cdb23ed1726c30df61325ccae381846eeda/src/resources/favicon.ico
--------------------------------------------------------------------------------
/src/resources/fonts/MullerRegular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxdenaro/gulp-maxgraph/2dc43cdb23ed1726c30df61325ccae381846eeda/src/resources/fonts/MullerRegular.woff2
--------------------------------------------------------------------------------
/src/resources/mail.php:
--------------------------------------------------------------------------------
1 | $value ) {
14 | if ( $value != "" && $key != "project_name" && $key != "admin_email" && $key != "form_subject" ) {
15 | $body .= "
16 | " . ( ($c = !$c) ? '':' ' ) . "
17 | $key
18 | $value
19 |
20 | ";
21 | }
22 | }
23 |
24 | $body = "";
25 |
26 | // Настройки PHPMailer
27 | $mail = new PHPMailer\PHPMailer\PHPMailer();
28 |
29 | try {
30 | $mail->isSMTP();
31 | $mail->CharSet = "UTF-8";
32 | $mail->SMTPAuth = true;
33 |
34 | // Настройки вашей почты
35 | $mail->Host = 'smtp.gmail.com'; // SMTP сервера вашей почты
36 | $mail->Username = ''; // Логин на почте
37 | $mail->Password = ''; // Пароль на почте
38 | $mail->SMTPSecure = 'ssl';
39 | $mail->Port = 465;
40 |
41 | $mail->setFrom('', 'Заявка с вашего сайта'); // Адрес самой почты и имя отправителя
42 |
43 | // Получатель письма
44 | $mail->addAddress('');
45 |
46 | // Прикрипление файлов к письму
47 | if (!empty($file['name'][0])) {
48 | for ($ct = 0; $ct < count($file['tmp_name']); $ct++) {
49 | $uploadfile = tempnam(sys_get_temp_dir(), sha1($file['name'][$ct]));
50 | $filename = $file['name'][$ct];
51 | if (move_uploaded_file($file['tmp_name'][$ct], $uploadfile)) {
52 | $mail->addAttachment($uploadfile, $filename);
53 | $rfile[] = "Файл $filename прикреплён";
54 | } else {
55 | $rfile[] = "Не удалось прикрепить файл $filename";
56 | }
57 | }
58 | }
59 |
60 | // Отправка сообщения
61 | $mail->isHTML(true);
62 | $mail->Subject = $title;
63 | $mail->Body = $body;
64 |
65 | $mail->send();
66 |
67 | } catch (Exception $e) {
68 | $status = "Сообщение не было отправлено. Причина ошибки: {$mail->ErrorInfo}";
69 | }
70 |
--------------------------------------------------------------------------------
/src/resources/phpmailer/Exception.php:
--------------------------------------------------------------------------------
1 |
10 | * @author Jim Jagielski (jimjag)
11 | * @author Andy Prevost (codeworxtech)
12 | * @author Brent R. Matzelle (original founder)
13 | * @copyright 2012 - 2020 Marcus Bointon
14 | * @copyright 2010 - 2012 Jim Jagielski
15 | * @copyright 2004 - 2009 Andy Prevost
16 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
17 | * @note This program is distributed in the hope that it will be useful - WITHOUT
18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 | * FITNESS FOR A PARTICULAR PURPOSE.
20 | */
21 |
22 | namespace PHPMailer\PHPMailer;
23 |
24 | /**
25 | * PHPMailer exception handler.
26 | *
27 | * @author Marcus Bointon
28 | */
29 | class Exception extends \Exception
30 | {
31 | /**
32 | * Prettify error message output.
33 | *
34 | * @return string
35 | */
36 | public function errorMessage()
37 | {
38 | return '' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . " \n";
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/resources/phpmailer/SMTP.php:
--------------------------------------------------------------------------------
1 |
10 | * @author Jim Jagielski (jimjag)
11 | * @author Andy Prevost (codeworxtech)
12 | * @author Brent R. Matzelle (original founder)
13 | * @copyright 2012 - 2020 Marcus Bointon
14 | * @copyright 2010 - 2012 Jim Jagielski
15 | * @copyright 2004 - 2009 Andy Prevost
16 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
17 | * @note This program is distributed in the hope that it will be useful - WITHOUT
18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 | * FITNESS FOR A PARTICULAR PURPOSE.
20 | */
21 |
22 | namespace PHPMailer\PHPMailer;
23 |
24 | /**
25 | * PHPMailer RFC821 SMTP email transport class.
26 | * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
27 | *
28 | * @author Chris Ryan
29 | * @author Marcus Bointon
30 | */
31 | class SMTP
32 | {
33 | /**
34 | * The PHPMailer SMTP version number.
35 | *
36 | * @var string
37 | */
38 | const VERSION = '6.5.3';
39 |
40 | /**
41 | * SMTP line break constant.
42 | *
43 | * @var string
44 | */
45 | const LE = "\r\n";
46 |
47 | /**
48 | * The SMTP port to use if one is not specified.
49 | *
50 | * @var int
51 | */
52 | const DEFAULT_PORT = 25;
53 |
54 | /**
55 | * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
56 | * *excluding* a trailing CRLF break.
57 | *
58 | * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6
59 | *
60 | * @var int
61 | */
62 | const MAX_LINE_LENGTH = 998;
63 |
64 | /**
65 | * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
66 | * *including* a trailing CRLF line break.
67 | *
68 | * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5
69 | *
70 | * @var int
71 | */
72 | const MAX_REPLY_LENGTH = 512;
73 |
74 | /**
75 | * Debug level for no output.
76 | *
77 | * @var int
78 | */
79 | const DEBUG_OFF = 0;
80 |
81 | /**
82 | * Debug level to show client -> server messages.
83 | *
84 | * @var int
85 | */
86 | const DEBUG_CLIENT = 1;
87 |
88 | /**
89 | * Debug level to show client -> server and server -> client messages.
90 | *
91 | * @var int
92 | */
93 | const DEBUG_SERVER = 2;
94 |
95 | /**
96 | * Debug level to show connection status, client -> server and server -> client messages.
97 | *
98 | * @var int
99 | */
100 | const DEBUG_CONNECTION = 3;
101 |
102 | /**
103 | * Debug level to show all messages.
104 | *
105 | * @var int
106 | */
107 | const DEBUG_LOWLEVEL = 4;
108 |
109 | /**
110 | * Debug output level.
111 | * Options:
112 | * * self::DEBUG_OFF (`0`) No debug output, default
113 | * * self::DEBUG_CLIENT (`1`) Client commands
114 | * * self::DEBUG_SERVER (`2`) Client commands and server responses
115 | * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
116 | * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
117 | *
118 | * @var int
119 | */
120 | public $do_debug = self::DEBUG_OFF;
121 |
122 | /**
123 | * How to handle debug output.
124 | * Options:
125 | * * `echo` Output plain-text as-is, appropriate for CLI
126 | * * `html` Output escaped, line breaks converted to ` `, appropriate for browser output
127 | * * `error_log` Output to error log as configured in php.ini
128 | * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
129 | *
130 | * ```php
131 | * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
132 | * ```
133 | *
134 | * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
135 | * level output is used:
136 | *
137 | * ```php
138 | * $mail->Debugoutput = new myPsr3Logger;
139 | * ```
140 | *
141 | * @var string|callable|\Psr\Log\LoggerInterface
142 | */
143 | public $Debugoutput = 'echo';
144 |
145 | /**
146 | * Whether to use VERP.
147 | *
148 | * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
149 | * @see http://www.postfix.org/VERP_README.html Info on VERP
150 | *
151 | * @var bool
152 | */
153 | public $do_verp = false;
154 |
155 | /**
156 | * The timeout value for connection, in seconds.
157 | * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
158 | * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
159 | *
160 | * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
161 | *
162 | * @var int
163 | */
164 | public $Timeout = 300;
165 |
166 | /**
167 | * How long to wait for commands to complete, in seconds.
168 | * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
169 | *
170 | * @var int
171 | */
172 | public $Timelimit = 300;
173 |
174 | /**
175 | * Patterns to extract an SMTP transaction id from reply to a DATA command.
176 | * The first capture group in each regex will be used as the ID.
177 | * MS ESMTP returns the message ID, which may not be correct for internal tracking.
178 | *
179 | * @var string[]
180 | */
181 | protected $smtp_transaction_id_patterns = [
182 | 'exim' => '/[\d]{3} OK id=(.*)/',
183 | 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/',
184 | 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/',
185 | 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
186 | 'Amazon_SES' => '/[\d]{3} Ok (.*)/',
187 | 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
188 | 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
189 | 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
190 | 'Mailjet' => '/[\d]{3} OK queued as (.*)/',
191 | ];
192 |
193 | /**
194 | * The last transaction ID issued in response to a DATA command,
195 | * if one was detected.
196 | *
197 | * @var string|bool|null
198 | */
199 | protected $last_smtp_transaction_id;
200 |
201 | /**
202 | * The socket for the server connection.
203 | *
204 | * @var ?resource
205 | */
206 | protected $smtp_conn;
207 |
208 | /**
209 | * Error information, if any, for the last SMTP command.
210 | *
211 | * @var array
212 | */
213 | protected $error = [
214 | 'error' => '',
215 | 'detail' => '',
216 | 'smtp_code' => '',
217 | 'smtp_code_ex' => '',
218 | ];
219 |
220 | /**
221 | * The reply the server sent to us for HELO.
222 | * If null, no HELO string has yet been received.
223 | *
224 | * @var string|null
225 | */
226 | protected $helo_rply;
227 |
228 | /**
229 | * The set of SMTP extensions sent in reply to EHLO command.
230 | * Indexes of the array are extension names.
231 | * Value at index 'HELO' or 'EHLO' (according to command that was sent)
232 | * represents the server name. In case of HELO it is the only element of the array.
233 | * Other values can be boolean TRUE or an array containing extension options.
234 | * If null, no HELO/EHLO string has yet been received.
235 | *
236 | * @var array|null
237 | */
238 | protected $server_caps;
239 |
240 | /**
241 | * The most recent reply received from the server.
242 | *
243 | * @var string
244 | */
245 | protected $last_reply = '';
246 |
247 | /**
248 | * Output debugging info via a user-selected method.
249 | *
250 | * @param string $str Debug string to output
251 | * @param int $level The debug level of this message; see DEBUG_* constants
252 | *
253 | * @see SMTP::$Debugoutput
254 | * @see SMTP::$do_debug
255 | */
256 | protected function edebug($str, $level = 0)
257 | {
258 | if ($level > $this->do_debug) {
259 | return;
260 | }
261 | //Is this a PSR-3 logger?
262 | if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
263 | $this->Debugoutput->debug($str);
264 |
265 | return;
266 | }
267 | //Avoid clash with built-in function names
268 | if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
269 | call_user_func($this->Debugoutput, $str, $level);
270 |
271 | return;
272 | }
273 | switch ($this->Debugoutput) {
274 | case 'error_log':
275 | //Don't output, just log
276 | error_log($str);
277 | break;
278 | case 'html':
279 | //Cleans up output a bit for a better looking, HTML-safe output
280 | echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
281 | preg_replace('/[\r\n]+/', '', $str),
282 | ENT_QUOTES,
283 | 'UTF-8'
284 | ), " \n";
285 | break;
286 | case 'echo':
287 | default:
288 | //Normalize line breaks
289 | $str = preg_replace('/\r\n|\r/m', "\n", $str);
290 | echo gmdate('Y-m-d H:i:s'),
291 | "\t",
292 | //Trim trailing space
293 | trim(
294 | //Indent for readability, except for trailing break
295 | str_replace(
296 | "\n",
297 | "\n \t ",
298 | trim($str)
299 | )
300 | ),
301 | "\n";
302 | }
303 | }
304 |
305 | /**
306 | * Connect to an SMTP server.
307 | *
308 | * @param string $host SMTP server IP or host name
309 | * @param int $port The port number to connect to
310 | * @param int $timeout How long to wait for the connection to open
311 | * @param array $options An array of options for stream_context_create()
312 | *
313 | * @return bool
314 | */
315 | public function connect($host, $port = null, $timeout = 30, $options = [])
316 | {
317 | //Clear errors to avoid confusion
318 | $this->setError('');
319 | //Make sure we are __not__ connected
320 | if ($this->connected()) {
321 | //Already connected, generate error
322 | $this->setError('Already connected to a server');
323 |
324 | return false;
325 | }
326 | if (empty($port)) {
327 | $port = self::DEFAULT_PORT;
328 | }
329 | //Connect to the SMTP server
330 | $this->edebug(
331 | "Connection: opening to $host:$port, timeout=$timeout, options=" .
332 | (count($options) > 0 ? var_export($options, true) : 'array()'),
333 | self::DEBUG_CONNECTION
334 | );
335 |
336 | $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
337 |
338 | if ($this->smtp_conn === false) {
339 | //Error info already set inside `getSMTPConnection()`
340 | return false;
341 | }
342 |
343 | $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
344 |
345 | //Get any announcement
346 | $this->last_reply = $this->get_lines();
347 | $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
348 | $responseCode = (int)substr($this->last_reply, 0, 3);
349 | if ($responseCode === 220) {
350 | return true;
351 | }
352 | //Anything other than a 220 response means something went wrong
353 | //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
354 | //https://tools.ietf.org/html/rfc5321#section-3.1
355 | if ($responseCode === 554) {
356 | $this->quit();
357 | }
358 | //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
359 | $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
360 | $this->close();
361 | return false;
362 | }
363 |
364 | /**
365 | * Create connection to the SMTP server.
366 | *
367 | * @param string $host SMTP server IP or host name
368 | * @param int $port The port number to connect to
369 | * @param int $timeout How long to wait for the connection to open
370 | * @param array $options An array of options for stream_context_create()
371 | *
372 | * @return false|resource
373 | */
374 | protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
375 | {
376 | static $streamok;
377 | //This is enabled by default since 5.0.0 but some providers disable it
378 | //Check this once and cache the result
379 | if (null === $streamok) {
380 | $streamok = function_exists('stream_socket_client');
381 | }
382 |
383 | $errno = 0;
384 | $errstr = '';
385 | if ($streamok) {
386 | $socket_context = stream_context_create($options);
387 | set_error_handler([$this, 'errorHandler']);
388 | $connection = stream_socket_client(
389 | $host . ':' . $port,
390 | $errno,
391 | $errstr,
392 | $timeout,
393 | STREAM_CLIENT_CONNECT,
394 | $socket_context
395 | );
396 | } else {
397 | //Fall back to fsockopen which should work in more places, but is missing some features
398 | $this->edebug(
399 | 'Connection: stream_socket_client not available, falling back to fsockopen',
400 | self::DEBUG_CONNECTION
401 | );
402 | set_error_handler([$this, 'errorHandler']);
403 | $connection = fsockopen(
404 | $host,
405 | $port,
406 | $errno,
407 | $errstr,
408 | $timeout
409 | );
410 | }
411 | restore_error_handler();
412 |
413 | //Verify we connected properly
414 | if (!is_resource($connection)) {
415 | $this->setError(
416 | 'Failed to connect to server',
417 | '',
418 | (string) $errno,
419 | $errstr
420 | );
421 | $this->edebug(
422 | 'SMTP ERROR: ' . $this->error['error']
423 | . ": $errstr ($errno)",
424 | self::DEBUG_CLIENT
425 | );
426 |
427 | return false;
428 | }
429 |
430 | //SMTP server can take longer to respond, give longer timeout for first read
431 | //Windows does not have support for this timeout function
432 | if (strpos(PHP_OS, 'WIN') !== 0) {
433 | $max = (int)ini_get('max_execution_time');
434 | //Don't bother if unlimited, or if set_time_limit is disabled
435 | if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
436 | @set_time_limit($timeout);
437 | }
438 | stream_set_timeout($connection, $timeout, 0);
439 | }
440 |
441 | return $connection;
442 | }
443 |
444 | /**
445 | * Initiate a TLS (encrypted) session.
446 | *
447 | * @return bool
448 | */
449 | public function startTLS()
450 | {
451 | if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
452 | return false;
453 | }
454 |
455 | //Allow the best TLS version(s) we can
456 | $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
457 |
458 | //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
459 | //so add them back in manually if we can
460 | if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
461 | $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
462 | $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
463 | }
464 |
465 | //Begin encrypted connection
466 | set_error_handler([$this, 'errorHandler']);
467 | $crypto_ok = stream_socket_enable_crypto(
468 | $this->smtp_conn,
469 | true,
470 | $crypto_method
471 | );
472 | restore_error_handler();
473 |
474 | return (bool) $crypto_ok;
475 | }
476 |
477 | /**
478 | * Perform SMTP authentication.
479 | * Must be run after hello().
480 | *
481 | * @see hello()
482 | *
483 | * @param string $username The user name
484 | * @param string $password The password
485 | * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
486 | * @param OAuth $OAuth An optional OAuth instance for XOAUTH2 authentication
487 | *
488 | * @return bool True if successfully authenticated
489 | */
490 | public function authenticate(
491 | $username,
492 | $password,
493 | $authtype = null,
494 | $OAuth = null
495 | ) {
496 | if (!$this->server_caps) {
497 | $this->setError('Authentication is not allowed before HELO/EHLO');
498 |
499 | return false;
500 | }
501 |
502 | if (array_key_exists('EHLO', $this->server_caps)) {
503 | //SMTP extensions are available; try to find a proper authentication method
504 | if (!array_key_exists('AUTH', $this->server_caps)) {
505 | $this->setError('Authentication is not allowed at this stage');
506 | //'at this stage' means that auth may be allowed after the stage changes
507 | //e.g. after STARTTLS
508 |
509 | return false;
510 | }
511 |
512 | $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
513 | $this->edebug(
514 | 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
515 | self::DEBUG_LOWLEVEL
516 | );
517 |
518 | //If we have requested a specific auth type, check the server supports it before trying others
519 | if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
520 | $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
521 | $authtype = null;
522 | }
523 |
524 | if (empty($authtype)) {
525 | //If no auth mechanism is specified, attempt to use these, in this order
526 | //Try CRAM-MD5 first as it's more secure than the others
527 | foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
528 | if (in_array($method, $this->server_caps['AUTH'], true)) {
529 | $authtype = $method;
530 | break;
531 | }
532 | }
533 | if (empty($authtype)) {
534 | $this->setError('No supported authentication methods found');
535 |
536 | return false;
537 | }
538 | $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
539 | }
540 |
541 | if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
542 | $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
543 |
544 | return false;
545 | }
546 | } elseif (empty($authtype)) {
547 | $authtype = 'LOGIN';
548 | }
549 | switch ($authtype) {
550 | case 'PLAIN':
551 | //Start authentication
552 | if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
553 | return false;
554 | }
555 | //Send encoded username and password
556 | if (
557 | //Format from https://tools.ietf.org/html/rfc4616#section-2
558 | //We skip the first field (it's forgery), so the string starts with a null byte
559 | !$this->sendCommand(
560 | 'User & Password',
561 | base64_encode("\0" . $username . "\0" . $password),
562 | 235
563 | )
564 | ) {
565 | return false;
566 | }
567 | break;
568 | case 'LOGIN':
569 | //Start authentication
570 | if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
571 | return false;
572 | }
573 | if (!$this->sendCommand('Username', base64_encode($username), 334)) {
574 | return false;
575 | }
576 | if (!$this->sendCommand('Password', base64_encode($password), 235)) {
577 | return false;
578 | }
579 | break;
580 | case 'CRAM-MD5':
581 | //Start authentication
582 | if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
583 | return false;
584 | }
585 | //Get the challenge
586 | $challenge = base64_decode(substr($this->last_reply, 4));
587 |
588 | //Build the response
589 | $response = $username . ' ' . $this->hmac($challenge, $password);
590 |
591 | //send encoded credentials
592 | return $this->sendCommand('Username', base64_encode($response), 235);
593 | case 'XOAUTH2':
594 | //The OAuth instance must be set up prior to requesting auth.
595 | if (null === $OAuth) {
596 | return false;
597 | }
598 | $oauth = $OAuth->getOauth64();
599 |
600 | //Start authentication
601 | if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
602 | return false;
603 | }
604 | break;
605 | default:
606 | $this->setError("Authentication method \"$authtype\" is not supported");
607 |
608 | return false;
609 | }
610 |
611 | return true;
612 | }
613 |
614 | /**
615 | * Calculate an MD5 HMAC hash.
616 | * Works like hash_hmac('md5', $data, $key)
617 | * in case that function is not available.
618 | *
619 | * @param string $data The data to hash
620 | * @param string $key The key to hash with
621 | *
622 | * @return string
623 | */
624 | protected function hmac($data, $key)
625 | {
626 | if (function_exists('hash_hmac')) {
627 | return hash_hmac('md5', $data, $key);
628 | }
629 |
630 | //The following borrowed from
631 | //http://php.net/manual/en/function.mhash.php#27225
632 |
633 | //RFC 2104 HMAC implementation for php.
634 | //Creates an md5 HMAC.
635 | //Eliminates the need to install mhash to compute a HMAC
636 | //by Lance Rushing
637 |
638 | $bytelen = 64; //byte length for md5
639 | if (strlen($key) > $bytelen) {
640 | $key = pack('H*', md5($key));
641 | }
642 | $key = str_pad($key, $bytelen, chr(0x00));
643 | $ipad = str_pad('', $bytelen, chr(0x36));
644 | $opad = str_pad('', $bytelen, chr(0x5c));
645 | $k_ipad = $key ^ $ipad;
646 | $k_opad = $key ^ $opad;
647 |
648 | return md5($k_opad . pack('H*', md5($k_ipad . $data)));
649 | }
650 |
651 | /**
652 | * Check connection state.
653 | *
654 | * @return bool True if connected
655 | */
656 | public function connected()
657 | {
658 | if (is_resource($this->smtp_conn)) {
659 | $sock_status = stream_get_meta_data($this->smtp_conn);
660 | if ($sock_status['eof']) {
661 | //The socket is valid but we are not connected
662 | $this->edebug(
663 | 'SMTP NOTICE: EOF caught while checking if connected',
664 | self::DEBUG_CLIENT
665 | );
666 | $this->close();
667 |
668 | return false;
669 | }
670 |
671 | return true; //everything looks good
672 | }
673 |
674 | return false;
675 | }
676 |
677 | /**
678 | * Close the socket and clean up the state of the class.
679 | * Don't use this function without first trying to use QUIT.
680 | *
681 | * @see quit()
682 | */
683 | public function close()
684 | {
685 | $this->setError('');
686 | $this->server_caps = null;
687 | $this->helo_rply = null;
688 | if (is_resource($this->smtp_conn)) {
689 | //Close the connection and cleanup
690 | fclose($this->smtp_conn);
691 | $this->smtp_conn = null; //Makes for cleaner serialization
692 | $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
693 | }
694 | }
695 |
696 | /**
697 | * Send an SMTP DATA command.
698 | * Issues a data command and sends the msg_data to the server,
699 | * finalizing the mail transaction. $msg_data is the message
700 | * that is to be send with the headers. Each header needs to be
701 | * on a single line followed by a with the message headers
702 | * and the message body being separated by an additional .
703 | * Implements RFC 821: DATA .
704 | *
705 | * @param string $msg_data Message data to send
706 | *
707 | * @return bool
708 | */
709 | public function data($msg_data)
710 | {
711 | //This will use the standard timelimit
712 | if (!$this->sendCommand('DATA', 'DATA', 354)) {
713 | return false;
714 | }
715 |
716 | /* The server is ready to accept data!
717 | * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
718 | * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
719 | * smaller lines to fit within the limit.
720 | * We will also look for lines that start with a '.' and prepend an additional '.'.
721 | * NOTE: this does not count towards line-length limit.
722 | */
723 |
724 | //Normalize line breaks before exploding
725 | $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
726 |
727 | /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
728 | * of the first line (':' separated) does not contain a space then it _should_ be a header and we will
729 | * process all lines before a blank line as headers.
730 | */
731 |
732 | $field = substr($lines[0], 0, strpos($lines[0], ':'));
733 | $in_headers = false;
734 | if (!empty($field) && strpos($field, ' ') === false) {
735 | $in_headers = true;
736 | }
737 |
738 | foreach ($lines as $line) {
739 | $lines_out = [];
740 | if ($in_headers && $line === '') {
741 | $in_headers = false;
742 | }
743 | //Break this line up into several smaller lines if it's too long
744 | //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
745 | while (isset($line[self::MAX_LINE_LENGTH])) {
746 | //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
747 | //so as to avoid breaking in the middle of a word
748 | $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
749 | //Deliberately matches both false and 0
750 | if (!$pos) {
751 | //No nice break found, add a hard break
752 | $pos = self::MAX_LINE_LENGTH - 1;
753 | $lines_out[] = substr($line, 0, $pos);
754 | $line = substr($line, $pos);
755 | } else {
756 | //Break at the found point
757 | $lines_out[] = substr($line, 0, $pos);
758 | //Move along by the amount we dealt with
759 | $line = substr($line, $pos + 1);
760 | }
761 | //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
762 | if ($in_headers) {
763 | $line = "\t" . $line;
764 | }
765 | }
766 | $lines_out[] = $line;
767 |
768 | //Send the lines to the server
769 | foreach ($lines_out as $line_out) {
770 | //Dot-stuffing as per RFC5321 section 4.5.2
771 | //https://tools.ietf.org/html/rfc5321#section-4.5.2
772 | if (!empty($line_out) && $line_out[0] === '.') {
773 | $line_out = '.' . $line_out;
774 | }
775 | $this->client_send($line_out . static::LE, 'DATA');
776 | }
777 | }
778 |
779 | //Message data has been sent, complete the command
780 | //Increase timelimit for end of DATA command
781 | $savetimelimit = $this->Timelimit;
782 | $this->Timelimit *= 2;
783 | $result = $this->sendCommand('DATA END', '.', 250);
784 | $this->recordLastTransactionID();
785 | //Restore timelimit
786 | $this->Timelimit = $savetimelimit;
787 |
788 | return $result;
789 | }
790 |
791 | /**
792 | * Send an SMTP HELO or EHLO command.
793 | * Used to identify the sending server to the receiving server.
794 | * This makes sure that client and server are in a known state.
795 | * Implements RFC 821: HELO
796 | * and RFC 2821 EHLO.
797 | *
798 | * @param string $host The host name or IP to connect to
799 | *
800 | * @return bool
801 | */
802 | public function hello($host = '')
803 | {
804 | //Try extended hello first (RFC 2821)
805 | if ($this->sendHello('EHLO', $host)) {
806 | return true;
807 | }
808 |
809 | //Some servers shut down the SMTP service here (RFC 5321)
810 | if (substr($this->helo_rply, 0, 3) == '421') {
811 | return false;
812 | }
813 |
814 | return $this->sendHello('HELO', $host);
815 | }
816 |
817 | /**
818 | * Send an SMTP HELO or EHLO command.
819 | * Low-level implementation used by hello().
820 | *
821 | * @param string $hello The HELO string
822 | * @param string $host The hostname to say we are
823 | *
824 | * @return bool
825 | *
826 | * @see hello()
827 | */
828 | protected function sendHello($hello, $host)
829 | {
830 | $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
831 | $this->helo_rply = $this->last_reply;
832 | if ($noerror) {
833 | $this->parseHelloFields($hello);
834 | } else {
835 | $this->server_caps = null;
836 | }
837 |
838 | return $noerror;
839 | }
840 |
841 | /**
842 | * Parse a reply to HELO/EHLO command to discover server extensions.
843 | * In case of HELO, the only parameter that can be discovered is a server name.
844 | *
845 | * @param string $type `HELO` or `EHLO`
846 | */
847 | protected function parseHelloFields($type)
848 | {
849 | $this->server_caps = [];
850 | $lines = explode("\n", $this->helo_rply);
851 |
852 | foreach ($lines as $n => $s) {
853 | //First 4 chars contain response code followed by - or space
854 | $s = trim(substr($s, 4));
855 | if (empty($s)) {
856 | continue;
857 | }
858 | $fields = explode(' ', $s);
859 | if (!empty($fields)) {
860 | if (!$n) {
861 | $name = $type;
862 | $fields = $fields[0];
863 | } else {
864 | $name = array_shift($fields);
865 | switch ($name) {
866 | case 'SIZE':
867 | $fields = ($fields ? $fields[0] : 0);
868 | break;
869 | case 'AUTH':
870 | if (!is_array($fields)) {
871 | $fields = [];
872 | }
873 | break;
874 | default:
875 | $fields = true;
876 | }
877 | }
878 | $this->server_caps[$name] = $fields;
879 | }
880 | }
881 | }
882 |
883 | /**
884 | * Send an SMTP MAIL command.
885 | * Starts a mail transaction from the email address specified in
886 | * $from. Returns true if successful or false otherwise. If True
887 | * the mail transaction is started and then one or more recipient
888 | * commands may be called followed by a data command.
889 | * Implements RFC 821: MAIL FROM: .
890 | *
891 | * @param string $from Source address of this message
892 | *
893 | * @return bool
894 | */
895 | public function mail($from)
896 | {
897 | $useVerp = ($this->do_verp ? ' XVERP' : '');
898 |
899 | return $this->sendCommand(
900 | 'MAIL FROM',
901 | 'MAIL FROM:<' . $from . '>' . $useVerp,
902 | 250
903 | );
904 | }
905 |
906 | /**
907 | * Send an SMTP QUIT command.
908 | * Closes the socket if there is no error or the $close_on_error argument is true.
909 | * Implements from RFC 821: QUIT .
910 | *
911 | * @param bool $close_on_error Should the connection close if an error occurs?
912 | *
913 | * @return bool
914 | */
915 | public function quit($close_on_error = true)
916 | {
917 | $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
918 | $err = $this->error; //Save any error
919 | if ($noerror || $close_on_error) {
920 | $this->close();
921 | $this->error = $err; //Restore any error from the quit command
922 | }
923 |
924 | return $noerror;
925 | }
926 |
927 | /**
928 | * Send an SMTP RCPT command.
929 | * Sets the TO argument to $toaddr.
930 | * Returns true if the recipient was accepted false if it was rejected.
931 | * Implements from RFC 821: RCPT TO: .
932 | *
933 | * @param string $address The address the message is being sent to
934 | * @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
935 | * or DELAY. If you specify NEVER all other notifications are ignored.
936 | *
937 | * @return bool
938 | */
939 | public function recipient($address, $dsn = '')
940 | {
941 | if (empty($dsn)) {
942 | $rcpt = 'RCPT TO:<' . $address . '>';
943 | } else {
944 | $dsn = strtoupper($dsn);
945 | $notify = [];
946 |
947 | if (strpos($dsn, 'NEVER') !== false) {
948 | $notify[] = 'NEVER';
949 | } else {
950 | foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
951 | if (strpos($dsn, $value) !== false) {
952 | $notify[] = $value;
953 | }
954 | }
955 | }
956 |
957 | $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
958 | }
959 |
960 | return $this->sendCommand(
961 | 'RCPT TO',
962 | $rcpt,
963 | [250, 251]
964 | );
965 | }
966 |
967 | /**
968 | * Send an SMTP RSET command.
969 | * Abort any transaction that is currently in progress.
970 | * Implements RFC 821: RSET .
971 | *
972 | * @return bool True on success
973 | */
974 | public function reset()
975 | {
976 | return $this->sendCommand('RSET', 'RSET', 250);
977 | }
978 |
979 | /**
980 | * Send a command to an SMTP server and check its return code.
981 | *
982 | * @param string $command The command name - not sent to the server
983 | * @param string $commandstring The actual command to send
984 | * @param int|array $expect One or more expected integer success codes
985 | *
986 | * @return bool True on success
987 | */
988 | protected function sendCommand($command, $commandstring, $expect)
989 | {
990 | if (!$this->connected()) {
991 | $this->setError("Called $command without being connected");
992 |
993 | return false;
994 | }
995 | //Reject line breaks in all commands
996 | if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
997 | $this->setError("Command '$command' contained line breaks");
998 |
999 | return false;
1000 | }
1001 | $this->client_send($commandstring . static::LE, $command);
1002 |
1003 | $this->last_reply = $this->get_lines();
1004 | //Fetch SMTP code and possible error code explanation
1005 | $matches = [];
1006 | if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
1007 | $code = (int) $matches[1];
1008 | $code_ex = (count($matches) > 2 ? $matches[2] : null);
1009 | //Cut off error code from each response line
1010 | $detail = preg_replace(
1011 | "/{$code}[ -]" .
1012 | ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
1013 | '',
1014 | $this->last_reply
1015 | );
1016 | } else {
1017 | //Fall back to simple parsing if regex fails
1018 | $code = (int) substr($this->last_reply, 0, 3);
1019 | $code_ex = null;
1020 | $detail = substr($this->last_reply, 4);
1021 | }
1022 |
1023 | $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
1024 |
1025 | if (!in_array($code, (array) $expect, true)) {
1026 | $this->setError(
1027 | "$command command failed",
1028 | $detail,
1029 | $code,
1030 | $code_ex
1031 | );
1032 | $this->edebug(
1033 | 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
1034 | self::DEBUG_CLIENT
1035 | );
1036 |
1037 | return false;
1038 | }
1039 |
1040 | $this->setError('');
1041 |
1042 | return true;
1043 | }
1044 |
1045 | /**
1046 | * Send an SMTP SAML command.
1047 | * Starts a mail transaction from the email address specified in $from.
1048 | * Returns true if successful or false otherwise. If True
1049 | * the mail transaction is started and then one or more recipient
1050 | * commands may be called followed by a data command. This command
1051 | * will send the message to the users terminal if they are logged
1052 | * in and send them an email.
1053 | * Implements RFC 821: SAML FROM: .
1054 | *
1055 | * @param string $from The address the message is from
1056 | *
1057 | * @return bool
1058 | */
1059 | public function sendAndMail($from)
1060 | {
1061 | return $this->sendCommand('SAML', "SAML FROM:$from", 250);
1062 | }
1063 |
1064 | /**
1065 | * Send an SMTP VRFY command.
1066 | *
1067 | * @param string $name The name to verify
1068 | *
1069 | * @return bool
1070 | */
1071 | public function verify($name)
1072 | {
1073 | return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
1074 | }
1075 |
1076 | /**
1077 | * Send an SMTP NOOP command.
1078 | * Used to keep keep-alives alive, doesn't actually do anything.
1079 | *
1080 | * @return bool
1081 | */
1082 | public function noop()
1083 | {
1084 | return $this->sendCommand('NOOP', 'NOOP', 250);
1085 | }
1086 |
1087 | /**
1088 | * Send an SMTP TURN command.
1089 | * This is an optional command for SMTP that this class does not support.
1090 | * This method is here to make the RFC821 Definition complete for this class
1091 | * and _may_ be implemented in future.
1092 | * Implements from RFC 821: TURN .
1093 | *
1094 | * @return bool
1095 | */
1096 | public function turn()
1097 | {
1098 | $this->setError('The SMTP TURN command is not implemented');
1099 | $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
1100 |
1101 | return false;
1102 | }
1103 |
1104 | /**
1105 | * Send raw data to the server.
1106 | *
1107 | * @param string $data The data to send
1108 | * @param string $command Optionally, the command this is part of, used only for controlling debug output
1109 | *
1110 | * @return int|bool The number of bytes sent to the server or false on error
1111 | */
1112 | public function client_send($data, $command = '')
1113 | {
1114 | //If SMTP transcripts are left enabled, or debug output is posted online
1115 | //it can leak credentials, so hide credentials in all but lowest level
1116 | if (
1117 | self::DEBUG_LOWLEVEL > $this->do_debug &&
1118 | in_array($command, ['User & Password', 'Username', 'Password'], true)
1119 | ) {
1120 | $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
1121 | } else {
1122 | $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
1123 | }
1124 | set_error_handler([$this, 'errorHandler']);
1125 | $result = fwrite($this->smtp_conn, $data);
1126 | restore_error_handler();
1127 |
1128 | return $result;
1129 | }
1130 |
1131 | /**
1132 | * Get the latest error.
1133 | *
1134 | * @return array
1135 | */
1136 | public function getError()
1137 | {
1138 | return $this->error;
1139 | }
1140 |
1141 | /**
1142 | * Get SMTP extensions available on the server.
1143 | *
1144 | * @return array|null
1145 | */
1146 | public function getServerExtList()
1147 | {
1148 | return $this->server_caps;
1149 | }
1150 |
1151 | /**
1152 | * Get metadata about the SMTP server from its HELO/EHLO response.
1153 | * The method works in three ways, dependent on argument value and current state:
1154 | * 1. HELO/EHLO has not been sent - returns null and populates $this->error.
1155 | * 2. HELO has been sent -
1156 | * $name == 'HELO': returns server name
1157 | * $name == 'EHLO': returns boolean false
1158 | * $name == any other string: returns null and populates $this->error
1159 | * 3. EHLO has been sent -
1160 | * $name == 'HELO'|'EHLO': returns the server name
1161 | * $name == any other string: if extension $name exists, returns True
1162 | * or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
1163 | *
1164 | * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
1165 | *
1166 | * @return string|bool|null
1167 | */
1168 | public function getServerExt($name)
1169 | {
1170 | if (!$this->server_caps) {
1171 | $this->setError('No HELO/EHLO was sent');
1172 |
1173 | return null;
1174 | }
1175 |
1176 | if (!array_key_exists($name, $this->server_caps)) {
1177 | if ('HELO' === $name) {
1178 | return $this->server_caps['EHLO'];
1179 | }
1180 | if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
1181 | return false;
1182 | }
1183 | $this->setError('HELO handshake was used; No information about server extensions available');
1184 |
1185 | return null;
1186 | }
1187 |
1188 | return $this->server_caps[$name];
1189 | }
1190 |
1191 | /**
1192 | * Get the last reply from the server.
1193 | *
1194 | * @return string
1195 | */
1196 | public function getLastReply()
1197 | {
1198 | return $this->last_reply;
1199 | }
1200 |
1201 | /**
1202 | * Read the SMTP server's response.
1203 | * Either before eof or socket timeout occurs on the operation.
1204 | * With SMTP we can tell if we have more lines to read if the
1205 | * 4th character is '-' symbol. If it is a space then we don't
1206 | * need to read anything else.
1207 | *
1208 | * @return string
1209 | */
1210 | protected function get_lines()
1211 | {
1212 | //If the connection is bad, give up straight away
1213 | if (!is_resource($this->smtp_conn)) {
1214 | return '';
1215 | }
1216 | $data = '';
1217 | $endtime = 0;
1218 | stream_set_timeout($this->smtp_conn, $this->Timeout);
1219 | if ($this->Timelimit > 0) {
1220 | $endtime = time() + $this->Timelimit;
1221 | }
1222 | $selR = [$this->smtp_conn];
1223 | $selW = null;
1224 | while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
1225 | //Must pass vars in here as params are by reference
1226 | //solution for signals inspired by https://github.com/symfony/symfony/pull/6540
1227 | set_error_handler([$this, 'errorHandler']);
1228 | $n = stream_select($selR, $selW, $selW, $this->Timelimit);
1229 | restore_error_handler();
1230 |
1231 | if ($n === false) {
1232 | $message = $this->getError()['detail'];
1233 |
1234 | $this->edebug(
1235 | 'SMTP -> get_lines(): select failed (' . $message . ')',
1236 | self::DEBUG_LOWLEVEL
1237 | );
1238 |
1239 | //stream_select returns false when the `select` system call is interrupted
1240 | //by an incoming signal, try the select again
1241 | if (stripos($message, 'interrupted system call') !== false) {
1242 | $this->edebug(
1243 | 'SMTP -> get_lines(): retrying stream_select',
1244 | self::DEBUG_LOWLEVEL
1245 | );
1246 | $this->setError('');
1247 | continue;
1248 | }
1249 |
1250 | break;
1251 | }
1252 |
1253 | if (!$n) {
1254 | $this->edebug(
1255 | 'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
1256 | self::DEBUG_LOWLEVEL
1257 | );
1258 | break;
1259 | }
1260 |
1261 | //Deliberate noise suppression - errors are handled afterwards
1262 | $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
1263 | $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
1264 | $data .= $str;
1265 | //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
1266 | //or 4th character is a space or a line break char, we are done reading, break the loop.
1267 | //String array access is a significant micro-optimisation over strlen
1268 | if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
1269 | break;
1270 | }
1271 | //Timed-out? Log and break
1272 | $info = stream_get_meta_data($this->smtp_conn);
1273 | if ($info['timed_out']) {
1274 | $this->edebug(
1275 | 'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
1276 | self::DEBUG_LOWLEVEL
1277 | );
1278 | break;
1279 | }
1280 | //Now check if reads took too long
1281 | if ($endtime && time() > $endtime) {
1282 | $this->edebug(
1283 | 'SMTP -> get_lines(): timelimit reached (' .
1284 | $this->Timelimit . ' sec)',
1285 | self::DEBUG_LOWLEVEL
1286 | );
1287 | break;
1288 | }
1289 | }
1290 |
1291 | return $data;
1292 | }
1293 |
1294 | /**
1295 | * Enable or disable VERP address generation.
1296 | *
1297 | * @param bool $enabled
1298 | */
1299 | public function setVerp($enabled = false)
1300 | {
1301 | $this->do_verp = $enabled;
1302 | }
1303 |
1304 | /**
1305 | * Get VERP address generation mode.
1306 | *
1307 | * @return bool
1308 | */
1309 | public function getVerp()
1310 | {
1311 | return $this->do_verp;
1312 | }
1313 |
1314 | /**
1315 | * Set error messages and codes.
1316 | *
1317 | * @param string $message The error message
1318 | * @param string $detail Further detail on the error
1319 | * @param string $smtp_code An associated SMTP error code
1320 | * @param string $smtp_code_ex Extended SMTP code
1321 | */
1322 | protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
1323 | {
1324 | $this->error = [
1325 | 'error' => $message,
1326 | 'detail' => $detail,
1327 | 'smtp_code' => $smtp_code,
1328 | 'smtp_code_ex' => $smtp_code_ex,
1329 | ];
1330 | }
1331 |
1332 | /**
1333 | * Set debug output method.
1334 | *
1335 | * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
1336 | */
1337 | public function setDebugOutput($method = 'echo')
1338 | {
1339 | $this->Debugoutput = $method;
1340 | }
1341 |
1342 | /**
1343 | * Get debug output method.
1344 | *
1345 | * @return string
1346 | */
1347 | public function getDebugOutput()
1348 | {
1349 | return $this->Debugoutput;
1350 | }
1351 |
1352 | /**
1353 | * Set debug output level.
1354 | *
1355 | * @param int $level
1356 | */
1357 | public function setDebugLevel($level = 0)
1358 | {
1359 | $this->do_debug = $level;
1360 | }
1361 |
1362 | /**
1363 | * Get debug output level.
1364 | *
1365 | * @return int
1366 | */
1367 | public function getDebugLevel()
1368 | {
1369 | return $this->do_debug;
1370 | }
1371 |
1372 | /**
1373 | * Set SMTP timeout.
1374 | *
1375 | * @param int $timeout The timeout duration in seconds
1376 | */
1377 | public function setTimeout($timeout = 0)
1378 | {
1379 | $this->Timeout = $timeout;
1380 | }
1381 |
1382 | /**
1383 | * Get SMTP timeout.
1384 | *
1385 | * @return int
1386 | */
1387 | public function getTimeout()
1388 | {
1389 | return $this->Timeout;
1390 | }
1391 |
1392 | /**
1393 | * Reports an error number and string.
1394 | *
1395 | * @param int $errno The error number returned by PHP
1396 | * @param string $errmsg The error message returned by PHP
1397 | * @param string $errfile The file the error occurred in
1398 | * @param int $errline The line number the error occurred on
1399 | */
1400 | protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
1401 | {
1402 | $notice = 'Connection failed.';
1403 | $this->setError(
1404 | $notice,
1405 | $errmsg,
1406 | (string) $errno
1407 | );
1408 | $this->edebug(
1409 | "$notice Error #$errno: $errmsg [$errfile line $errline]",
1410 | self::DEBUG_CONNECTION
1411 | );
1412 | }
1413 |
1414 | /**
1415 | * Extract and return the ID of the last SMTP transaction based on
1416 | * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
1417 | * Relies on the host providing the ID in response to a DATA command.
1418 | * If no reply has been received yet, it will return null.
1419 | * If no pattern was matched, it will return false.
1420 | *
1421 | * @return bool|string|null
1422 | */
1423 | protected function recordLastTransactionID()
1424 | {
1425 | $reply = $this->getLastReply();
1426 |
1427 | if (empty($reply)) {
1428 | $this->last_smtp_transaction_id = null;
1429 | } else {
1430 | $this->last_smtp_transaction_id = false;
1431 | foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
1432 | $matches = [];
1433 | if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
1434 | $this->last_smtp_transaction_id = trim($matches[1]);
1435 | break;
1436 | }
1437 | }
1438 | }
1439 |
1440 | return $this->last_smtp_transaction_id;
1441 | }
1442 |
1443 | /**
1444 | * Get the queue/transaction ID of the last SMTP transaction
1445 | * If no reply has been received yet, it will return null.
1446 | * If no pattern was matched, it will return false.
1447 | *
1448 | * @return bool|string|null
1449 | *
1450 | * @see recordLastTransactionID()
1451 | */
1452 | public function getLastTransactionID()
1453 | {
1454 | return $this->last_smtp_transaction_id;
1455 | }
1456 | }
1457 |
--------------------------------------------------------------------------------
/src/scss/_fonts.scss:
--------------------------------------------------------------------------------
1 | // @include font-face("Muller", "MullerRegular", 400, normal);
2 |
--------------------------------------------------------------------------------
/src/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | @import "./mixins/breakpoint";
2 | @import "./mixins/burger";
3 | @import "./mixins/checkbox";
4 | @import "./mixins/font-face";
5 | @import "./mixins/flex";
6 | @import "./mixins/mini";
7 | @import "./mixins/tabs";
8 | @import "./mixins/disable-mob-hover";
9 | @import "./mixins/layout";
10 |
--------------------------------------------------------------------------------
/src/scss/_settings.scss:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: border-box;
5 | }
6 |
7 | .page {
8 | height: 100%;
9 | font-family: var(--font-family, sans-serif);
10 | -webkit-text-size-adjust: 100%;
11 | scroll-behavior: smooth;
12 | }
13 |
14 | .page__body {
15 | margin: 0;
16 | min-width: 360px;
17 | min-height: 100%;
18 | font-size: 16px;
19 | }
20 |
21 | img {
22 | height: auto;
23 | max-width: 100%;
24 | object-fit: cover;
25 | }
26 |
27 | a {
28 | text-decoration: none;
29 | }
30 |
31 | .site-container {
32 | overflow: hidden; // если используете на сайте position: sticky - уберите эту настройку
33 | }
34 |
35 | .is-hidden {
36 | display: none !important; /* stylelint-disable-line declaration-no-important */
37 | }
38 |
39 | .btn-reset {
40 | border: none;
41 | padding: 0;
42 | background-color: transparent;
43 | cursor: pointer;
44 | }
45 |
46 | .list-reset {
47 | list-style: none;
48 | margin: 0;
49 | padding: 0;
50 | }
51 |
52 | .input-reset {
53 | -webkit-appearance: none;
54 | appearance: none;
55 | border: none;
56 | border-radius: 0;
57 | background-color: #fff;
58 |
59 | &::-webkit-search-decoration,
60 | &::-webkit-search-cancel-button,
61 | &::-webkit-search-results-button,
62 | &::-webkit-search-results-decoration {
63 | display: none;
64 | }
65 | }
66 |
67 | .visually-hidden {
68 | position: absolute;
69 | overflow: hidden;
70 | margin: -1px;
71 | border: 0;
72 | padding: 0;
73 | width: 1px;
74 | height: 1px;
75 | clip: rect(0 0 0 0);
76 | }
77 |
78 | .container {
79 | margin: 0 auto;
80 | padding: 0 var(--container-offset);
81 | max-width: var(--container-width);
82 | }
83 |
84 | .centered {
85 | text-align: center;
86 | }
87 |
88 | .dis-scroll {
89 | position: fixed;
90 | left: 0;
91 | top: 0;
92 | overflow: hidden;
93 | width: 100%;
94 | height: 100vh;
95 | overscroll-behavior: none;
96 | }
97 |
98 | .page--ios .dis-scroll {
99 | position: relative;
100 | }
101 |
--------------------------------------------------------------------------------
/src/scss/_vars.scss:
--------------------------------------------------------------------------------
1 | // если вы хотите использовать sass-переменные - удалите root
2 | // colors
3 | :root {
4 | // base
5 | --font-family: "Open Sans", sans-serif;
6 | --content-width: 1170px;
7 | --container-offset: 15px;
8 | --container-width: calc(var(--content-width) + (var(--container-offset) * 2));
9 |
10 | // colors
11 | --light-color: #fff;
12 | }
13 |
--------------------------------------------------------------------------------
/src/scss/components/_header.scss:
--------------------------------------------------------------------------------
1 | // ваш код
2 |
--------------------------------------------------------------------------------
/src/scss/main.scss:
--------------------------------------------------------------------------------
1 | // базовые подключения
2 | @import "vars";
3 | @import "mixins";
4 | @import "fonts";
5 | @import "settings";
6 |
7 | // подключения компонентов страницы
8 | @import "./components/header";
9 |
10 | // тестовые стили, удалите их
11 | .page__body {
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | height: var(--vh);
16 | }
17 |
--------------------------------------------------------------------------------
/src/scss/mixins/_breakpoint.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable */
2 |
3 | @mixin for-desktop {
4 | @media (min-width: (1025px)) {
5 | @content;
6 | }
7 | }
8 |
9 | @mixin big-desktop {
10 | @media (max-width: (1440px)) {
11 | @content;
12 | }
13 | }
14 |
15 | @mixin tablet {
16 | @media (max-width: (1024px)) {
17 | @content;
18 | }
19 | }
20 |
21 | @mixin small-tablet {
22 | @media (max-width: (768px)) {
23 | @content;
24 | }
25 | }
26 |
27 | @mixin mobile {
28 | @media (max-width: (576px)) {
29 | @content;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/scss/mixins/_burger.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable */
2 |
3 | @mixin burger {
4 | --burger-width: 30px;
5 | --burger-height: 30px;
6 | --burger-line-height: 2px;
7 |
8 | position: relative;
9 | border: none;
10 | padding: 0;
11 | width: var(--burger-width);
12 | height: var(--burger-height);
13 | color: #000;
14 | background-color: transparent;
15 | cursor: pointer;
16 |
17 | &::before,
18 | &::after {
19 | content: "";
20 | position: absolute;
21 | left: 0;
22 | width: 100%;
23 | height: var(--burger-line-height);
24 | background-color: currentColor;
25 | transition: transform 0.3s ease-in-out, top 0.3s ease-in-out;
26 | }
27 |
28 | &::before {
29 | top: 0;
30 | }
31 |
32 | &::after {
33 | top: calc(100% - var(--burger-line-height));
34 | }
35 |
36 | &__line {
37 | position: absolute;
38 | left: 0;
39 | top: 50%;
40 | width: 100%;
41 | height: var(--burger-line-height);
42 | background-color: currentColor;
43 | transform: translateY(-50%);
44 | transition: transform 0.3s ease-in-out;
45 | }
46 |
47 | &--active {
48 | &::before {
49 | top: 50%;
50 | transform: rotate(45deg);
51 | transition: transform 0.3s ease-in-out, top 0.3s ease-in-out;
52 | }
53 |
54 | &::after {
55 | top: 50%;
56 | transform: rotate(-45deg);
57 | transition: transform 0.3s ease-in-out, top 0.3s ease-in-out;
58 | }
59 |
60 | .burger__line {
61 | transform: scale(0);
62 | transition: transform 0.3s ease-in-out;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/scss/mixins/_checkbox.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable */
2 |
3 | @mixin custom-checkbox {
4 | position: relative;
5 |
6 | &__field {
7 | appearance: none;
8 | position: absolute;
9 | }
10 |
11 | &__content {
12 | padding-left: 25px;
13 | cursor: pointer;
14 |
15 | &::before {
16 | content: "";
17 | position: absolute;
18 | left: 0;
19 | top: 0;
20 | border: 1px solid #000;
21 | width: 15px;
22 | height: 15px;
23 | box-sizing: border-box;
24 | }
25 |
26 | &::after {
27 | content: "";
28 | position: absolute;
29 | left: 0;
30 | top: 0;
31 | width: 15px;
32 | height: 15px;
33 | box-sizing: border-box;
34 | background-image: url("data:image/svg+xml,%3Csvg version='1.1' id='fi_32282' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='405.272px' height='405.272px' viewBox='0 0 405.272 405.272' style='enable-background:new 0 0 405.272 405.272;' xml:space='preserve'%3E%3Cg%3E%3Cpath d='M393.401,124.425L179.603,338.208c-15.832,15.835-41.514,15.835-57.361,0L11.878,227.836 c-15.838-15.835-15.838-41.52,0-57.358c15.841-15.841,41.521-15.841,57.355-0.006l81.698,81.699L336.037,67.064 c15.841-15.841,41.523-15.829,57.358,0C409.23,82.902,409.23,108.578,393.401,124.425z'%3E%3C/path%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3C/svg%3E");
35 | background-position: center;
36 | background-size: 10px 10px;
37 | background-repeat: no-repeat;
38 | opacity: 0;
39 | transition: opacity 0.3s ease-in-out;
40 | }
41 | }
42 | }
43 |
44 | .custom-checkbox__field:checked + .custom-checkbox__content::after {
45 | opacity: 1;
46 | }
47 |
48 | .custom-checkbox__field:focus + .custom-checkbox__content::before {
49 | outline: 2px solid #f00;
50 | outline-offset: 2px;
51 | }
52 |
53 | .custom-checkbox__field:disabled + .custom-checkbox__content {
54 | opacity: 0.4;
55 | pointer-events: none;
56 | }
57 |
--------------------------------------------------------------------------------
/src/scss/mixins/_disable-mob-hover.scss:
--------------------------------------------------------------------------------
1 | @mixin hover {
2 | @media (any-hover: hover) {
3 | &:hover {
4 | @content;
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/scss/mixins/_flex.scss:
--------------------------------------------------------------------------------
1 | @mixin flex {
2 | display: flex;
3 | }
4 |
5 | @mixin flex-v-center {
6 | display: flex;
7 | align-items: center;
8 | }
9 |
10 | @mixin flex-h-center {
11 | display: flex;
12 | justify-content: center;
13 | }
14 |
15 | @mixin flex-all-center {
16 | display: flex;
17 | align-items: center;
18 | justify-content: center;
19 | }
20 |
21 | @mixin flex-all-sb {
22 | display: flex;
23 | align-items: center;
24 | justify-content: space-between;
25 | }
26 |
--------------------------------------------------------------------------------
/src/scss/mixins/_font-face.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable */
2 | @mixin font-face($font-family, $url, $weight, $style) {
3 | @font-face {
4 | font-family: "#{$font-family}";
5 | src: url('../fonts/#{$url}.woff2') format("woff2");
6 | font-weight: #{$weight};
7 | font-display: swap;
8 | font-style: $style;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/scss/mixins/_layout.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable value-keyword-case */
2 | @use "sass:map";
3 |
4 | @mixin flex-layout($options) {
5 |
6 | .#{map.get($options, parentClass)} {
7 | --gap: #{map.get($options, desktopGap)};
8 | --elems: #{map.get($options, desktopElems)};
9 |
10 | display: flex;
11 | flex-wrap: wrap;
12 | gap: var(--gap);
13 | }
14 |
15 | .#{map.get($options, itemsClass)} {
16 | width: calc((100% - ((var(--elems) - 1) * var(--gap))) / (var(--elems)));
17 |
18 | @media (max-width: map.get($options, tablet)) {
19 | --gap: #{map.get($options, tabletGap)};
20 | --elems: #{map.get($options, tabletElems)};
21 | }
22 |
23 | @media (max-width: map.get($options, mobile)) {
24 | --gap: #{map.get($options, mobileGap)};
25 | --elems: #{map.get($options, mobileElems)};
26 | }
27 | }
28 | }
29 |
30 | // $options: (
31 | // parentClass: "cards",
32 | // itemsClass: "cards__item",
33 | // desktopGap: 30px,
34 | // desktopElems: 3,
35 | // tablet: "1024px",
36 | // tabletElems: 2,
37 | // tabletGap: 30px,
38 | // mobile: "600px",
39 | // mobileElems: 1,
40 | // mobileGap: 20px
41 | // );
42 |
43 | // @include flex-layout($options);
44 |
--------------------------------------------------------------------------------
/src/scss/mixins/_mini.scss:
--------------------------------------------------------------------------------
1 | /* stylelint-disable */
2 |
3 | @mixin bg-cover {
4 | background-position: center;
5 | background-size: cover;
6 | background-repeat: no-repeat;
7 | }
8 |
9 | $browser-context: 16; // Default
10 |
11 | @function rem($pixels, $context: $browser-context) {
12 | @return #{$pixels/$context}rem;
13 | }
14 |
15 | @mixin image-set($pathToImage) {
16 | background-image: url("#{$pathToImage}.jpg");
17 | background-image: image-set(
18 | "#{$pathToImage}.webp"type("image/webp"),
19 | "#{$pathToImage}.jpg"type("image/jpg")
20 | );
21 | }
22 |
23 | @mixin footerToBottom {
24 | display: grid;
25 | grid-template-columns: 100%;
26 | grid-template-rows: auto 1fr auto;
27 | min-height: 100vh;
28 | }
29 |
30 | @mixin mr($value) {
31 | &:not(:last-child) {
32 | margin-right: $value;
33 | }
34 | }
35 |
36 | @mixin ml($value) {
37 | &:not(:last-child) {
38 | margin-left: $value;
39 | }
40 | }
41 |
42 | @mixin mb($value) {
43 | &:not(:last-child) {
44 | margin-bottom: $value;
45 | }
46 | }
47 |
48 | @mixin mt($value) {
49 | &:not(:last-child) {
50 | margin-top: $value;
51 | }
52 | }
53 |
54 | @mixin pseudo() {
55 | content: "";
56 | display: block;
57 | }
58 |
--------------------------------------------------------------------------------
/src/scss/mixins/_tabs.scss:
--------------------------------------------------------------------------------
1 | @mixin tabs {
2 | .tabs__nav-btn--active {
3 | background-color: #ff0001;
4 | }
5 |
6 | .tabs__panel {
7 | display: none;
8 | }
9 |
10 | .tabs__panel--active {
11 | display: block;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/scss/vendor.scss:
--------------------------------------------------------------------------------
1 | @import "./vendor/normalize";
2 |
3 | // стили для модального окна
4 | // @import "../../node_modules/graph-modal/dist/graph-modal.min";
5 |
6 | // стили для табов
7 | // @import "../../node_modules/graph-tabs/dist/graph-tabs.min";
8 |
9 | // стили для библиотеки aos
10 | // @import "../../node_modules/aos/dist/aos";
11 |
12 | // стили для свайпера
13 | // @import "../../node_modules/swiper/swiper-bundle.min";
14 |
15 | // стили для скролла simplebar
16 | // @import "../../node_modules/simplebar/dist/simplebar.min";
17 |
--------------------------------------------------------------------------------
/src/scss/vendor/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15;
13 | /* 1 */
14 | -webkit-text-size-adjust: 100%;
15 | /* 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 | font-size: 2em;
44 | margin: 0.67em 0;
45 | }
46 |
47 | /* Grouping content
48 | ========================================================================== */
49 |
50 | /**
51 | * 1. Add the correct box sizing in Firefox.
52 | * 2. Show the overflow in Edge and IE.
53 | */
54 |
55 | hr {
56 | box-sizing: content-box;
57 | /* 1 */
58 | height: 0;
59 | /* 1 */
60 | overflow: visible;
61 | /* 2 */
62 | }
63 |
64 | /**
65 | * 1. Correct the inheritance and scaling of font size in all browsers.
66 | * 2. Correct the odd `em` font sizing in all browsers.
67 | */
68 |
69 | pre {
70 | font-family: monospace, monospace;
71 | /* 1 */
72 | font-size: 1em;
73 | /* 2 */
74 | }
75 |
76 | /* Text-level semantics
77 | ========================================================================== */
78 |
79 | /**
80 | * Remove the gray background on active links in IE 10.
81 | */
82 |
83 | a {
84 | background-color: transparent;
85 | }
86 |
87 | /**
88 | * 1. Remove the bottom border in Chrome 57-
89 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
90 | */
91 |
92 | abbr[title] {
93 | border-bottom: none;
94 | /* 1 */
95 | text-decoration: underline;
96 | /* 2 */
97 | text-decoration: underline dotted;
98 | /* 2 */
99 | }
100 |
101 | /**
102 | * Add the correct font weight in Chrome, Edge, and Safari.
103 | */
104 |
105 | b,
106 | strong {
107 | font-weight: bolder;
108 | }
109 |
110 | /**
111 | * 1. Correct the inheritance and scaling of font size in all browsers.
112 | * 2. Correct the odd `em` font sizing in all browsers.
113 | */
114 |
115 | code,
116 | kbd,
117 | samp {
118 | font-family: monospace, monospace;
119 | /* 1 */
120 | font-size: 1em;
121 | /* 2 */
122 | }
123 |
124 | /**
125 | * Add the correct font size in all browsers.
126 | */
127 |
128 | small {
129 | font-size: 80%;
130 | }
131 |
132 | /**
133 | * Prevent `sub` and `sup` elements from affecting the line height in
134 | * all browsers.
135 | */
136 |
137 | sub,
138 | sup {
139 | font-size: 75%;
140 | line-height: 0;
141 | position: relative;
142 | vertical-align: baseline;
143 | }
144 |
145 | sub {
146 | bottom: -0.25em;
147 | }
148 |
149 | sup {
150 | top: -0.5em;
151 | }
152 |
153 | /* Embedded content
154 | ========================================================================== */
155 |
156 | /**
157 | * Remove the border on images inside links in IE 10.
158 | */
159 |
160 | img {
161 | border-style: none;
162 | }
163 |
164 | /* Forms
165 | ========================================================================== */
166 |
167 | /**
168 | * 1. Change the font styles in all browsers.
169 | * 2. Remove the margin in Firefox and Safari.
170 | */
171 |
172 | button,
173 | input,
174 | optgroup,
175 | select,
176 | textarea {
177 | font-family: inherit;
178 | /* 1 */
179 | font-size: 100%;
180 | /* 1 */
181 | line-height: 1.15;
182 | /* 1 */
183 | margin: 0;
184 | /* 2 */
185 | }
186 |
187 | /**
188 | * Show the overflow in IE.
189 | * 1. Show the overflow in Edge.
190 | */
191 |
192 | button,
193 | input {
194 | /* 1 */
195 | overflow: visible;
196 | }
197 |
198 | /**
199 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
200 | * 1. Remove the inheritance of text transform in Firefox.
201 | */
202 |
203 | button,
204 | select {
205 | /* 1 */
206 | text-transform: none;
207 | }
208 |
209 | /**
210 | * Correct the inability to style clickable types in iOS and Safari.
211 | */
212 |
213 | button,
214 | [type="button"],
215 | [type="reset"],
216 | [type="submit"] {
217 | -webkit-appearance: button;
218 | }
219 |
220 | /**
221 | * Remove the inner border and padding in Firefox.
222 | */
223 |
224 | button::-moz-focus-inner,
225 | [type="button"]::-moz-focus-inner,
226 | [type="reset"]::-moz-focus-inner,
227 | [type="submit"]::-moz-focus-inner {
228 | border-style: none;
229 | padding: 0;
230 | }
231 |
232 | /**
233 | * Restore the focus styles unset by the previous rule.
234 | */
235 |
236 | button:-moz-focusring,
237 | [type="button"]:-moz-focusring,
238 | [type="reset"]:-moz-focusring,
239 | [type="submit"]:-moz-focusring {
240 | outline: 1px dotted ButtonText;
241 | }
242 |
243 | /**
244 | * Correct the padding in Firefox.
245 | */
246 |
247 | fieldset {
248 | padding: 0.35em 0.75em 0.625em;
249 | }
250 |
251 | /**
252 | * 1. Correct the text wrapping in Edge and IE.
253 | * 2. Correct the color inheritance from `fieldset` elements in IE.
254 | * 3. Remove the padding so developers are not caught out when they zero out
255 | * `fieldset` elements in all browsers.
256 | */
257 |
258 | legend {
259 | box-sizing: border-box;
260 | /* 1 */
261 | color: inherit;
262 | /* 2 */
263 | display: table;
264 | /* 1 */
265 | max-width: 100%;
266 | /* 1 */
267 | padding: 0;
268 | /* 3 */
269 | white-space: normal;
270 | /* 1 */
271 | }
272 |
273 | /**
274 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
275 | */
276 |
277 | progress {
278 | vertical-align: baseline;
279 | }
280 |
281 | /**
282 | * Remove the default vertical scrollbar in IE 10+.
283 | */
284 |
285 | textarea {
286 | overflow: auto;
287 | }
288 |
289 | /**
290 | * 1. Add the correct box sizing in IE 10.
291 | * 2. Remove the padding in IE 10.
292 | */
293 |
294 | [type="checkbox"],
295 | [type="radio"] {
296 | box-sizing: border-box;
297 | /* 1 */
298 | padding: 0;
299 | /* 2 */
300 | }
301 |
302 | /**
303 | * Correct the cursor style of increment and decrement buttons in Chrome.
304 | */
305 |
306 | [type="number"]::-webkit-inner-spin-button,
307 | [type="number"]::-webkit-outer-spin-button {
308 | height: auto;
309 | }
310 |
311 | /**
312 | * 1. Correct the odd appearance in Chrome and Safari.
313 | * 2. Correct the outline style in Safari.
314 | */
315 |
316 | [type="search"] {
317 | -webkit-appearance: textfield;
318 | /* 1 */
319 | outline-offset: -2px;
320 | /* 2 */
321 | }
322 |
323 | /**
324 | * Remove the inner padding in Chrome and Safari on macOS.
325 | */
326 |
327 | [type="search"]::-webkit-search-decoration {
328 | -webkit-appearance: none;
329 | }
330 |
331 | /**
332 | * 1. Correct the inability to style clickable types in iOS and Safari.
333 | * 2. Change font properties to `inherit` in Safari.
334 | */
335 |
336 | ::-webkit-file-upload-button {
337 | -webkit-appearance: button;
338 | /* 1 */
339 | font: inherit;
340 | /* 2 */
341 | }
342 |
343 | /* Interactive
344 | ========================================================================== */
345 |
346 | /*
347 | * Add the correct display in Edge, IE 10+, and Firefox.
348 | */
349 |
350 | details {
351 | display: block;
352 | }
353 |
354 | /*
355 | * Add the correct display in all browsers.
356 | */
357 |
358 | summary {
359 | display: list-item;
360 | }
361 |
362 | /* Misc
363 | ========================================================================== */
364 |
365 | /**
366 | * Add the correct display in IE 10+.
367 | */
368 |
369 | template {
370 | display: none;
371 | }
372 |
373 | /**
374 | * Add the correct display in IE 10.
375 | */
376 |
377 | [hidden] {
378 | display: none;
379 | }
--------------------------------------------------------------------------------