1
0
Fork 0
mirror of https://github.com/codex-team/codex.docs.git synced 2025-07-18 20:59:42 +02:00

Sidebar redesign (#200)

* New sidebar

* Save state to local storage

* Make sidebar sticky

* Text overflow

* Fix add page button on mobile

* Mobile layout

* Display sidebar when ready

* Add logo

* Remove files

* Fix margin

* Update logo padding-bottom

* Hovers

* Decrease logo's font size

* Make logo not sticky

* Cleanup classnames

* Simplify css

* Update sidebar module

* Fix animation

* Fix cursor issue

* Fix vars and logo paddings
This commit is contained in:
Tanya 2022-06-16 21:37:37 +08:00 committed by GitHub
parent 16ba86fddb
commit 30d96909d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 531 additions and 185 deletions

View file

@ -1,49 +0,0 @@
<div class="docs-aside-toggler" onclick="document.querySelector('.docs-aside').classList.toggle('docs-aside--toggled')">
{{ svg('menu') }} Table of contents
</div>
<div class="docs-aside">
{% for firstLevelPage in menu %}
<section class="docs-aside__section">
<a
{% if page is defined and page._id == firstLevelPage._id%}
class="docs-aside__section-title docs-aside__current"
{% else %}
class="docs-aside__section-title"
{% endif %}
{% if firstLevelPage.uri %}
href="/{{ firstLevelPage.uri }}"
{% else %}
href="/page/{{ firstLevelPage._id }}"
{% endif %}>
{{ firstLevelPage.title | striptags }}
</a>
{% if firstLevelPage.children is not empty %}
<ul class="docs-aside__section-list">
{% for child in firstLevelPage.children %}
<li>
<a
{% if page is defined and page._id == child._id %}
class="docs-aside__current"
{% endif %}
{% if child.uri %}
href="/{{ child.uri }}"
{% else %}
href="/page/{{ child._id }}"
{% endif %}>
{{ child.title | striptags }}
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</section>
{% endfor %}
<a class="docs-aside__logo-wrapper" href="https://github.com/codex-team/codex.docs">
<div class="logo">
{{ svg('aside-logo') }}
</div>
<div class="caption">
<span>Powered by CodeX Docs</span>
</div>
</a>
</div>

View file

@ -4,9 +4,12 @@
</a>
<ul class="docs-header__menu">
{% if isAuthorized == true %}
<li class="docs-header__menu-add">
<li class="docs-header__menu-add docs-header__menu-add--desktop">
{% include 'components/button.twig' with {label: 'Add page', icon: 'plus', size: 'small', url: '/page/new'} %}
</li>
<li class="docs-header__menu-add docs-header__menu-add--mobile">
{% include 'components/button.twig' with {icon: 'plus', size: 'small', url: '/page/new'} %}
</li>
{% endif %}
{% for option in config.menu %}
<li>

View file

@ -0,0 +1,55 @@
<div data-module="sidebar" class="docs-sidebar">
<div class="docs-sidebar__toggler">
{{ svg('menu') }} Table of contents
</div>
<aside class="docs-sidebar__content docs-sidebar__content--hidden">
{% for firstLevelPage in menu %}
<section class="docs-sidebar__section" data-id="{{firstLevelPage._id}}">
<a class="docs-sidebar__section-title-wrapper"
href="{{firstLevelPage.uri ? '/' ~ firstLevelPage.uri : '/page/' ~ firstLevelPage._id }}"
>
<div class="docs-sidebar__section-title {{page is defined and page._id == firstLevelPage._id ? 'docs-sidebar__section-title--active' : ''}}">
<span>
{{ firstLevelPage.title | striptags }}
</span>
{% if firstLevelPage.children is not empty %}
<button class="docs-sidebar__section-toggler">
{{ svg('arrow-up') }}
</button>
{% endif %}
</div>
</a>
{% if firstLevelPage.children is not empty %}
<ul class="docs-sidebar__section-list">
{% for child in firstLevelPage.children %}
<li>
<a
class="docs-sidebar__section-list-item-wrapper"
href="{{ child.uri ? '/' ~ child.uri : '/page/' ~ child._id }}">
<div class="docs-sidebar__section-list-item {{page is defined and page._id == child._id ? 'docs-sidebar__section-list-item--active' : ''}}">
<span>{{ child.title | striptags }}</span>
</div>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
</section>
{% endfor %}
<div class="docs-sidebar__logo">
<a class="docs-sidebar__logo-wrapper" href="https://github.com/codex-team/codex.docs">
<div class="docs-sidebar__logo-image">
{{ svg('aside-logo') }}
</div>
<p class="docs-sidebar__logo-caption">
Powered by CodeX Docs
</p>
</a>
</div>
</aside>
</div>

View file

@ -17,9 +17,9 @@
<body>
{% include "components/header.twig" with res.locals.isAuthorized %}
<div class="docs">
<aside class="docs__aside">
{% include "components/aside.twig" %}
</aside>
{% include "components/sidebar.twig" %}
<div class="docs__content">
<div class="docs__content-inner">
{% block body %}{% endblock %}

View file

@ -16,6 +16,7 @@ import ModuleDispatcher from 'module-dispatcher';
import Writing from './modules/writing';
import Page from './modules/page';
import Extensions from './modules/extensions';
import Sidebar from './modules/sidebar';
/**
* Main app class
@ -28,6 +29,7 @@ class Docs {
this.writing = new Writing();
this.page = new Page();
this.extensions = new Extensions();
this.sidebar = new Sidebar();
document.addEventListener('DOMContentLoaded', (event) => {
this.docReady();

View file

@ -0,0 +1,178 @@
import { Storage } from '../utils/storage';
/**
* Local storage key
*/
const LOCAL_STORAGE_KEY = 'docs_sidebar_state';
/**
* Section list item height in px
*/
const ITEM_HEIGHT = 31;
/**
* Sidebar module
*/
export default class Sidebar {
/**
* CSS classes
*
* @returns {Record<string, string>}
*/
static get CSS() {
return {
toggler: 'docs-sidebar__section-toggler',
section: 'docs-sidebar__section',
sectionCollapsed: 'docs-sidebar__section--collapsed',
sectionAnimated: 'docs-sidebar__section--animated',
sectionTitle: 'docs-sidebar__section-title',
sectionTitleActive: 'docs-sidebar__section-title--active',
sectionList: 'docs-sidebar__section-list',
sectionListItemActive: 'docs-sidebar__section-list-item--active',
sidebarToggler: 'docs-sidebar__toggler',
sidebarContent: 'docs-sidebar__content',
sidebarContentHidden: 'docs-sidebar__content--hidden',
};
}
/**
* Creates base properties
*/
constructor() {
/**
* Stores refs to HTML elements needed for correct sidebar work
*/
this.nodes = {
sections: [],
sidebarContent: null,
toggler: null,
};
this.sidebarStorage = new Storage(LOCAL_STORAGE_KEY);
const storedState = this.sidebarStorage.get();
this.sectionsState = storedState ? JSON.parse(storedState) : {};
}
/**
* Called by ModuleDispatcher to initialize module from DOM
*
* @param {writingSettings} settings - module settings
* @param {HTMLElement} moduleEl - module element
*/
init(settings, moduleEl) {
this.nodes.sections = Array.from(moduleEl.querySelectorAll('.' + Sidebar.CSS.section));
this.nodes.sections.forEach(section => this.initSection(section));
this.nodes.sidebarContent = moduleEl.querySelector('.' + Sidebar.CSS.sidebarContent);
this.nodes.toggler = moduleEl.querySelector('.' + Sidebar.CSS.sidebarToggler);
this.nodes.toggler.addEventListener('click', () => this.toggleSidebar());
this.ready();
}
/**
* Initializes sidebar sections: applies stored state and adds event listeners
*
* @param {HTMLElement} section
* @returns {void}
*/
initSection(section) {
const id = section.dataset.id;
const togglerEl = section.querySelector('.' + Sidebar.CSS.toggler);
if (!togglerEl) {
return;
}
togglerEl.addEventListener('click', e => this.handleSectionTogglerClick(id, section, e));
if (typeof this.sectionsState[id] === 'undefined') {
this.sectionsState[id] = false;
}
if (this.sectionsState[id]) {
this.setSectionCollapsed(section, true, false);
}
/**
* Calculate and set sections list max height for smooth animation
*/
const sectionList = section.querySelector('.' + Sidebar.CSS.sectionList);
if (!sectionList) {
return;
}
const itemsCount = sectionList.children.length;
sectionList.style.maxHeight = `${ itemsCount * ITEM_HEIGHT }px`;
}
/**
* Toggles section expansion
*
* @param {number} sectionId - id of the section to toggle
* @param {HTMLElement} sectionEl - section html element
* @param {MouseEvent} event - click event
* @returns {void}
*/
handleSectionTogglerClick(sectionId, sectionEl, event) {
event.preventDefault();
this.sectionsState[sectionId] = !this.sectionsState[sectionId];
this.sidebarStorage.set(JSON.stringify(this.sectionsState));
this.setSectionCollapsed(sectionEl, this.sectionsState[sectionId]);
}
/**
* Updates section's collapsed state
*
* @param {HTMLElement} sectionEl - element of the section to toggle
* @param {boolean} collapsed - new collapsed state
* @param {boolean} [animated] - true if state should change with animation
*/
setSectionCollapsed(sectionEl, collapsed, animated = true) {
const sectionList = sectionEl.querySelector('.' + Sidebar.CSS.sectionList);
if (!sectionList) {
return;
}
sectionEl.classList.toggle(Sidebar.CSS.sectionAnimated, animated);
sectionEl.classList.toggle(Sidebar.CSS.sectionCollapsed, collapsed);
/**
* Highlight section item as active if active child item is collapsed.
*/
const activeSectionListItem = sectionList.querySelector('.' + Sidebar.CSS.sectionListItemActive);
const sectionTitle = sectionEl.querySelector('.' + Sidebar.CSS.sectionTitle);
if (!activeSectionListItem) {
return;
}
if (collapsed && animated) {
/**
* Highlights section title as active with a delay to let section collapse animation finish first
*/
setTimeout(() => {
sectionTitle.classList.toggle(Sidebar.CSS.sectionTitleActive, collapsed);
}, 200);
} else {
sectionTitle.classList.toggle(Sidebar.CSS.sectionTitleActive, collapsed);
}
}
/**
* Toggles sidebar visibility
*
* @returns {void}
*/
toggleSidebar() {
this.nodes.sidebarContent.classList.toggle(Sidebar.CSS.sidebarContentHidden);
}
/**
* Displays sidebar when ready
*
* @returns {void}
*/
ready() {
this.nodes.sidebarContent.classList.remove(Sidebar.CSS.sidebarContentHidden);
}
}

View file

@ -0,0 +1,30 @@
/**
* Utility class to handle interaction with local storage
*/
export class Storage {
/**
* @param {string} key - storage key
*/
constructor(key) {
this.key = key;
}
/**
* Saves value to storage
*
* @param {string} value - value to be saved
*/
set(value) {
localStorage.setItem(this.key, value);
}
/**
* Retreives value from storage
*
* @returns {string}
*/
get() {
return localStorage.getItem(this.key);
}
}

View file

@ -1,92 +0,0 @@
.docs-aside {
font-size: 14px;
letter-spacing: 0.01em;
@media (--mobile) {
position: static;
overflow: visible;
max-height: none;
font-size: 13px;
display: none;
margin-top: 20px;
}
&--toggled {
display: block !important;
}
a {
color: inherit;
text-decoration: none;
&:hover {
color: var(--color-link-active);
}
}
&__section {
margin-bottom: 30px;
@media (--mobile) {
margin-bottom: 20px;
}
&:last-of-type {
margin-bottom: 0;
}
&-title {
display: inline-block;
margin-bottom: 10px;
font-size: 1.18em;
font-weight: 600;
@media (--mobile) {
margin-bottom: 10px;
}
}
&-list {
padding-left: 0;
margin: 0;
list-style: none;
a {
display: inline-block;
color: inherit;
padding: 8px 0;
line-height: 1.5em;
}
}
}
&__current {
color: var(--color-page-active) !important;
}
&__logo-wrapper {
margin-top: 5rem;
display: flex;
align-items: center;
gap: 18px;
.logo {
display: inline-flex;
}
}
}
.docs-aside-toggler {
display: none;
font-size: 13px;
cursor: pointer;
color: var(--color-text-second);
@media (--mobile) {
display: block;
}
svg {
margin-right: 10px;
}
}

View file

@ -14,10 +14,8 @@
transition-duration: 0.1s;
border-radius: 8px;
@supports(-webkit-mask-box-image: url('')){
border-radius: 0;
-webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 10.3872C0 1.83334 1.83334 0 10.3872 0H13.6128C22.1667 0 24 1.83334 24 10.3872V13.6128C24 22.1667 22.1667 24 13.6128 24H10.3872C1.83334 24 0 22.1667 0 13.6128V10.3872Z' fill='black'/%3E%3C/svg%3E%0A") 48% 41% 37.9% 53.3%;;
}
@apply --squircle;
&__icon {
display: inline-flex;

View file

@ -7,6 +7,12 @@
font-size: 18px;
flex-wrap: wrap;
position: relative;
height: var(--layout-height-header);
box-sizing: border-box;
position: sticky;
top: 0;
background: white;
z-index: 10;
@media (--mobile){
line-height: 40px;
@ -60,25 +66,24 @@
}
}
&-add {
@media (--mobile) {
position: absolute;
right: 15px;
top: 15px;
line-height: 1em;
margin: 0 !important;
}
a {
li&-add {
flex-shrink: 0;
&--desktop {
@media (--mobile) {
font-size: 0;
padding: 0 4px;
margin-right: 0;
display: none;
}
}
.docs-button__icon {
&--mobile {
display: none;
@media (--mobile) {
margin-right: 0;
display: block;
position: absolute;
right: 15px;
top: 15px;
line-height: 1em;
}
}
}

View file

@ -0,0 +1,216 @@
.docs-sidebar {
width: 100vw;
@media (--desktop) {
width: var(--layout-sidebar-width);
}
&__content {
border-bottom: 1px solid var(--color-line-gray);
box-sizing: border-box;
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
position: sticky;
top: var(--layout-height-header);
display: flex;
flex-direction: column;
overflow: auto;
@media (--desktop) {
height: calc(100vh - var(--layout-height-header));
border-right: 1px solid var(--color-line-gray);
border-bottom: 0;
padding-bottom: 0;
}
&--hidden {
display: none;
}
}
&__section {
overflow: hidden;
flex-shrink: 0;
&--animated {
.docs-sidebar__section-list {
transition: max-height 200ms ease-in-out;
}
.docs-sidebar__section-toggler {
svg {
transition: transform 200ms ease-in;
}
}
}
&--collapsed {
.docs-sidebar__section-list {
max-height: 0 !important;
}
.docs-sidebar__section-toggler {
svg {
transform: rotate(180deg);
}
}
}
}
&__section:not(:first-child) {
margin-top: 20px;
}
&__section-title {
font-size: 16px;
line-height: 24px;
font-weight: 700;
z-index: 2;
position: relative;
height: 34px;
}
&__section-list-item {
font-size: 14px;
line-height: 21px;
height: 29px;
}
&__section-title,
&__section-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
transition-property: background-color;
transition-duration: 0.1s;
@apply --squircle;
}
&__section-title > span,
&__section-list-item > span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__section-title-wrapper {
&:not(:last-child) {
padding-bottom: 1px;
display: block;
}
}
&__section-list-item-wrapper {
padding: 1px 0;
display: block;
}
li:last-child {
.docs-sidebar__section-list-item-wrapper {
padding-bottom: 0;
}
}
&__section-title:not(&__section-title--active),
&__section-list-item:not(&__section-list-item--active) {
@media (--can-hover) {
&:hover {
background: var(--color-link-hover);
}
}
}
&__section-title--active,
&__section-list-item--active {
background: linear-gradient(270deg, #129BFF 0%, #8A53FF 100%);
color: white;
@media (--can-hover) {
.docs-sidebar__section-toggler:hover {
background: rgba(0,0,0,0.3);
}
}
}
&__section-list {
list-style: none;
padding: 0;
margin: 0;
z-index: 1;
position: relative;
}
&__section-toggler {
color: inherit;
background: transparent;
padding: 0;
border: 0;
cursor: pointer;
width: 24px;
height: 24px;
transition-property: background-color;
transition-duration: 0.1s;
@apply --squircle;
@media (--can-hover) {
&:hover {
background: white;
}
}
svg {
display: block;
}
}
&__toggler {
font-size: 13px;
cursor: pointer;
color: var(--color-text-second);
padding: 20px 15px;
border-bottom: 1px solid var(--color-line-gray);
@media (--desktop) {
display: none;
}
svg {
margin-right: 10px;
}
}
&__logo {
display: none;
margin-top: auto;
background: white;
z-index: 2;
padding-bottom: 20px;
padding-top: 60px;
font-size: 14px;
@media (--desktop) {
display: block;
}
&-image{
display: inline-flex;
}
&-caption {
margin: 0;
}
&-wrapper {
display: flex;
align-items: center;
gap: 18px;
padding: 8px;
}
}
}

View file

@ -1,27 +1,15 @@
.docs {
display: flex;
padding: 0 var(--layout-padding-horizontal);
min-height: calc(100vh - var(--layout-height-header));
@media (--mobile) {
flex-wrap: wrap;
}
&__aside {
width: var(--layout-width-aside);
@media (--mobile) {
width: 100%;
flex-basis: 100%;
padding: 20px var(--layout-padding-horizontal) !important;
margin: 0 calc(-1 * var(--layout-padding-horizontal));
border-bottom: 1px solid var(--color-line-gray);
}
@media (--desktop) {
display: flex;
}
&__content {
flex-grow: 2;
word-wrap: break-word;
@media (--mobile) {
width: 100%;
flex-basis: 100%;
@ -33,12 +21,11 @@
}
}
&__aside,
&__content {
padding: var(--layout-padding-vertical) 0;
padding: var(--layout-padding-vertical) var(--layout-padding-horizontal);
@media (--mobile) {
padding: 20px 0;
padding: 20px var(--layout-padding-horizontal);
}
}
}

View file

@ -3,12 +3,12 @@
@import './layout.pcss';
@import './carbon.pcss';
@import './components/header.pcss';
@import './components/aside.pcss';
@import './components/writing.pcss';
@import './components/page.pcss';
@import './components/landing.pcss';
@import './components/auth.pcss';
@import './components/button.pcss';
@import './components/sidebar.pcss';
body {
font-family: system-ui, Helvetica, Arial, Verdana;

View file

@ -23,14 +23,16 @@
/**
* Site layout sizes
*/
--layout-padding-horizontal: 16px;
--layout-padding-horizontal: 22px;
--layout-padding-vertical: 30px;
--layout-width-aside: 300px;
--layout-sidebar-width: 344px;
--layout-width-main-col: 650px;
--layout-height-header: 56px;
@media (--mobile) {
--layout-padding-horizontal: 15px;
--layout-padding-vertical: 15px;
--layout-height-header: 88px;
}
--font-mono: Menlo,Monaco,Consolas,Courier New,monospace;
@ -70,6 +72,13 @@
background: color-mod(var(--color-link-active) blackness(+10%));
}
}
--squircle {
@supports(-webkit-mask-box-image: url('')){
border-radius: 0;
-webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 10.3872C0 1.83334 1.83334 0 10.3872 0H13.6128C22.1667 0 24 1.83334 24 10.3872V13.6128C24 22.1667 22.1667 24 13.6128 24H10.3872C1.83334 24 0 22.1667 0 13.6128V10.3872Z' fill='black'/%3E%3C/svg%3E%0A") 48% 41% 37.9% 53.3%;;
}
}
}
/**
@ -80,3 +89,4 @@
@custom-media --tablet all and (min-width: 980px) and (max-width: 1050px);
@custom-media --mobile all and (max-width: 980px);
@custom-media --retina all and (-webkit-min-device-pixel-ratio: 1.5);
@custom-media --can-hover all and (hover:hover)

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M7.29571 15.3171C7.48307 15.5034 7.73652 15.6079 8.00071 15.6079C8.26489 15.6079 8.51834 15.5034 8.70571 15.3171L12.2957 11.7771L15.8357 15.3171C16.0231 15.5034 16.2765 15.6079 16.5407 15.6079C16.8049 15.6079 17.0583 15.5034 17.2457 15.3171C17.3394 15.2242 17.4138 15.1136 17.4646 14.9917C17.5154 14.8698 17.5415 14.7391 17.5415 14.6071C17.5415 14.4751 17.5154 14.3444 17.4646 14.2225C17.4138 14.1007 17.3394 13.9901 17.2457 13.8971L13.0057 9.65712C12.9127 9.56339 12.8021 9.48899 12.6803 9.43823C12.5584 9.38746 12.4277 9.36132 12.2957 9.36132C12.1637 9.36132 12.033 9.38746 11.9111 9.43823C11.7893 9.48899 11.6787 9.56339 11.5857 9.65712L7.29571 13.8971C7.20198 13.9901 7.12758 14.1007 7.07681 14.2225C7.02605 14.3444 6.99991 14.4751 6.99991 14.6071C6.99991 14.7391 7.02605 14.8698 7.07681 14.9917C7.12758 15.1136 7.20198 15.2242 7.29571 15.3171Z"/>
</svg>

After

Width:  |  Height:  |  Size: 954 B