├── .babelrc ├── .eslintrc ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── TODO.md ├── changelog.md ├── dist ├── immerser.min.js └── immerser.min.js.map ├── docs ├── favicon.ico ├── index.html ├── main.css ├── main.js ├── main.js.map └── ru.html ├── example ├── content │ ├── code │ │ ├── cloning-event-listeners.html │ │ ├── handle-clone-hover.css │ │ ├── handle-dom-change.js │ │ ├── initialization.js │ │ ├── markup.html │ │ ├── styles.css │ │ ├── table.html │ │ └── table.md │ ├── highlighted-cloning-event-listeners.html │ ├── highlighted-handle-clone-hover.html │ ├── highlighted-handle-dom-change.html │ ├── highlighted-initialization.html │ ├── highlighted-markup.html │ └── highlighted-styles.html ├── favicon │ ├── favicon.gif │ ├── favicon.ico │ ├── favicon.jpg │ ├── favicon.png │ └── favicon.svg ├── index.html ├── main.js ├── styles │ ├── components │ │ ├── about.scss │ │ ├── code-highlight.scss │ │ ├── common.scss │ │ ├── emoji.scss │ │ ├── fixed.scss │ │ ├── footer.scss │ │ ├── header.scss │ │ ├── highlighter.scss │ │ ├── language.scss │ │ ├── logo.scss │ │ ├── menu.scss │ │ ├── pager.scss │ │ └── typography.scss │ ├── main.scss │ └── shared │ │ ├── _breakpoints.scss │ │ ├── _colors.scss │ │ ├── _globals.scss │ │ ├── _grid.scss │ │ └── _transitions.scss └── svg │ ├── how-it-works-face.svg │ ├── how-it-works-hand.svg │ ├── how-to-use.svg │ ├── options.svg │ ├── possibilities.svg │ └── why-immerser.svg ├── generateOptionsTables.js ├── how-to-highlight.md ├── i18n ├── en.js └── ru.js ├── jsconfig.json ├── package.json ├── postBuild.js ├── readme.js ├── src ├── defaults.js ├── immerser.js └── utils.js ├── webpack.config.docs.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8", "ie >= 11"] 9 | } 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // Это наш общий стайлгайд. Он будет дополняться 2 | { 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "parserOptions": { 10 | "parser": "babel-eslint", 11 | "sourceType": "module", 12 | "ecmaVersion": 2018 13 | }, 14 | "extends": ["eslint:recommended"], 15 | "rules": { 16 | // пробелы внутри квадратных скобок массива 17 | "array-bracket-spacing": ["error", "never"], 18 | // стрелка арроу функции обрамляется пробелами с обеих сторон 19 | "arrow-spacing": ["error", { "before": true, "after": true }], 20 | // Хотим, чтобы в конце строки многострочного массива или объека всегда была запятая 21 | // Чтобы при мультивыделении был единообразный конец строк 22 | "comma-dangle": ["error", "always-multiline"], 23 | // не дропаем кудрявые скобки в блоках 24 | "curly": ["error", "all"], 25 | // отступы в 2 пробела 26 | "indent": ["error", 2, { "SwitchCase": 1 }], 27 | // ключевые слова всегда отбиваем пробелами 28 | "keyword-spacing": ["error", { "before": true, "after": true }], 29 | // не используем alert, prompt, confirm 30 | "no-alert": "error", 31 | // не импортируем из одного файла по нескольку раз, чтобы не путаться в импортах 32 | "no-duplicate-imports": ["error", { "includeExports": true }], 33 | // не плодим больше 2 пустых строк в коде 34 | "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }], 35 | // не бросаем ошибку, но предупреждаем о ненужном экранировании 36 | "no-useless-escape": "warn", 37 | // используем var по назначению 38 | "no-var": "error", 39 | // всегда отбиваем кудрявые скобки в объектах пробелами 40 | "object-curly-spacing": ["error", "always"], 41 | // каждую переменную, константу отдельно объявляем, так лучше видно 42 | "one-var": ["error", { "var": "never", "let": "never", "const": "never" }], 43 | // стрелочные функции лучше читаются и автоматически получают контекст 44 | "prefer-arrow-callback": ["error"], 45 | // если не переназначаем переменную, лучше использовать константу 46 | "prefer-const": "error", 47 | // кавычки одинарные используем 48 | "quotes": ["error", "single", { "allowTemplateLiterals": true }], 49 | // явно вставляем точку с запятой в концах строк, где они подразумеваются движком 50 | "semi": ["error", "always"], 51 | // сортируем импортируемые модули внутри кудрявых скобок 52 | "sort-imports": ["error", { "ignoreDeclarationSort": true }], 53 | // пробелом отбиваем только асинхронную стелочную функцию 54 | "space-before-function-paren": [ 55 | "error", 56 | { 57 | "anonymous": "never", 58 | "named": "never", 59 | "asyncArrow": "always" 60 | } 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dev_dist/ 4 | /.cache 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.language": "en,ru", 3 | "cSpell.words": [ 4 | "BUNDLESIZE", 5 | "classname", 6 | "dubaua", 7 | "gzipped", 8 | "immerser", 9 | "Lysov", 10 | "pseudoselector", 11 | "rsquo", 12 | "statemap", 13 | "гитхаб", 14 | "джаваскрипт", 15 | "джаваскрипте", 16 | "иммёрсер", 17 | "иммёрсера", 18 | "колбек", 19 | "нодам", 20 | "псевдоселектор", 21 | "псевдоселектором", 22 | "скролле", 23 | "скроллом", 24 | "скроллу" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Library for Switching Fixed Elements on Scroll 2 | 3 | Sometimes designers create complex logic and fix parts of the interface. Also they colour page sections contrasted. How to deal with this mess? 4 | 5 | Immerser comes to help you. It’s a javascript library to change fixed elements on scroll. 6 | 7 | Immerser fast, because it calculates states once on init. Then it watches the scroll position and schedules redraw document in the next event loop tick with requestAnimationFrame. Script changes transform property, so it uses graphic hardware acceleration. 8 | 9 | Immerser is written on vanilla js. Only 5.39Kb gzipped. 10 | 11 | ## Terms 12 | 13 | `Immerser root` — is the parent container for your fixed parts `solids`. Actually, solids are positioned absolutely to fixed immerser root. The `layers` are sections of your page. Also you may want to add `pager` to navigate through layers and indicate active state. 14 | 15 | # How to Use 16 | 17 | ## Install 18 | 19 | Using npm: 20 | 21 | ```shell 22 | npm install immerser 23 | ``` 24 | 25 | Using yarn: 26 | 27 | ```shell 28 | yarn add immerser 29 | ``` 30 | 31 | Or if you want to use immerser in browser as global variable: 32 | 33 | ```html 34 | 35 | ``` 36 | 37 | ## Prepare Your Markup 38 | 39 | First, setup fixed container as the immerser root container, and add the `data-immerser` attribute. 40 | 41 | Next place absolutely positioned children into the immerser parent and add `data-immerser-solid="solid-id"` to each. 42 | 43 | Then add `data-immerser-layer` attribute to each section and pass configuration in `data-immerser-layer-config='{"solid-id": "classname-modifier"}'`. Otherwise, you can pass configuration as `solidClassnameArray` option to immerser. Config should contain JSON describing what class should be applied on each solid element, when it's over a section. 44 | 45 | Also feel free to add `data-immerser-pager` to create a pager for your layers. 46 | 47 | ```html 48 |
<%= getTranslation('option') %> | 5 |<%= getTranslation('type') %> | 6 |<%= getTranslation('default') %> | 7 |<%= getTranslation('description') %> | 8 |
---|---|---|---|
solidClassnameArray | 13 |array | 14 |[] | 15 |<%= getTranslation('option-solidClassnameArray') %> | 16 |
fromViewportWidth | 19 |number | 20 |0 | 21 |<%= getTranslation('option-fromViewportWidth') %> | 22 |
pagerThreshold | 25 |number | 26 |0.5 | 27 |<%= getTranslation('option-pagerThreshold') %> | 28 |
hasToUpdateHash | 31 |boolean | 32 |false | 33 |<%= getTranslation('option-hasToUpdateHash') %> | 34 |
scrollAdjustThreshold | 37 |number | 38 |0 | 39 |<%= getTranslation('option-scrollAdjustThreshold') %> | 40 |
scrollAdjustDelay | 43 |number | 44 |600 | 45 |<%= getTranslation('option-scrollAdjustDelay') %> | 46 |
pagerLinkActiveClassname | 49 |string | 50 |pager-link-active | 51 |<%= getTranslation('option-pagerLinkActiveClassname') %> | 52 |
isScrollHandled | 55 |boolean | 56 |true | 57 |<%= getTranslation('option-isScrollHandled') %> | 58 |
onInit | 61 |function | 62 |null | 63 |<%= getTranslation('option-onInit') %> | 64 |
onBind | 67 |function | 68 |null | 69 |<%= getTranslation('option-onBind') %> | 70 |
onUnbind | 73 |function | 74 |null | 75 |<%= getTranslation('option-onUnbind') %> | 76 |
onDestroy | 79 |function | 80 |null | 81 |<%= getTranslation('option-onDestroy') %> | 82 |
onActiveLayerChange | 85 |function | 86 |null | 87 |<%= getTranslation('option-onActiveLayerChange') %> | 88 |
<div class="fixed" data-immerser>
2 | <div data-immerser-mask>
3 | <div data-immerser-mask-inner>
4 | <!-- <%= getTranslation('your-markup') %> -->
5 | </div>
6 | </div>
7 | <div data-immerser-mask>
8 | <div data-immerser-mask-inner>
9 | <!-- <%= getTranslation('your-markup') %> -->
10 | </div>
11 | </div>
12 | </div>
--------------------------------------------------------------------------------
/example/content/highlighted-handle-clone-hover.html:
--------------------------------------------------------------------------------
1 | a:hover,
2 | a._hover {
3 | color: magenta;
4 | }
--------------------------------------------------------------------------------
/example/content/highlighted-handle-dom-change.html:
--------------------------------------------------------------------------------
1 | // adding or removing node, that changes DOM height
2 | document.appendChild(someNode);
3 | document.removeChild(anotherNode);
4 |
5 | // then explicitly redraw immerser
6 | immerserInstance.onDOMChange();
--------------------------------------------------------------------------------
/example/content/highlighted-initialization.html:
--------------------------------------------------------------------------------
1 | // <%= getTranslation('dont-import-if-umd-line-1') %>
2 | // <%= getTranslation('dont-import-if-umd-line-2') %>
3 | import Immerser from 'immerser';
4 |
5 | const immerserInstance = new Immerser({
6 | // <%= getTranslation('data-attribute-will-override-this-option-line-1') %>
7 | // <%= getTranslation('data-attribute-will-override-this-option-line-2') %>
8 | solidClassnameArray: [
9 | {
10 | logo: 'logo--contrast-lg',
11 | pager: 'pager--contrast-lg',
12 | language: 'language--contrast-lg',
13 | },
14 | {
15 | pager: 'pager--contrast-only-md',
16 | menu: 'menu--contrast',
17 | about: 'about--contrast',
18 | },
19 | {
20 | logo: 'logo--contrast-lg',
21 | pager: 'pager--contrast-lg',
22 | language: 'language--contrast-lg',
23 | },
24 | {
25 | logo: 'logo--contrast-only-md',
26 | pager: 'pager--contrast-only-md',
27 | language: 'language--contrast-only-md',
28 | menu: 'menu--contrast',
29 | about: 'about--contrast',
30 | },
31 | {
32 | logo: 'logo--contrast-lg',
33 | pager: 'pager--contrast-lg',
34 | language: 'language--contrast-lg',
35 | },
36 | ],
37 | hasToUpdateHash: true,
38 | fromViewportWidth: 1024,
39 | pagerLinkActiveClassname: 'pager__link--active',
40 | scrollAdjustThreshold: 50,
41 | scrollAdjustDelay: 600,
42 | onInit(immerser) {
43 | // <%= getTranslation('callback-on-init') %>
44 | },
45 | onBind(immerser) {
46 | // <%= getTranslation('callback-on-bind') %>
47 | },
48 | onUnbind(immerser) {
49 | // <%= getTranslation('callback-on-unbind') %>
50 | },
51 | onDestroy(immerser) {
52 | // <%= getTranslation('callback-on-destroy') %>
53 | },
54 | onActiveLayerChange(activeIndex, immerser) {
55 | // <%= getTranslation('callback-on-active-layer-change') %>
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/example/content/highlighted-markup.html:
--------------------------------------------------------------------------------
1 | <div class="fixed" data-immerser>
2 | <div class="fixed__pager pager" data-immerser-pager data-immerser-solid="pager"></div>
3 | <a href="#reasoning" class="fixed__logo logo" data-immerser-solid="logo"><%= getTranslation('immerser') %></a>
4 | <div class="fixed__menu menu" data-immerser-solid="menu">
5 | <a href="#reasoning" class="menu__link"><%= getTranslation('menu-link-reasoning') %></a>
6 | <a href="#how-to-use" class="menu__link"><%= getTranslation('menu-link-how-to-use') %></a>
7 | <a href="#how-it-works" class="menu__link"><%= getTranslation('menu-link-how-it-works') %></a>
8 | <a href="#options" class="menu__link"><%= getTranslation('menu-link-options') %></a>
9 | <a href="#recipes" class="menu__link"><%= getTranslation('menu-link-recipes') %></a>
10 | </div>
11 | <div class="fixed__language language" data-immerser-solid="language">
12 | <a href="/" class="language__link">english</a>
13 | <a href="/ru.html" class="language__link">по-русски</a>
14 | </div>
15 | <div class="fixed__about about" data-immerser-solid="about">
16 | <%= getTranslation('copyright') %>
17 | <a href="https://github.com/dubaua/immerser"><%= getTranslation('github') %></a>
18 | <a href="mailto:dubaua@gmail.com">dubaua@gmail.com</a>
19 | </div>
20 | </div>
21 |
22 | <div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="reasoning"></div>
23 | <div data-immerser-layer data-immerser-layer-config='{"menu": "menu--contrast", "about": "about--contrast"}' id="how-to-use"></div>
24 | <div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="how-it-works"></div>
25 | <div data-immerser-layer data-immerser-layer-config='{"menu": "menu--contrast", "about": "about--contrast"}' id="options"></div>
26 | <div data-immerser-layer data-immerser-layer-config='{"logo": "logo--contrast", "pager": "pager--contrast", "social": "social--contrast"}' id="recipes"></div>
27 |
--------------------------------------------------------------------------------
/example/content/highlighted-styles.html:
--------------------------------------------------------------------------------
1 | .fixed {
2 | position: fixed;
3 | top: 2em;
4 | bottom: 3em;
5 | left: 3em;
6 | right: 3em;
7 | z-index: 1;
8 | }
9 | .fixed__pager {
10 | position: absolute;
11 | top: 50%;
12 | left: 0;
13 | transform: translate(0, -50%);
14 | }
15 | .fixed__logo {
16 | position: absolute;
17 | top: 0;
18 | left: 0;
19 | }
20 | .fixed__menu {
21 | position: absolute;
22 | top: 0;
23 | right: 0;
24 | }
25 | .fixed__language {
26 | position: absolute;
27 | bottom: 0;
28 | left: 0;
29 | }
30 | .fixed__about {
31 | position: absolute;
32 | bottom: 0;
33 | right: 0;
34 | }
35 | .pager,
36 | .logo,
37 | .menu,
38 | .language,
39 | .about {
40 | color: black;
41 | }
42 | .pager--contrast,
43 | .logo--contrast,
44 | .menu--contrast,
45 | .language--contrast,
46 | .about--contrast {
47 | color: white;
48 | }
49 |
--------------------------------------------------------------------------------
/example/favicon/favicon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/example/favicon/favicon.gif
--------------------------------------------------------------------------------
/example/favicon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/example/favicon/favicon.ico
--------------------------------------------------------------------------------
/example/favicon/favicon.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/example/favicon/favicon.jpg
--------------------------------------------------------------------------------
/example/favicon/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dubaua/immerser/85f14f92cc903f68a2ec27be216ea14704fbc8c4/example/favicon/favicon.png
--------------------------------------------------------------------------------
/example/favicon/favicon.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/example/main.js:
--------------------------------------------------------------------------------
1 | import Immerser from '../dist/immerser.min.js';
2 | import SimpleBar from 'simplebar';
3 | import './styles/main.scss';
4 | // import Prism from 'prismjs';
5 |
6 | const scrollbarNodeList = document.querySelectorAll('.scroller-x');
7 | for (let i = 0; i < scrollbarNodeList.length; i++) {
8 | const scrollbarNode = scrollbarNodeList[i];
9 | new SimpleBar(scrollbarNode, { autoHide: false });
10 | }
11 |
12 | const immerserInstance = new Immerser({
13 | solidClassnameArray: [
14 | {
15 | logo: 'logo--contrast-lg',
16 | pager: 'pager--contrast-lg',
17 | language: 'language--contrast-lg',
18 | },
19 | {
20 | pager: 'pager--contrast-only-md',
21 | menu: 'menu--contrast',
22 | about: 'about--contrast',
23 | },
24 | {
25 | logo: 'logo--contrast-lg',
26 | pager: 'pager--contrast-lg',
27 | language: 'language--contrast-lg',
28 | },
29 | {
30 | logo: 'logo--contrast-only-md',
31 | pager: 'pager--contrast-only-md',
32 | language: 'language--contrast-only-md',
33 | menu: 'menu--contrast',
34 | about: 'about--contrast',
35 | },
36 | {
37 | logo: 'logo--contrast-lg',
38 | pager: 'pager--contrast-lg',
39 | language: 'language--contrast-lg',
40 | },
41 | ],
42 | fromViewportWidth: 1024,
43 | pagerLinkActiveClassname: 'pager__link--active',
44 | scrollAdjustThreshold: 50,
45 | scrollAdjustDelay: 600,
46 | onInit(immerser) {
47 | window.imm = immerser;
48 | console.log('onInit', immerser);
49 | },
50 | onBind(immerser) {
51 | console.log('onBind', immerser);
52 | },
53 | onUnbind(immerser) {
54 | console.log('onUnbind', immerser);
55 | },
56 | onDestroy(immerser) {
57 | console.log('onDestroy', immerser);
58 | },
59 | onActiveLayerChange(activeIndex, immerser) {
60 | console.log('onActiveLayerChange', activeIndex, immerser);
61 | },
62 | });
63 |
64 | const highlighterNodeList = document.querySelectorAll('[data-highlighter]');
65 | const highlighterAnimationClassname = 'highlighter-animation-active';
66 |
67 | function highlight(highlighterNode) {
68 | return () => {
69 | if (!immerserInstance.isBound) {
70 | return;
71 | }
72 | const targetSelector = highlighterNode.dataset.highlighter;
73 | const targetNodeList = document.querySelectorAll(targetSelector);
74 | for (let j = 0; j < targetNodeList.length; j++) {
75 | const targetNode = targetNodeList[j];
76 | if (!targetNode.isHighlighting) {
77 | targetNode.isHighlighting = true;
78 | targetNode.classList.add(highlighterAnimationClassname);
79 | const timerId = setTimeout(() => {
80 | targetNode.classList.remove(highlighterAnimationClassname);
81 | clearTimeout(timerId);
82 | targetNode.isHighlighting = false;
83 | }, 1500);
84 | }
85 | }
86 | };
87 | }
88 |
89 | for (let i = 0; i < highlighterNodeList.length; i++) {
90 | const highlighterNode = highlighterNodeList[i];
91 | highlighterNode.addEventListener('mouseover', highlight(highlighterNode));
92 | highlighterNode.addEventListener('click', highlight(highlighterNode));
93 | }
94 |
95 | const emojiNodeList = document.querySelectorAll('[data-emoji-animating]');
96 | for (let i = 0; i < emojiNodeList.length; i++) {
97 | const emojiNode = emojiNodeList[i];
98 | emojiNode.addEventListener('click', () => {
99 | if (emojiNode.dataset.emojiAnimating === 'false') {
100 | emojiNode.dataset.emojiAnimating = 'true';
101 | setTimeout(() => {
102 | emojiNode.dataset.emojiAnimating = 'false';
103 | }, 620);
104 | }
105 | });
106 | }
107 |
108 | const rulersNode = document.getElementById('rulers');
109 | document.addEventListener('keydown', ({ altKey, code, keyCode }) => {
110 | const isR = code === 'KeyR' || keyCode === 82;
111 | if (altKey && isR) {
112 | rulersNode.classList.toggle('rulers--active');
113 | }
114 | });
115 |
116 | console.log('welcome here, fella. Press Alt+R to see vertical rhythm');
117 |
118 | window.immerserInstance = immerserInstance;
119 |
--------------------------------------------------------------------------------
/example/styles/components/about.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .about {
4 | padding-bottom: $base - 2px;
5 | margin: 0 $base * -1;
6 |
7 | @at-root #{$cyrillic-modifier} & {
8 | padding-bottom: $base - 1px;
9 | font-size: 14px;
10 | letter-spacing: -0.01em;
11 | }
12 | color: $color-text;
13 | &--contrast {
14 | color: $color-text--contrast;
15 | }
16 | span, a {
17 | display: inline-block;
18 | padding-left: $base;
19 | color: inherit;
20 | @include transition('color');
21 | @include from('lg') {
22 | padding-right: $base;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/example/styles/components/code-highlight.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | $code-color-muted: #b0b0b0;
4 | $code-color-muted--contrast: #404040;
5 |
6 | $code-color-sintax: scale-color($color-accent, $lightness: 20%, $saturation: -38%);
7 | $code-color-sintax--contrast: scale-color($color-accent, $lightness: -20%, $saturation: -62%);
8 |
9 | $code-color-name: scale-color($color-highlight, $lightness: -20%, $saturation: -62%);
10 | $code-color-name--contrast: scale-color($color-highlight, $lightness: -20%, $saturation: -62%);
11 |
12 | $code-color-punctuation: scale-color($color-highlight, $lightness: -50%, $saturation: -90%);
13 | $code-color-punctuation--contrast: scale-color($color-highlight, $lightness: 75%, $saturation: -90%);
14 |
15 | $code-color-text: scale-color($color-highlight, $lightness: -50%, $saturation: -90%);
16 | $code-color-text--contrast: scale-color($color-highlight, $lightness: 75%, $saturation: -90%);
17 |
18 | $code-color-value: scale-color($color-highlight, $lightness: -75%, $saturation: -80%);
19 | $code-color-value--contrast: scale-color($color-highlight, $lightness: 62%, $saturation: -80%);
20 |
21 | $code-color-function: scale-color($color-accent, $lightness: 0%, $saturation: 0%);
22 | $code-color-function--contrast: scale-color($color-accent, $lightness: 30%, $saturation: -38%);
23 |
24 |
25 | $theme-prolog: $code-color-muted;
26 | $theme-doctype: $code-color-muted;
27 | $theme-comment: $code-color-muted;
28 | $theme-cdata: $code-color-muted;
29 |
30 | $theme-attr-name: $code-color-name;
31 | $theme-property: $code-color-name;
32 |
33 | $theme-selector: $code-color-sintax;
34 | $theme-tag: $code-color-sintax;
35 | $theme-keyword: $code-color-sintax;
36 |
37 | $theme-attr-value: $code-color-value;
38 | $theme-boolean: $code-color-value;
39 | $theme-string: $code-color-value;
40 | $theme-class-name: $code-color-value;
41 | $theme-number: $code-color-value;
42 |
43 | $theme-punctuation: $code-color-punctuation;
44 | $theme-operator: $code-color-punctuation;
45 |
46 | $theme-function: $code-color-function;
47 | $theme-deleted: $code-color-function;
48 | $theme-important: $code-color-function;
49 |
50 | $theme-text: $code-color-text;
51 |
52 | $theme-atrule: red;
53 | $theme-builtin: red;
54 | $theme-char: red;
55 | $theme-constant: red;
56 | $theme-entity: red;
57 | $theme-inserted: red;
58 | $theme-regex: red;
59 | $theme-symbol: red;
60 | $theme-url: red;
61 | $theme-variable: red;
62 |
63 | .code-highlight {
64 | padding: $base * 1.5 0;
65 | color: $theme-text;
66 |
67 | &--inline {
68 | margin-top: 0;
69 | padding-bottom: 0;
70 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
71 | }
72 | pre {
73 | margin: 0;
74 | font-size: $base;
75 | line-height: $base * 1.5;
76 | }
77 | code {
78 | font-size: $base !important;
79 | line-height: $base * 1.5;
80 | }
81 | }
82 |
83 | code[class*='language-'],
84 | pre[class*='language-'] {
85 | background: none;
86 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
87 | font-size: 1em;
88 | text-align: left;
89 | white-space: pre;
90 | word-spacing: normal;
91 | word-break: normal;
92 | word-wrap: normal;
93 | line-height: $base * 1.5 - 1px;
94 |
95 | -moz-tab-size: 4;
96 | -o-tab-size: 4;
97 | tab-size: 4;
98 |
99 | -webkit-hyphens: none;
100 | -moz-hyphens: none;
101 | -ms-hyphens: none;
102 | hyphens: none;
103 | }
104 |
105 | /* Code blocks */
106 | pre[class*='language-'] {
107 | padding: 1em;
108 | margin: 0.5em 0;
109 | overflow: auto;
110 | border-radius: 0.3em;
111 | }
112 |
113 | :not(pre) > code[class*='language-'],
114 | pre[class*='language-'] {
115 | background: #272822;
116 | }
117 |
118 | /* Inline code */
119 | :not(pre) > code[class*='language-'] {
120 | padding: 0.1em;
121 | border-radius: 0.3em;
122 | white-space: normal;
123 | }
124 |
125 | .namespace {
126 | opacity: 0.7;
127 | }
128 |
129 | .token.important,
130 | .token.bold {
131 | font-weight: bold;
132 | }
133 |
134 | .token.italic {
135 | font-style: italic;
136 | }
137 |
138 | .token.entity {
139 | cursor: help;
140 | }
141 |
142 | .token.atrule {
143 | color: $theme-atrule;
144 | }
145 | .token.attr-name {
146 | color: $theme-attr-name;
147 | }
148 | .token.attr-value,
149 | .language-css.token.string .style.token.string {
150 | color: $theme-attr-value;
151 | }
152 | .token.boolean {
153 | color: $theme-boolean;
154 | }
155 | .token.builtin {
156 | color: $theme-builtin;
157 | }
158 | .token.cdata {
159 | color: $theme-cdata;
160 | }
161 | .token.char {
162 | color: $theme-char;
163 | }
164 | .token.class-name {
165 | color: $theme-class-name;
166 | }
167 | .token.comment {
168 | color: $theme-comment;
169 | }
170 | .token.constant {
171 | color: $theme-constant;
172 | }
173 | .token.deleted {
174 | color: $theme-deleted;
175 | }
176 | .token.doctype {
177 | color: $theme-doctype;
178 | }
179 | .token.entity {
180 | color: $theme-entity;
181 | }
182 | .token.function {
183 | color: $theme-function;
184 | }
185 | .token.important {
186 | color: $theme-important;
187 | }
188 | .token.inserted {
189 | color: $theme-inserted;
190 | }
191 | .token.keyword {
192 | color: $theme-keyword;
193 | }
194 | .token.number {
195 | color: $theme-number;
196 | }
197 | .token.operator {
198 | color: $theme-operator;
199 | }
200 | .token.prolog {
201 | color: $theme-prolog;
202 | }
203 | .token.property {
204 | color: $theme-property;
205 | }
206 | .token.punctuation {
207 | color: $theme-punctuation;
208 | }
209 | .token.regex {
210 | color: $theme-regex;
211 | }
212 | .token.selector {
213 | color: $theme-selector;
214 | }
215 | .token.string {
216 | color: $theme-string;
217 | }
218 | .token.symbol {
219 | color: $theme-symbol;
220 | }
221 | .token.tag {
222 | color: $theme-tag;
223 | }
224 | .token.url {
225 | color: $theme-url;
226 | }
227 | .token.variable {
228 | color: $theme-variable;
229 | }
230 |
231 | .code-highlight--contrast {
232 | $theme-prolog: $code-color-muted--contrast;
233 | $theme-doctype: $code-color-muted--contrast;
234 | $theme-comment: $code-color-muted--contrast;
235 | $theme-cdata: $code-color-muted--contrast;
236 |
237 | $theme-attr-name: $code-color-name--contrast;
238 | $theme-property: $code-color-name--contrast;
239 |
240 | $theme-selector: $code-color-sintax--contrast;
241 | $theme-tag: $code-color-sintax--contrast;
242 | $theme-keyword: $code-color-sintax--contrast;
243 |
244 | $theme-attr-value: $code-color-value--contrast;
245 | $theme-boolean: $code-color-value--contrast;
246 | $theme-string: $code-color-value--contrast;
247 | $theme-class-name: $code-color-value--contrast;
248 | $theme-number: $code-color-value--contrast;
249 |
250 | $theme-punctuation: $code-color-punctuation--contrast;
251 | $theme-operator: $code-color-punctuation--contrast;
252 |
253 | $theme-function: $code-color-function--contrast;
254 | $theme-deleted: $code-color-function--contrast;
255 | $theme-important: $code-color-function--contrast;
256 |
257 | $theme-text: $code-color-text--contrast;
258 |
259 | color: $theme-text;
260 | background-color: $color-background--contrast;
261 |
262 | code[class*='language-'],
263 | pre[class*='language-'] {
264 | color: $theme-text;
265 | }
266 |
267 | .token.important,
268 | .token.bold {
269 | font-weight: bold;
270 | }
271 |
272 | .token.italic {
273 | font-style: italic;
274 | }
275 |
276 | .token.entity {
277 | cursor: help;
278 | }
279 |
280 | .token.atrule {
281 | color: $theme-atrule;
282 | }
283 | .token.attr-name {
284 | color: $theme-attr-name;
285 | }
286 | .token.attr-value,
287 | .language-css.token.string .style.token.string {
288 | color: $theme-attr-value;
289 | }
290 | .token.boolean {
291 | color: $theme-boolean;
292 | }
293 | .token.builtin {
294 | color: $theme-builtin;
295 | }
296 | .token.cdata {
297 | color: $theme-cdata;
298 | }
299 | .token.char {
300 | color: $theme-char;
301 | }
302 | .token.class-name {
303 | color: $theme-class-name;
304 | }
305 | .token.comment {
306 | color: $theme-comment;
307 | }
308 | .token.constant {
309 | color: $theme-constant;
310 | }
311 | .token.deleted {
312 | color: $theme-deleted;
313 | }
314 | .token.doctype {
315 | color: $theme-doctype;
316 | }
317 | .token.entity {
318 | color: $theme-entity;
319 | }
320 | .token.function {
321 | color: $theme-function;
322 | }
323 | .token.important {
324 | color: $theme-important;
325 | }
326 | .token.inserted {
327 | color: $theme-inserted;
328 | }
329 | .token.keyword {
330 | color: $theme-keyword;
331 | }
332 | .token.number {
333 | color: $theme-number;
334 | }
335 | .token.operator {
336 | color: $theme-operator;
337 | }
338 | .token.prolog {
339 | color: $theme-prolog;
340 | }
341 | .token.property {
342 | color: $theme-property;
343 | }
344 | .token.punctuation {
345 | color: $theme-punctuation;
346 | }
347 | .token.regex {
348 | color: $theme-regex;
349 | }
350 | .token.selector {
351 | color: $theme-selector;
352 | }
353 | .token.string {
354 | color: $theme-string;
355 | }
356 | .token.symbol {
357 | color: $theme-symbol;
358 | }
359 | .token.tag {
360 | color: $theme-tag;
361 | }
362 | .token.url {
363 | color: $theme-url;
364 | }
365 | .token.variable {
366 | color: $theme-variable;
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/example/styles/components/common.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | html {
4 | scroll-behavior: smooth;
5 | font-family: 'Questrial', 'Montserrat', sans-serif;
6 | letter-spacing: 0.01em;
7 |
8 | {$cyrillic-modifier} {
9 | font-family: 'Montserrat', sans-serif;
10 | }
11 | }
12 |
13 | *::selection {
14 | background: $color-marker;
15 | color: $color-secondary;
16 | }
17 | *::-moz-selection {
18 | background: $color-marker;
19 | color: $color-secondary;
20 | }
21 |
22 | @include from('md') {
23 | .tall {
24 | box-sizing: border-box;
25 | min-height: 100vh;
26 | }
27 |
28 | .start {
29 | padding-top: $base * 4.875;
30 | @include from('lg') {
31 | padding-top: $base * 6.375;
32 | }
33 | }
34 |
35 | .end {
36 | padding-bottom: $base * 5;
37 | }
38 |
39 | .as-if-title {
40 | height: $base * 6;
41 | }
42 | }
43 |
44 | .background {
45 | background: $color-background;
46 | color: $color-background--contrast;
47 | &--contrast {
48 | background: $color-background--contrast;
49 | color: $color-text--contrast;
50 | }
51 | }
52 |
53 | .code-background {
54 | background-color: $color-code-background;
55 | @include from('md') {
56 | background-color: transparent;
57 | }
58 | }
59 |
60 | .scroller-x {
61 | overflow-y: hidden;
62 |
63 | &.background .simplebar-scrollbar:before {
64 | background: scale-color($color-accent, $lightness: 62%, $saturation: -62%);
65 | }
66 | &.background .simplebar-scrollbar.simplebar-hover:before {
67 | background: scale-color($color-accent, $lightness: 62%+5%, $saturation: -62%+5%);
68 | }
69 |
70 | &.background--contrast .simplebar-scrollbar:before {
71 | background: scale-color($color-highlight, $lightness: -62%, $saturation: -62%);
72 | }
73 | &.background--contrast .simplebar-scrollbar.simplebar-hover:before {
74 | background: scale-color($color-highlight, $lightness: -62%+5%, $saturation: -62%+5%);
75 | }
76 |
77 | .simplebar-track.simplebar-horizontal {
78 | height: $base;
79 | .simplebar-scrollbar {
80 | height: $base;
81 | top: 0;
82 | &.simplebar-visible:before {
83 | border-radius: 0;
84 | height: $base;
85 | left: 0;
86 | right: 0;
87 | opacity: 1;
88 | }
89 | }
90 | }
91 | }
92 |
93 | a:hover,
94 | a._hover {
95 | color: $color-active !important;
96 | }
97 |
98 | .rulers {
99 | $ruler-color: rgba(magenta, 0.3);
100 | $horizontal-ruler-gutter: $base * 1.5;
101 | $vertical-ruler-gutter: 4.1666666666666664%;
102 | position: fixed;
103 | top: 0;
104 | right: 0;
105 | bottom: 0;
106 | left: 0;
107 | pointer-events: none;
108 |
109 | display: none;
110 | &--active {
111 | display: block;
112 | }
113 |
114 | background-image: repeating-linear-gradient(
115 | to bottom,
116 | transparent,
117 | transparent $horizontal-ruler-gutter - 1,
118 | $ruler-color $horizontal-ruler-gutter - 1,
119 | $ruler-color $horizontal-ruler-gutter
120 | );
121 |
122 | &:after {
123 | content: '';
124 | position: fixed;
125 | z-index: 1;
126 | top: 0;
127 | height: 100%;
128 | left: 0;
129 | width: 100%;
130 | pointer-events: none;
131 | background-image: repeating-linear-gradient(
132 | to right,
133 | transparent,
134 | transparent calc(#{$vertical-ruler-gutter / 2} - 1px),
135 | $ruler-color calc(#{$vertical-ruler-gutter / 2} - 1px),
136 | $ruler-color $vertical-ruler-gutter / 2
137 | );
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/example/styles/components/emoji.scss:
--------------------------------------------------------------------------------
1 | .emoji {
2 | $emoji: &;
3 | position: relative;
4 | &__face {
5 | display: block;
6 | }
7 | &__hand {
8 | position: absolute;
9 | left: 0;
10 | top: 0;
11 | }
12 | &[data-emoji-animating='true'] #{$emoji}__face {
13 | animation: spinning 0.62s ease-in-out;
14 | }
15 | &[data-emoji-animating='true'] #{$emoji}__hand {
16 | transform-origin: left bottom;
17 | animation: hang 0.62s ease-in-out;
18 | }
19 | }
20 |
21 | @keyframes spinning {
22 | 100% {
23 | transform: rotate(360deg);
24 | }
25 | }
26 |
27 | @keyframes hang {
28 | 50% {
29 | transform: rotate(-3deg);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example/styles/components/fixed.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .fixed {
4 | display: none;
5 | @include from('md') {
6 | position: fixed;
7 | display: block;
8 | top: $base * 2;
9 | bottom: $base * 2;
10 | left: $col-width * 0.5;
11 | right: $col-width * 0.5;
12 | z-index: 2;
13 |
14 | &__pager {
15 | position: absolute;
16 | top: 50%;
17 | right: 0;
18 | @include from('lg') {
19 | right: auto;
20 | left: 0;
21 | }
22 | transform: translate(0, -50%);
23 | }
24 |
25 | &__logo {
26 | position: absolute;
27 | top: 0;
28 | left: 0;
29 | }
30 |
31 | &__menu {
32 | position: absolute;
33 | top: 0;
34 | right: 0;
35 | }
36 |
37 | &__language {
38 | position: absolute;
39 | bottom: 0;
40 | left: 0;
41 | }
42 |
43 | &__about {
44 | position: absolute;
45 | bottom: 0;
46 | right: 0;
47 | }
48 |
49 | &__emoji {
50 | position: absolute;
51 | right: 0;
52 | top: 50%;
53 | transform: translate(0, -50%);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/example/styles/components/footer.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .footer {
4 | margin-top: $base * 1.5;
5 | padding-top: $base * 1.5;
6 | padding-bottom: $base * 1.5;
7 | line-height: $base * 1.5;
8 | @include show-from-to('xs', 'md') {
9 | padding-left: $col-width * 2;
10 | padding-right: $col-width * 2;
11 | @include from('sm') {
12 | padding-left: $col-width;
13 | padding-right: $col-width;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/example/styles/components/header.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .header {
4 | padding-top: $base * 2;
5 | padding-bottom: $base * 0.28;
6 | @include show-from-to('xs', 'md') {
7 | padding-left: $col-width * 2;
8 | padding-right: $col-width * 2;
9 | @include from('sm') {
10 | padding-left: $col-width;
11 | padding-right: $col-width;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/example/styles/components/highlighter.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | @include from('md') {
4 | .highlighter,
5 | p code.highlighter {
6 | cursor: pointer;
7 | text-decoration: underline;
8 | text-decoration-line: underline;
9 | text-decoration-style: double;
10 | text-decoration-color: $color-highlight;
11 | font-size: inherit;
12 | font-family: inherit;
13 | padding: 0;
14 | background: none;
15 | }
16 |
17 | .highlighter-animation-active {
18 | animation: highlight 1.5s linear infinite;
19 | &[data-immerser-layer] {
20 | position: relative;
21 | animation: none;
22 | &:after {
23 | content: '';
24 | position: absolute;
25 | top: 0;
26 | bottom: 0;
27 | left: 0;
28 | right: 0;
29 | pointer-events: none;
30 | animation: highlight 1.5s linear infinite;
31 | }
32 | }
33 | }
34 |
35 | @keyframes highlight {
36 | 25% {
37 | background: rgba($color-highlight, 0.5);
38 | }
39 | 50% {
40 | background: transparent;
41 | }
42 | 75% {
43 | background: rgba($color-highlight, 0.5);
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/example/styles/components/language.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .language {
4 | @include from('md') {
5 | padding-bottom: $base - 2px;
6 | }
7 | margin: 0 $base * -1;
8 |
9 | @at-root #{$cyrillic-modifier} & {
10 | @include from('lg') {
11 | padding-bottom: $base - 1px;
12 | }
13 | font-size: 14px;
14 | letter-spacing: -0.01em;
15 | }
16 |
17 | color: $color-text;
18 | &--contrast a {
19 | color: $color-text--contrast;
20 | }
21 | @include from('lg') {
22 | &--contrast-lg {
23 | color: $color-text--contrast;
24 | }
25 | }
26 | @include from-to('md', 'lg') {
27 | &--contrast-only-md {
28 | color: $color-text--contrast;
29 | }
30 | }
31 | &__link {
32 | color: inherit;
33 | @include transition('color');
34 | display: inline-block;
35 | padding-left: $base;
36 | @include from('lg') {
37 | padding-right: $base;
38 | }
39 | font-size: 14px;
40 |
41 | &--active {
42 | font-size: 16px;
43 | @at-root #{$cyrillic-modifier} & {
44 | font-size: 14px;
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/example/styles/components/logo.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .logo {
4 | padding-top: $base * 0.62;
5 | font-size: $base * 3;
6 | @at-root #{$cyrillic-modifier} & {
7 | font-size: $base * 2.75;
8 | transform: translate(0px, -3px);
9 | letter-spacing: -0.03em;
10 | }
11 | line-height: $base * 2;
12 | color: $color-text;
13 | display: block;
14 | text-decoration: none;
15 | @include transition('color');
16 |
17 | &--contrast {
18 | color: $color-text--contrast;
19 | }
20 |
21 | @include from('lg') {
22 | &--contrast-lg {
23 | color: $color-text--contrast;
24 | }
25 | }
26 |
27 | @include from-to('md', 'lg') {
28 | &--contrast-only-md {
29 | color: $color-text--contrast;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example/styles/components/menu.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .menu {
4 | $menu: &;
5 | display: flex;
6 | margin: 0 $base * -1;
7 |
8 | &__link {
9 | color: black;
10 | display: block;
11 | font-size: $base;
12 | line-height: 1;
13 | text-decoration: none;
14 | padding: 26px $base;
15 | @include transition('color');
16 | @at-root #{$cyrillic-modifier} & {
17 | font-size: 14px;
18 | letter-spacing: -0.03em;
19 | padding-top: 28px;
20 | }
21 | }
22 |
23 | &--contrast #{$menu}__link {
24 | color: $color-text--contrast;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/example/styles/components/pager.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .pager {
4 | $pager: &;
5 | $pager-size: $base * 0.75;
6 | $border-width: 1.5px;
7 | $border-width--active: $pager-size * 0.5;
8 |
9 | &__link {
10 | display: block;
11 | margin: $pager-size 0;
12 | width: $pager-size;
13 | height: $pager-size;
14 | box-sizing: border-box;
15 | border-radius: 50%;
16 | color: $color-text;
17 | border: $border-width solid;
18 | @include transition('color, border-width');
19 |
20 | &:hover,
21 | &._hover {
22 | color: $color-active;
23 | }
24 |
25 | &--active {
26 | border-width: $border-width--active;
27 | }
28 | }
29 |
30 | &--contrast #{$pager}__link {
31 | color: $color-text--contrast;
32 | }
33 |
34 | @include from('lg') {
35 | &--contrast-lg #{$pager}__link {
36 | color: $color-text--contrast;
37 | }
38 | }
39 |
40 | @include from-to('md', 'lg') {
41 | &--contrast-only-md #{$pager}__link {
42 | color: $color-text--contrast;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/example/styles/components/typography.scss:
--------------------------------------------------------------------------------
1 | @import '../shared/globals.scss';
2 |
3 | .typography {
4 | * {
5 | margin-bottom: 0;
6 | }
7 | h1 {
8 | font-size: $base * 2;
9 | line-height: $base * 2;
10 | margin-top: $base * 4;
11 | font-weight: normal;
12 | @at-root #{$cyrillic-modifier} & {
13 | letter-spacing: -0.03em;
14 | transform: translate(0, -1px);
15 | }
16 | }
17 | p {
18 | line-height: $base * 1.5;
19 | margin: $base * 1.5 0;
20 | @at-root #{$cyrillic-modifier} & {
21 | font-size: 14px;
22 | letter-spacing: -0.01em;
23 | }
24 | code {
25 | background-color: $color-code-background;
26 | border-radius: 3px;
27 | margin: 0;
28 | padding: 4px 6px 2px;
29 | font-size: 15px;
30 | vertical-align: bottom;
31 | }
32 | }
33 | a {
34 | @include transition('color');
35 | color: inherit;
36 | }
37 | strong {
38 | font-weight: normal;
39 | }
40 | table {
41 | border-spacing: 0;
42 | }
43 | thead,
44 | tbody,
45 | tr {
46 | padding: 0;
47 | border: 0;
48 | margin: 0;
49 | }
50 | td,
51 | th {
52 | vertical-align: top;
53 | font-weight: normal;
54 | text-align: left;
55 | line-height: $base * 1.5;
56 | padding: 0 $base * 2 $base * 1.5 0;
57 | white-space: nowrap;
58 | @include from('md') {
59 | white-space: normal;
60 | &.nowrap {
61 | white-space: nowrap;
62 | }
63 | }
64 | }
65 | abbr[title] {
66 | text-decoration: none;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/example/styles/main.scss:
--------------------------------------------------------------------------------
1 | @import '~normalize.css';
2 | @import '~simplebar/dist/simplebar.css';
3 | @import './components/about.scss';
4 | @import './components/code-highlight.scss';
5 | @import './components/common.scss';
6 | @import './components/emoji.scss';
7 | @import './components/fixed.scss';
8 | @import './components/footer.scss';
9 | @import './components/header.scss';
10 | @import './components/language.scss';
11 | @import './components/logo.scss';
12 | @import './components/menu.scss';
13 | @import './components/pager.scss';
14 | @import './components/typography.scss';
15 | @import './components/highlighter.scss';
16 |
--------------------------------------------------------------------------------
/example/styles/shared/_breakpoints.scss:
--------------------------------------------------------------------------------
1 | $breakpoints: (
2 | xs: 0,
3 | sm: 586px,
4 | md: 1024px,
5 | lg: 1600px,
6 | );
7 |
8 | @mixin from($breakpoint) {
9 | $size: map-get($breakpoints, $breakpoint);
10 | @if ($size == 0) {
11 | @content;
12 | } @else {
13 | @media screen and (min-width: $size) {
14 | @content;
15 | }
16 | }
17 | }
18 |
19 | @mixin from-to($from, $to) {
20 | $min: map-get($breakpoints, $from);
21 | $max: map-get($breakpoints, $to) - 1px;
22 |
23 | @if ($min == 0) {
24 | @media screen and (max-width: $max) {
25 | @content;
26 | }
27 | } @else {
28 | @media screen and (min-width: $min) and (max-width: $max) {
29 | @content;
30 | }
31 | }
32 | }
33 |
34 | @mixin show-from($breakpoint) {
35 | $size: map-get($breakpoints, $breakpoint);
36 | @if ($size != 0) {
37 | display: none;
38 | @media screen and (min-width: $size) {
39 | display: inherit;
40 | @content;
41 | }
42 | } @else {
43 | @content;
44 | }
45 | }
46 |
47 | @mixin show-from-to($from, $to) {
48 | $min: map-get($breakpoints, $from);
49 | $max: map-get($breakpoints, $to) - 1px;
50 |
51 | display: none;
52 | @if ($min == 0) {
53 | @media screen and (max-width: $max) {
54 | display: inherit;
55 | @content;
56 | }
57 | } @else {
58 | @media screen and (min-width: $min) and (max-width: $max) {
59 | display: inherit;
60 | @content;
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/example/styles/shared/_colors.scss:
--------------------------------------------------------------------------------
1 | $color-primary: white;
2 | $color-secondary: black;
3 | $color-accent: magenta;
4 | $color-highlight: cyan;
5 | $color-marker: yellow;
6 | $color-primary--muted: #999;
7 | $color-secondary--muted: #666;
8 |
9 | $color-background: $color-primary;
10 | $color-background--contrast: $color-secondary;
11 | $color-text: $color-secondary;
12 | $color-text--contrast: $color-primary;
13 | $color-active: $color-accent;
14 |
15 | $color-code-background: scale-color($color-accent, $lightness: 90%, $saturation: -75%);
--------------------------------------------------------------------------------
/example/styles/shared/_globals.scss:
--------------------------------------------------------------------------------
1 | @import './breakpoints.scss';
2 | @import './colors.scss';
3 | @import './grid.scss';
4 | @import './transitions.scss';
5 |
6 | $base: 16px;
7 | $cyrillic-modifier: '.font-cyrillic';
8 |
--------------------------------------------------------------------------------
/example/styles/shared/_grid.scss:
--------------------------------------------------------------------------------
1 | $alpha-width: 0;
2 | $alpha-width--xl: 4;
3 |
4 | $beta-width: 10;
5 | $beta-width--xl: 7;
6 |
7 | $gamma-width: 14;
8 | $gamma-width--xl: 13;
9 |
10 | $grid-width: $alpha-width + $beta-width + $gamma-width;
11 |
12 | $col-width: 1 / $grid-width * 100vw;
13 |
14 | .grid {
15 | @include from('md') {
16 | display: flex;
17 | }
18 |
19 | &__content {
20 | padding-left: $col-width * 2;
21 | padding-right: $col-width * 2;
22 | @include from('sm') {
23 | padding-left: $col-width;
24 | padding-right: $col-width;
25 | }
26 | @include from('lg') {
27 | padding-left: $col-width * 0.5;
28 | padding-right: $col-width * 0.5;
29 | }
30 | }
31 |
32 | &__col {
33 | &--alpha {
34 | display: none;
35 | }
36 |
37 | @include from('md') {
38 | &--alpha-beta {
39 | $width: #{($alpha-width + $beta-width) / $grid-width * 100}vw;
40 | flex-basis: $width;
41 | max-width: $width;
42 | }
43 |
44 | &--alpha-beta-gamma {
45 | $width: #{($alpha-width + $beta-width + $gamma-width) / $grid-width * 100}vw;
46 | flex-basis: $width;
47 | max-width: $width;
48 | }
49 |
50 | &--beta {
51 | $width: #{$beta-width / $grid-width * 100}vw;
52 | flex-basis: $width;
53 | max-width: $width;
54 | }
55 |
56 | &--beta-gamma {
57 | $width: #{($beta-width + $gamma-width) / $grid-width * 100}vw;
58 | flex-basis: $width;
59 | max-width: $width;
60 | }
61 |
62 | &--gamma {
63 | $width: #{$gamma-width / $grid-width * 100}vw;
64 | flex-basis: $width;
65 | max-width: $width;
66 | }
67 | }
68 | @include from('lg') {
69 | &--alpha {
70 | display: block;
71 | $width: #{$alpha-width--xl / $grid-width * 100}vw;
72 | flex-basis: $width;
73 | max-width: $width;
74 | }
75 |
76 | &--alpha-beta {
77 | $width: #{($alpha-width--xl + $beta-width--xl) / $grid-width * 100}vw;
78 | flex-basis: $width;
79 | max-width: $width;
80 | }
81 |
82 | &--alpha-beta-gamma {
83 | $width: #{($alpha-width--xl + $beta-width--xl + $gamma-width--xl) / $grid-width * 100}vw;
84 | flex-basis: $width;
85 | max-width: $width;
86 | }
87 |
88 | &--beta {
89 | $width: #{$beta-width--xl / $grid-width * 100}vw;
90 | flex-basis: $width;
91 | max-width: $width;
92 | }
93 |
94 | &--beta-gamma {
95 | $width: #{($beta-width--xl + $gamma-width--xl) / $grid-width * 100}vw;
96 | flex-basis: $width;
97 | max-width: $width;
98 | }
99 |
100 | &--gamma {
101 | $width: #{$gamma-width--xl / $grid-width * 100}vw;
102 | flex-basis: $width;
103 | max-width: $width;
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/example/styles/shared/_transitions.scss:
--------------------------------------------------------------------------------
1 | @mixin transition(
2 | $property: all,
3 | $duration: 0.2s,
4 | $timing-function: cubic-bezier(0.25, 0.1, 0, 1),
5 | $delay: 0s
6 | ) {
7 | transition-property: #{$property};
8 | transition-duration: $duration;
9 | transition-timing-function: $timing-function;
10 | transition-delay: $delay;
11 | }
12 |
--------------------------------------------------------------------------------
/example/svg/how-it-works-face.svg:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/example/svg/how-it-works-hand.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/example/svg/how-to-use.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/example/svg/options.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/svg/possibilities.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/example/svg/why-immerser.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/generateOptionsTables.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const kindOf = require('kind-of');
3 | const TurndownService = require('turndown');
4 | const en = require('./i18n/en.js');
5 | const { OPTION_CONFIG } = require('./src/defaults.js');
6 |
7 | const turndownService = new TurndownService();
8 |
9 | function getClassNames(type) {
10 | switch (type.toLowerCase()) {
11 | case 'array':
12 | case 'object':
13 | return 'punctuation';
14 | case 'number':
15 | return 'number';
16 | case 'boolean':
17 | return 'boolean';
18 | case 'string':
19 | return 'string nowrap';
20 | case 'function':
21 | case 'null':
22 | case 'undefined':
23 | return 'keyword';
24 | }
25 | }
26 |
27 | const options = Object.keys(OPTION_CONFIG).map((optionName) => {
28 | let defaultValue = OPTION_CONFIG[optionName].default;
29 | let type = kindOf(defaultValue);
30 | if (type === 'null' && optionName.startsWith('on')) {
31 | type = 'function';
32 | }
33 | if (type === 'array') {
34 | defaultValue = '[]';
35 | }
36 | return {
37 | optionName,
38 | type,
39 | defaultValue,
40 | };
41 | });
42 |
43 | const HTMLRowsMarkup = options.map(({ optionName, type, defaultValue }) => {
44 | return ` <%= getTranslation('option') %> | 56 |<%= getTranslation('type') %> | 57 |<%= getTranslation('default') %> | 58 |<%= getTranslation('description') %> | 59 |
---|
19 | Sometimes designers create complex logic and fix parts of the interface. 20 | Also they colour page sections contrasted. How to deal with this mess? 21 |
22 |23 | Immerser comes to help you. It’s a javascript library to change fixed elements on scroll. 24 |
25 |26 | Immerser fast, because it calculates states once on init. 27 | Then it watches the scroll position and schedules redraw document in the next event loop tick with requestAnimationFrame. 28 | Script changes transform property, so it uses graphic hardware acceleration. 29 |
30 |31 | Immerser is written on vanilla js. Only %%BUNDLESIZE%%Kb gzipped. 32 |
33 | `, 34 | 35 | 'terms-title': 'Terms', 36 | 'terms-content': ` 37 |
38 | Immerser root
— is the parent
39 | container for your fixed parts solids
.
40 | Actually, solids are positioned absolutely to fixed immerser root. The
41 | layers
are sections of your page.
42 | Also you may want to add
43 | pager
to navigate through layers
44 | and indicate active state.
45 |
Using npm:
', 50 | 'install-yarn-label': 'Using yarn:
', 51 | 'install-browser-label': 'Or if you want to use immerser in browser as global variable:
', 52 | 53 | 'prepare-your-markup-title': 'Prepare Your Markup', 54 | 'prepare-your-markup-content': ` 55 |First, setup fixed container as the immerser root container, and add the data-immerser
attribute.
Next place absolutely positioned children into the immerser parent and add data-immerser-solid="solid-id"
to each.
Then add data-immerser-layer
attribute to each section and pass configuration in
58 | data-immerser-layer-config='{"solid-id": "classname-modifier"}'
. Otherwise, you can pass configuration as
59 | solidClassnameArray
option to immerser. Config should contain JSON describing what class should be
60 | applied on each solid element, when it's over a section.
Also feel free to add data-immerser-pager
to create a pager for your layers.
67 | Apply colour and background styles to your layers and solids according to your classname configuration passed in data attribute or options. 68 | I’m using BEM methodology in this example. 69 |
70 | `, 71 | 72 | 'dont-import-if-umd-line-1': `You don't have to import immerser`, 73 | 'dont-import-if-umd-line-2': `if you're using it in browser as global variable`, 74 | 'data-attribute-will-override-this-option-line-1': 'this option will be overridden by options', 75 | 'data-attribute-will-override-this-option-line-2': 'passed in data-immerser-layer-config attribute in each layer', 76 | 77 | 'initialize-immerser-title': 'Initialize Immerser', 78 | 'initialize-immerser-content': `Include immerser in your code and create immerser instance with options.
`, 79 | 80 | 'callback-on-init': 'callback on init', 81 | 'callback-on-bind': 'callback on bind', 82 | 'callback-on-unbind': 'callback on unbind', 83 | 'callback-on-destroy': 'callback on destroy', 84 | 'callback-on-active-layer-change': 'callback on active layer change', 85 | 86 | 'how-it-works-title': 'How it Works', 87 | 'how-it-works-content': ` 88 |First, immerser gathers information about the layers, solids, window and document. Then it creates a statemap for each layer, containing all necessary information, when the layer is partially and fully in viewport.
89 |After that immerser modifies DOM, cloning all solids into mask containers for each layer and applying the classnames given in configuration. If you have added a pager, immerser also creates links for layers.
90 |Finally, immerser binds listeners to scroll and resize events. On resize, it will meter layers, the window and document heights again and recalculate the statemap.
91 |On scroll, immerser moves a mask of solids to show part of each solid group according to the layer below.
92 | `, 93 | 94 | 'options-title': 'Options', 95 | 'options-content': ` 96 |97 | You can pass options to immerser as data-attributes on layers or as object as function parameter. Data-attributes are 98 | processed last, so they override the options passed to the function. 99 |
100 | `, 101 | 102 | option: 'option', 103 | type: 'type', 104 | default: 'default', 105 | description: 'description', 106 | 107 | 'option-solidClassnameArray': 108 | 'Array of layer class configurations. Overriding by config passed in data-immerser-layer-config for corresponding layer. Configuration example is shown above', 109 | 'option-fromViewportWidth': 'A viewport width, from which immerser will init', 110 | 'option-pagerThreshold': 'How much next layer should be in viewport to trigger pager', 111 | 'option-hasToUpdateHash': 'Flag to control changing hash on pager active state change', 112 | 'option-scrollAdjustThreshold': 113 | 'A distance from the viewport top or bottom to the section top or bottom edge in pixels. If the current distance is below the threshold, the scroll adjustment will be applied. Will not adjust, if zero passed', 114 | 'option-scrollAdjustDelay': 'Delay after user interaction and before scroll adjust', 115 | 'option-pagerLinkActiveClassname': 'Added to each pager link pointing to active', 116 | 'option-isScrollHandled': 'Binds scroll listener if true. Set to false if you\'re using remote scroll controller', 117 | 'option-onInit': 'Fired after initialization. Accept an immerser instance as the only parameter', 118 | 'option-onBind': 'Fired after binding DOM. Accept an immerser instance as the only parameter', 119 | 'option-onUnbind': 'Fired after unbinding DOM. Accept an immerser instance as the only parameter', 120 | 'option-onDestroy': 'Fired after destroy. Accept an immerser instance as the only parameter', 121 | 'option-onActiveLayerChange': 122 | 'Fired after active layer change. Accept active layer index as first parameter and an immerser instance as second', 123 | 124 | 'cloning-event-listeners-title': 'Cloning Event Listeners', 125 | 'cloning-event-listeners-content': ` 126 |127 | Since immerser cloning nested nodes by default, all event listeners and data bound on nodes will be lost after 128 | init. Fortunately, you can markup the immerser yourself. It can be useful when you have event listeners 129 | on solids, reactive logic or more than classname switching. All you need is to place the number 130 | of nested immerser masks equal to the number of the layers. Look how I change the smiley emoji 131 | on the right in this page source. 132 |
133 | `, 134 | 135 | 'your-markup': 'your markup', 136 | 137 | 'handle-clone-hover-title': 'Handle Clone Hover', 138 | 'handle-clone-hover-content': ` 139 |
140 | As mentioned above, immerser cloning nested nodes to achieve changing on scroll. Therefore if you
141 | hover a partially visible element, only the visible part will change. If you want to synchronize all cloned links, just
142 | pass
143 | data-immerser-synchro-hover="hoverId"
attribute. It will share _hover
class between all
144 | nodes with this hoverId
when the mouse is over one of them. Add _hover
selector alongside your
145 | :hover
pseudoselector to style your interactive elements.
146 |
151 | Immerser is not aware of changes in DOM, if you dynamically add or remove nodes. If you change height of the document
152 | and want immerser to recalculate and redraw solids, call onDOMChange
method on the immerser instance.
153 |
19 | Иногда дизайнеры создают сложную логику и фиксируют части интерфейса. 20 | А еще они красят разделы страницы в контрастные цвета. Как с этим справиться? 21 |
22 |23 | Вам поможет иммёрсер — джаваскрипт библиотека для замены фиксированных элементов при прокрутке страницы. 24 |
25 |26 | Иммёрсер вычисляет состояния один раз в момент инициализации. 27 | Затем он следит за позицией скролла и планирует перерисовку документа 28 | в следующем такте цикла событий через метод requestAnimationFrame. 29 | Скрипт изменяет свойство transform, это задействует графический ускоритель. 30 |
31 |32 | Иммёрсер написан на чистом джаваскрипте. Всего %%BUNDLESIZE%%Кб в сжатии gzip. 33 |
34 | `, 35 | 36 | 'terms-title': 'Термины', 37 | 'terms-content': ` 38 |
39 | Корневой элемент иммёрсера
— это родительский контейнер для ваших фиксированных блоков
.
40 | Фактически они позиционированы абсолютно внутри фиксированного корневого элемента.
41 | Слои
— это разделы страницы, окрашенные в разные цвета.
42 | Еще вы наверняка захотите добавить навигацию
по разделам, выделяющую активный раздел.
43 |
Через npm:
', 48 | 'install-yarn-label': 'Через yarn:
', 49 | 'install-browser-label': 'Или если вы хотите использовать иммёрсер в браузере как глобальную переменную:
', 50 | 51 | 'prepare-your-markup-title': 'Подготовьте разметку', 52 | 'prepare-your-markup-content': ` 53 |Сначала настройте свой фиксированный контейнер как корневой элемент иммёрсера, добавив атрибут data-immerser
Затем расположите в нем абсолютно позиционированные дочерние элементы и добавьте каждому атрибут data-immerser-solid="solid-id"
с идентификатором блока.
Добавьте каждому слою атрибут data-immerser-layer
. Передайте конфигурацию в виде JSON в каждый слой с помощью атрибута
56 | data-immerser-layer-config='{"solid-id": "classname-modifier"}'
.
57 | Также вы можете передать конфигурацию всех слоев массивом в параметре solidClassnameArray
настроек.
58 | Конфигурация должна содержать описание классов для блоков, когда они находятся поверх слоя.
Так же вы можете добавить элемент с атрибутом data-immerser-pager
для создания навигации.
65 | Добавьте стили цвета текста и фона на ваши блоки и слои с помощью классов, переданных в дата-атрибут или настройки. 66 | В примере я использую методологию БЭМ. 67 |
68 | `, 69 | 'dont-import-if-umd-line-1': `Вам не нужно импортировать иммёрсер,`, 70 | 'dont-import-if-umd-line-2': `если вы используете его в браузере как глобальную переменную`, 71 | 'data-attribute-will-override-this-option-line-1': 'будет переопределена настройками,', 72 | 'data-attribute-will-override-this-option-line-2': 'переданными в атрибут data-immerser-layer-config каждого слоя', 73 | 74 | 'initialize-immerser-title': 'Инициализируйте иммёрсер', 75 | 'initialize-immerser-content': `Добавьте иммёрсер в код и создайте экземпляр с настройками.
`, 76 | 77 | 'callback-on-init': 'колбек после инициализации', 78 | 'callback-on-bind': 'колбек после привязки к документу', 79 | 'callback-on-unbind': 'колбек после отвязки от документа', 80 | 'callback-on-destroy': 'колбек после уничтожения', 81 | 'callback-on-active-layer-change': 'колбек после смены активного слоя', 82 | 83 | 'how-it-works-title': 'Принцип работы', 84 | 'how-it-works-content': ` 85 |Сначала иммёрсер собирает информацию о слоях, блоках, окне и документе. Затем скрипт создает карту состояний для каждого слоя. Карта содержит размеры слоя, блоков и позиции их пересечений при скролле.
86 |После сбора информации скрипт копирует все блоки в маскирующий контейнер и применяет к каждому классы, переданные в настройках. Если вы добавили навигацию, то иммёрсер создаст ссылки на каждый слой.
87 |Затем иммёрсер подписывается на события скролла документа и изменения размеров окна.
88 |При скролле иммёрсер двигает маскирующий контейнер так, чтобы показывать часть каждой группы блоков для каждого слоя под ними. При изменении размеров окна скрипт рассчитает карту состояний заново.
89 | `, 90 | 91 | 'options-title': 'Настройки', 92 | 'options-content': ` 93 |94 | Вы можете передать настройки параметром функции конструктора или дата-атрибутом в документе. 95 | Дата-аттрибут обрабатывается последним, поэтому он переопределит настройки, переданные в конструктор. 96 |
97 | `, 98 | 99 | option: 'параметр', 100 | type: 'тип', 101 | default: 'значение по умолчанию', 102 | description: 'описание', 103 | 104 | 'option-solidClassnameArray': 105 | 'Массив настроек слоев. Конфигурация, переданная в data-immerser-layer-config перезапишет эту настройку для соответствующего слоя. Пример конфигурации показан выше', 106 | 'option-fromViewportWidth': 'Минимальная ширина окна для инициализации иммёрсера', 107 | 'option-pagerThreshold': 'Насколько должен следующий слой быть видим в окне, чтобы он стал активен в навигации', 108 | 'option-hasToUpdateHash': 'Флаг, контролирующий обновление хеша страницы', 109 | 'option-scrollAdjustThreshold': 110 | 'Дистанция до верха или низа окна браузера в пикселях. Если текущая дистанция меньше переданного значения, то скрипт подстроит положение скролла', 111 | 'option-scrollAdjustDelay': 'Сколько ждать бездействия пользователя, чтобы начать подстройку скролла', 112 | 'option-pagerLinkActiveClassname': 'Применяется, к каждой ссылке пейджера, ссылающуюся на активный слой', 113 | 'option-isScrollHandled': 'Подписывается на событие прокрутки, если включено. Выключите в случае, когда скроллом управляет внешний контроллер', 114 | 'option-onInit': 'Колбек после инициализации. Принимает один параметр — экземпляр иммёрсера', 115 | 'option-onBind': 'Колбек после привязки к документу. Принимает один параметр — экземпляр иммёрсера', 116 | 'option-onUnbind': 'Колбек после отвязки от документа. Принимает один параметр — экземпляр иммёрсера', 117 | 'option-onDestroy': 'Колбек после уничтожения. Принимает один параметр — экземпляр иммёрсера', 118 | 'option-onActiveLayerChange': 119 | 'Колбек после смены активного слоя. Принимает два параметра: индекс следующего слоя и экземпляр иммёрсера', 120 | 121 | 'cloning-event-listeners-title': 'Клонирование подписчиков событий', 122 | 'cloning-event-listeners-content': ` 123 |124 | Вы уже знаете, что иммёрсер клонирует элементы. 125 | Подписчики событий и данные, привязанные к нодам, не клонируются вместе с элементом. 126 | К счастью, вы можете разметить иммёрсер самостоятельно. 127 | Для этого разместите внутри корневого элемента маскирующие контейнеры для блоков по числу слоев. 128 | В таком случае скрипт не будет клонировать элементы. Подписчики и реактивная логика останутся нетронутыми. 129 | В примере на этой странице я создаю подписчик на клик по смайлу справа до инициализации. 130 |
131 | `, 132 | 133 | 'your-markup': 'ваша разметка', 134 | 135 | 'handle-clone-hover-title': 'Обработка наведения на границах слоев', 136 | 'handle-clone-hover-content': ` 137 |
138 | Если вы наведете мышь на элемент, находящийся на границе слоев,
139 | то псевдоселектор :hover
сработает только на одну часть.
140 | Чтобы наведение сработало на все клоны элемента, задайте идентификатор наведения в атрибуте data-immerser-synchro-hover="hoverId"
.
141 | При наведении мыши на такой элемент, ко всем его клонам добавится класс _hover
.
142 | Стилизуйте по этому селектору вместе с псевдоселектором :hover
, чтобы добиться нужного эффекта.
143 |
148 | Иммёрсер не отслеживает изменения документа, если вы динамически добавляете или удаляете ноды. Если вы меняете высоту документа,
149 | и хотите, чтобы иммёрсер пересчитал и перерисовал блоки, вызовите метод onDOMChange
у экземпляра иммёрсера.
150 |