mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-19 05:09:41 +02:00
Table of content (#199)
* Create nodemon.json
* Add table of content
* update view
* remove logs
* update tags var
* update layout
* Revert "update layout"
This reverts commit 18aad62257
.
* update layout
* Update layout.pcss
* update from master
* Update sidebar.twig
* remove non valued changes
* Update table-of-content.js
* Update table-of-content.pcss
* Update table-of-content.pcss
* Update layout.pcss
* Update table-of-content.js
* remove unused styles
* not module
* rename var
* remove log
* update structure
* Update table-of-content.js
* Update table-of-content.js
* Update layout.pcss
* Update table-of-content.js
* try not to use intersection observer
* Update table-of-content.js
* fix scroll padding
* fix header component layout
* update logic
* fix click area
* Update table-of-content.js
* Update table-of-content.js
* small fixes
* remove unused
* Update table-of-content.js
* Update decorators.js
* Update table-of-content.js
* Update table-of-content.js
* Update table-of-content.js
* Update table-of-content.js
* Update table-of-content.js
* fix scroll issues, resolve eslit ts/js conflicts
* add some todos
* handle up-direction scroll as well
* optimization
* update offsets
* Update header.pcss
Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
This commit is contained in:
parent
13cc53e4ae
commit
213f9d89a3
16 changed files with 630 additions and 999 deletions
13
nodemon.json
Normal file
13
nodemon.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"verbose": true,
|
||||
"ignore": [
|
||||
".git",
|
||||
"node_modules",
|
||||
"public",
|
||||
"src/frontend"
|
||||
],
|
||||
"watch": [
|
||||
"**/*"
|
||||
],
|
||||
"ext": "js,twig"
|
||||
}
|
|
@ -27,9 +27,7 @@
|
|||
{% block body %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<aside class="docs__aside-right">
|
||||
<div style="width: 100%; height: 100px; background: grey"/>
|
||||
</aside>
|
||||
<aside class="docs__aside-right" id="layout-sidebar-right"></aside>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/dist/main.bundle.js"></script>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<a name="{{ text | urlify }}" style="display: inline-block; position: absolute; margin-top: -20px;"></a>
|
||||
<h{{ level }} class="block-header block-header--{{ level }} block-header--anchor">
|
||||
<h{{ level }} id="{{ text | urlify }}" class="block-header block-header--{{ level }} block-header--anchor">
|
||||
<a href="#{{ text | urlify }}">
|
||||
{{ text }}
|
||||
</a>
|
||||
|
|
13
src/frontend/.eslintrc
Normal file
13
src/frontend/.eslintrc
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": [
|
||||
"codex"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": true
|
||||
},
|
||||
"globals": {
|
||||
"HTMLElement": true
|
||||
}
|
||||
}
|
405
src/frontend/js/classes/table-of-content.js
Normal file
405
src/frontend/js/classes/table-of-content.js
Normal file
|
@ -0,0 +1,405 @@
|
|||
import * as Decorators from '../utils/decorators';
|
||||
import * as $ from '../utils/dom';
|
||||
|
||||
/**
|
||||
* Generate dynamic table of content
|
||||
*/
|
||||
export default class TableOfContent {
|
||||
/**
|
||||
* Initialize table of content
|
||||
*
|
||||
* @param {object} options - constructor params
|
||||
* @param {string} options.tagSelector - selector for tags to observe
|
||||
* @param {HTMLElement} options.appendTo - element for appending of the table of content
|
||||
*/
|
||||
constructor({ tagSelector, appendTo }) {
|
||||
/**
|
||||
* Array of tags to observe
|
||||
*/
|
||||
this.tags = [];
|
||||
this.tagsSectionsMap = [];
|
||||
|
||||
/**
|
||||
* Selector for tags to observe
|
||||
*/
|
||||
this.tagSelector = tagSelector || 'h2,h3,h4';
|
||||
|
||||
/**
|
||||
* Element to append the Table of Content
|
||||
*/
|
||||
this.tocParentElement = appendTo;
|
||||
|
||||
if (!this.tocParentElement) {
|
||||
throw new Error('Table of Content wrapper not found');
|
||||
}
|
||||
|
||||
this.nodes = {
|
||||
/**
|
||||
* Main Table of Content element
|
||||
*/
|
||||
wrapper: null,
|
||||
|
||||
/**
|
||||
* List of Table of Content links
|
||||
*/
|
||||
items: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Currently highlighted element of ToC
|
||||
*/
|
||||
this.activeItem = null;
|
||||
|
||||
this.CSS = {
|
||||
tocContainer: 'table-of-content',
|
||||
tocHeader: 'table-of-content__header',
|
||||
tocElement: 'table-of-content__list',
|
||||
tocElementItem: 'table-of-content__list-item',
|
||||
tocElementItemActive: 'table-of-content__list-item--active',
|
||||
tocElementItemIndent: number => `table-of-content__list-item--indent-${number}x`,
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize table of content
|
||||
*/
|
||||
init() {
|
||||
this.tags = this.getSectionTagsOnThePage();
|
||||
|
||||
/**
|
||||
* Check if no tags found then table of content is not needed
|
||||
*/
|
||||
if (this.tags.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize table of content element
|
||||
*/
|
||||
this.createTableOfContent();
|
||||
this.addTableOfContent();
|
||||
|
||||
/**
|
||||
* Calculate bounds for each tag and watch active section
|
||||
*/
|
||||
this.calculateBounds();
|
||||
this.watchActiveSection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all section tags on the page
|
||||
*
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
getSectionTagsOnThePage() {
|
||||
return Array.from(document.querySelectorAll(this.tagSelector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate top line position for each tag
|
||||
*/
|
||||
calculateBounds() {
|
||||
this.tagsSectionsMap = this.tags.map((tag) => {
|
||||
const rect = tag.getBoundingClientRect();
|
||||
const top = Math.floor(rect.top + window.scrollY);
|
||||
|
||||
return {
|
||||
top,
|
||||
tag,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch active section while scrolling
|
||||
*/
|
||||
watchActiveSection() {
|
||||
/**
|
||||
* Where content zone starts in document
|
||||
*/
|
||||
const contentTopOffset = this.getScrollPadding();
|
||||
|
||||
/**
|
||||
* Additional offset for correct calculation of active section
|
||||
*
|
||||
* Cause opening page with anchor could scroll almost
|
||||
* near to the target section and we need to add 1px to calculations
|
||||
*/
|
||||
const activationOffset = 1;
|
||||
|
||||
const detectSection = () => {
|
||||
/**
|
||||
* Calculate scroll position
|
||||
*
|
||||
* @todo research how not to use magic number
|
||||
*/
|
||||
const scrollPosition = contentTopOffset + window.scrollY + activationOffset;
|
||||
|
||||
/**
|
||||
* Find the nearest section above the scroll position
|
||||
*/
|
||||
const section = this.tagsSectionsMap.filter((tag) => {
|
||||
return tag.top <= scrollPosition;
|
||||
}).pop();
|
||||
|
||||
/**
|
||||
* If section found then set it as active
|
||||
*/
|
||||
if (section) {
|
||||
const targetLink = section.tag.querySelector('a').getAttribute('href');
|
||||
|
||||
this.setActiveItem(targetLink);
|
||||
} else {
|
||||
/**
|
||||
* Otherwise no active link will be highlighted
|
||||
*/
|
||||
this.setActiveItem(null);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Define a flag to reduce number of calls to detectSection function
|
||||
*/
|
||||
const throttledDetectSectionFunction = Decorators.throttle(() => {
|
||||
detectSection();
|
||||
}, 400);
|
||||
|
||||
/**
|
||||
* Scroll listener
|
||||
*/
|
||||
document.addEventListener('scroll', throttledDetectSectionFunction, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create table of content
|
||||
*
|
||||
* <section>
|
||||
* <header>On this page</header>
|
||||
* <ul>
|
||||
* <li><a href="#"></a></li>
|
||||
* ...
|
||||
* </ul>
|
||||
* </section>
|
||||
*/
|
||||
createTableOfContent() {
|
||||
this.tocElement = $.make('section', this.CSS.tocElement);
|
||||
|
||||
this.tags.forEach((tag) => {
|
||||
const linkTarget = tag.querySelector('a').getAttribute('href');
|
||||
|
||||
const linkWrapper = $.make('li', this.CSS.tocElementItem);
|
||||
const linkBlock = $.make('a', null, {
|
||||
innerText: tag.innerText,
|
||||
href: `${linkTarget}`,
|
||||
});
|
||||
|
||||
/**
|
||||
* Additional indent for h3-h6 headers
|
||||
*/
|
||||
switch (tag.tagName.toLowerCase()) {
|
||||
case 'h3':
|
||||
linkWrapper.classList.add(this.CSS.tocElementItemIndent(1));
|
||||
break;
|
||||
case 'h4':
|
||||
linkWrapper.classList.add(this.CSS.tocElementItemIndent(2));
|
||||
break;
|
||||
case 'h5':
|
||||
linkWrapper.classList.add(this.CSS.tocElementItemIndent(3));
|
||||
break;
|
||||
case 'h6':
|
||||
linkWrapper.classList.add(this.CSS.tocElementItemIndent(4));
|
||||
break;
|
||||
}
|
||||
|
||||
linkWrapper.appendChild(linkBlock);
|
||||
this.tocElement.appendChild(linkWrapper);
|
||||
|
||||
this.nodes.items.push(linkWrapper);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add table of content to the page
|
||||
*/
|
||||
addTableOfContent() {
|
||||
this.nodes.wrapper = $.make('section', this.CSS.tocContainer);
|
||||
|
||||
const header = $.make('header', this.CSS.tocHeader, {
|
||||
textContent: 'On this page',
|
||||
});
|
||||
|
||||
this.nodes.wrapper.appendChild(header);
|
||||
this.nodes.wrapper.appendChild(this.tocElement);
|
||||
|
||||
this.tocParentElement.appendChild(this.nodes.wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight link's item with a given href
|
||||
*
|
||||
* @param {string|null} targetLink - href of the link. Null if we need to clear all highlights
|
||||
*/
|
||||
setActiveItem(targetLink) {
|
||||
/**
|
||||
* Clear current highlight
|
||||
*/
|
||||
if (this.activeItem) {
|
||||
this.activeItem.classList.remove(this.CSS.tocElementItemActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* If targetLink is null, that means we reached top, nothing to highlight
|
||||
*/
|
||||
if (targetLink === null) {
|
||||
this.activeItem = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looking for a target link
|
||||
*
|
||||
* @todo do not fire DOM search, use saved map instead
|
||||
*/
|
||||
const targetElement = this.tocElement.querySelector(`a[href="${targetLink}"]`);
|
||||
|
||||
/**
|
||||
* Getting link's wrapper
|
||||
*/
|
||||
const listItem = targetElement.parentNode;
|
||||
|
||||
/**
|
||||
* Highlight and save current item
|
||||
*/
|
||||
listItem.classList.add(this.CSS.tocElementItemActive);
|
||||
this.activeItem = listItem;
|
||||
|
||||
/**
|
||||
* If need, scroll parent to active item
|
||||
*/
|
||||
this.scrollToActiveItemIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Document scroll ending callback
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
scrollToActiveItemIfNeeded() {
|
||||
/**
|
||||
* Do nothing if the Table of Content has no internal scroll at this page
|
||||
*
|
||||
* @todo compute it once
|
||||
*/
|
||||
const hasScroll = this.nodes.wrapper.scrollHeight > this.nodes.wrapper.clientHeight;
|
||||
|
||||
if (!hasScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If some item is highlighted, check whether we need to scroll to it or not
|
||||
*/
|
||||
if (this.activeItem) {
|
||||
/**
|
||||
* First, check do we need to scroll to item?
|
||||
* We need to scroll in case when:
|
||||
* item bottom coord is bigger than parent height + current parent scroll
|
||||
*
|
||||
* @todo use memoization for calculating of the itemBottomCoordWithPadding
|
||||
*/
|
||||
const itemOffsetTop = this.activeItem.offsetTop;
|
||||
const itemHeight = this.activeItem.offsetHeight;
|
||||
const itemBottomCoord = itemOffsetTop + itemHeight;
|
||||
const scrollPadding = 10; // scroll offset below/above item
|
||||
const itemBottomCoordWithPadding = itemBottomCoord + scrollPadding;
|
||||
const itemTopCoordWithPadding = itemOffsetTop - scrollPadding;
|
||||
|
||||
const scrollableParentHeight = this.nodes.wrapper.offsetHeight; // @todo compute it once
|
||||
const scrollableParentScrolledDistance = this.nodes.wrapper.scrollTop;
|
||||
|
||||
/**
|
||||
* Scroll bottom required if item ends below the parent bottom boundary
|
||||
*/
|
||||
const isScrollDownRequired = itemBottomCoordWithPadding > scrollableParentHeight + scrollableParentScrolledDistance;
|
||||
|
||||
/**
|
||||
* Scroll upward required when item starts above the visible parent zone
|
||||
*/
|
||||
const isScrollUpRequired = itemTopCoordWithPadding < scrollableParentScrolledDistance;
|
||||
|
||||
/**
|
||||
* If item is fully visible, scroll is not required
|
||||
*/
|
||||
const isScrollRequired = isScrollDownRequired || isScrollUpRequired;
|
||||
|
||||
|
||||
if (isScrollRequired === false) {
|
||||
/**
|
||||
* Item is visible, scroll is not needed
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Now compute the scroll distance to make item visible
|
||||
*/
|
||||
let distanceToMakeItemFullyVisible;
|
||||
|
||||
|
||||
if (isScrollDownRequired) {
|
||||
distanceToMakeItemFullyVisible = itemBottomCoordWithPadding - scrollableParentHeight;
|
||||
} else { // scrollUpRequired=true
|
||||
distanceToMakeItemFullyVisible = itemTopCoordWithPadding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scroll
|
||||
* Using RAF to prevent overloading of regular scroll animation FPS
|
||||
*/
|
||||
window.requestAnimationFrame(() => {
|
||||
this.nodes.wrapper.scrollTop = distanceToMakeItemFullyVisible;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scroll padding top value from HTML element
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getScrollPadding() {
|
||||
const defaultScrollPaddingValue = 0;
|
||||
|
||||
/**
|
||||
* Try to get calculated value or fallback to default value
|
||||
*/
|
||||
try {
|
||||
/**
|
||||
* Getting the HTML element
|
||||
*/
|
||||
const htmlElement = document.documentElement;
|
||||
|
||||
/**
|
||||
* Getting css scroll padding value
|
||||
*/
|
||||
const scrollPaddingTopValue = window.getComputedStyle(htmlElement)
|
||||
.getPropertyValue('scroll-padding-top');
|
||||
|
||||
/**
|
||||
* Parse value to number
|
||||
*/
|
||||
return parseInt(scrollPaddingTopValue, 10);
|
||||
} catch (e) {}
|
||||
|
||||
/**
|
||||
* On any errors return default value
|
||||
*/
|
||||
return defaultScrollPaddingValue;
|
||||
}
|
||||
}
|
|
@ -1,27 +1,23 @@
|
|||
/**
|
||||
* @typedef {object} pageModuleSettings
|
||||
*/
|
||||
|
||||
/**
|
||||
* @class Page
|
||||
* @classdesc Class for page module
|
||||
*/
|
||||
export default class Writing {
|
||||
export default class Page {
|
||||
/**
|
||||
* Creates base properties
|
||||
*/
|
||||
constructor() {
|
||||
this.codeStyler = null;
|
||||
this.tableOfContent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by ModuleDispatcher to initialize module from DOM
|
||||
* @param {pageModuleSettings} settings - module settings
|
||||
* @param {HTMLElement} moduleEl - module element
|
||||
*/
|
||||
init(settings = {}, moduleEl) {
|
||||
init() {
|
||||
this.codeStyler = this.createCodeStyling();
|
||||
};
|
||||
this.tableOfContent = this.createTableOfContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init code highlighting
|
||||
|
@ -29,8 +25,35 @@ export default class Writing {
|
|||
async createCodeStyling() {
|
||||
const { default: CodeStyler } = await import(/* webpackChunkName: "code-styling" */ './../classes/codeStyler');
|
||||
|
||||
return new CodeStyler({
|
||||
selector: '.block-code__content'
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new CodeStyler({
|
||||
selector: '.block-code__content',
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error); // @todo send to Hawk
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init table of content
|
||||
*
|
||||
* @returns {Promise<TableOfContent>}
|
||||
*/
|
||||
async createTableOfContent() {
|
||||
const { default: TableOfContent } = await import(/* webpackChunkName: "table-of-content" */ '../classes/table-of-content');
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new TableOfContent({
|
||||
tagSelector:
|
||||
'h2.block-header--anchor,' +
|
||||
'h3.block-header--anchor,' +
|
||||
'h4.block-header--anchor',
|
||||
appendTo: document.getElementById('layout-sidebar-right'),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error); // @todo send to Hawk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
62
src/frontend/js/utils/decorators.js
Normal file
62
src/frontend/js/utils/decorators.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* A few useful utility functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Throttle decorator function
|
||||
*
|
||||
* @param {Function} func - function to throttle
|
||||
* @param {number} ms - milliseconds to throttle
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function throttle(func, ms) {
|
||||
let isThrottled = false,
|
||||
savedArgs,
|
||||
savedThis;
|
||||
|
||||
// eslint-disable-next-line jsdoc/require-jsdoc
|
||||
function wrapper() {
|
||||
if (isThrottled) {
|
||||
savedArgs = arguments;
|
||||
savedThis = this;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
func.apply(this, arguments);
|
||||
|
||||
isThrottled = true;
|
||||
|
||||
setTimeout(function () {
|
||||
isThrottled = false;
|
||||
|
||||
if (savedArgs) {
|
||||
wrapper.apply(savedThis, savedArgs);
|
||||
savedArgs = savedThis = null;
|
||||
}
|
||||
}, ms);
|
||||
}
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce decorator function
|
||||
*
|
||||
* @param {Function} f - function to debounce
|
||||
* @param {number} ms - milliseconds to debounce
|
||||
*
|
||||
* @returns {(function(): void)|*}
|
||||
*/
|
||||
export function debounce(f, ms) {
|
||||
let timeoutId = null;
|
||||
|
||||
return function () {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => f.apply(this, arguments), ms);
|
||||
};
|
||||
}
|
23
src/frontend/js/utils/dom.js
Normal file
23
src/frontend/js/utils/dom.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Helper method for elements creation
|
||||
*
|
||||
* @param {string} tagName - name of tag to create
|
||||
* @param {string | string[]} classNames - list of CSS classes
|
||||
* @param {object} attributes - any properties to add
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function make(tagName, classNames = null, attributes = {}) {
|
||||
const el = document.createElement(tagName);
|
||||
|
||||
if (Array.isArray(classNames)) {
|
||||
el.classList.add(...classNames);
|
||||
} else if (classNames) {
|
||||
el.classList.add(classNames);
|
||||
}
|
||||
|
||||
for (const attrName in attributes) {
|
||||
el[attrName] = attributes[attrName];
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
/**
|
||||
* Utility class to handle interaction with local storage
|
||||
*/
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
html {
|
||||
scroll-padding-top: calc(var(--layout-height-header) + 50px);
|
||||
}
|
||||
|
||||
.docs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -6,7 +10,6 @@
|
|||
border-bottom: 1px solid var(--color-line-gray);
|
||||
font-size: 18px;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
height: var(--layout-height-header);
|
||||
box-sizing: border-box;
|
||||
position: sticky;
|
||||
|
|
58
src/frontend/styles/components/table-of-content.pcss
Normal file
58
src/frontend/styles/components/table-of-content.pcss
Normal file
|
@ -0,0 +1,58 @@
|
|||
.table-of-content {
|
||||
border-left: 1px solid var(--color-line-gray);
|
||||
padding-left: var(--layout-padding-horizontal);
|
||||
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
|
||||
box-sizing: border-box;
|
||||
|
||||
&__header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 21px;
|
||||
letter-spacing: -0.01em;
|
||||
|
||||
margin-bottom: 12px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
margin: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 0;
|
||||
|
||||
list-style: none;
|
||||
|
||||
gap: 6px;
|
||||
|
||||
&-item {
|
||||
@apply --squircle;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-link-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background-color: var(--color-link-hover);
|
||||
}
|
||||
|
||||
&--indent-1x { margin-left: 6px; }
|
||||
&--indent-2x { margin-left: 12px; }
|
||||
&--indent-3x { margin-left: 18px; }
|
||||
&--indent-4x { margin-left: 24px; }
|
||||
|
||||
& > a {
|
||||
padding: 4px 8px;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 150%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,15 +20,15 @@
|
|||
margin: 0 auto;
|
||||
|
||||
@media (--desktop) {
|
||||
margin-right: var(--layout-padding-horizontal);
|
||||
margin-left: 0;
|
||||
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
--max-space-between-cols: 160px;
|
||||
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: calc(var(--layout-width-main-col) + var(--max-space-between-cols) + var(--layout-sidebar-width));
|
||||
|
@ -46,7 +46,10 @@
|
|||
display: none;
|
||||
|
||||
position: sticky;
|
||||
top: calc(var(--layout-height-header) + var(--layout-padding-vertical));
|
||||
overflow: auto;
|
||||
height: calc(100vh - var(--layout-height-header));
|
||||
|
||||
top: calc(var(--layout-height-header));
|
||||
align-self: flex-start;
|
||||
|
||||
@media (--desktop) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import 'normalize.css';
|
||||
|
||||
@import './vars.pcss';
|
||||
@import './layout.pcss';
|
||||
@import './carbon.pcss';
|
||||
|
@ -9,6 +10,7 @@
|
|||
@import './components/auth.pcss';
|
||||
@import './components/button.pcss';
|
||||
@import './components/sidebar.pcss';
|
||||
@import './components/table-of-content.pcss';
|
||||
|
||||
body {
|
||||
font-family: system-ui, Helvetica, Arial, Verdana;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue