Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions _includes/partials/_nav-links.njk
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
<a href="{{ page | relative }}/get/">Get<span class="wide"> Color.js</span></a>
<div class="menu">
<a href="{{ page | relative }}/get/" data-nav-item="1">Get<span class="wide"> Color.js</span></a>
<div class="menu" data-nav-item="2">
<a href="{{ page | relative }}/docs/">Docs</a>
<ul>
{% include "./_docs-nav.njk" %}
</ul>
</div>
<a href="{{ page | relative }}/api/">API</a>
<a href="{{ page | relative }}/notebook/">Play!</a>
<a href="https://apps.colorjs.io/">Demos</a>
<a href="https://elements.colorjs.io/">Elements</a>
<a href="{{ page | relative }}/test/" class="footer">Tests</a>
<a href="https://github.com/LeaVerou/color.js">GitHub</a>
<a href="https://discord.gg/K64FJBznq4">Discord</a>
<a href="https://opencollective.com/color">♡&nbsp;Sponsor</a>
<a href="https://github.com/LeaVerou/color.js/issues/new" class="footer">File bug</a>
<a href="{{ page | relative }}/api/" data-nav-item="3">API</a>
<a href="{{ page | relative }}/notebook/" data-nav-item="4">Play!</a>
<a href="https://apps.colorjs.io/" data-nav-item="5">Demos</a>
<a href="https://elements.colorjs.io/" data-nav-item="6">Elements</a>
<a href="{{ page | relative }}/test/" class="footer" data-nav-item="7">Tests</a>
<a href="https://github.com/LeaVerou/color.js" data-nav-item="8">GitHub</a>
<a href="https://discord.gg/K64FJBznq4" data-nav-item="9">Discord</a>
<a href="https://opencollective.com/color" data-nav-item="10">♡&nbsp;Sponsor</a>
<a href="https://github.com/LeaVerou/color.js/issues/new" class="footer" data-nav-item="11">File bug</a>
<div class="hamburger-menu">
<button class="hamburger-button" aria-label="Menu">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 48 48">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="M7.95 11.95h32m-32 12h32m-32 12h32"/>
</svg>
</button>
<ul class="hamburger-list">
<li data-nav-item="1" hidden><a href="{{ page | relative }}/get/">Get Color.js</a></li>
<li data-nav-item="2" hidden><a href="{{ page | relative }}/docs/">Docs</a></li>
<li data-nav-item="3" hidden><a href="{{ page | relative }}/api/">API</a></li>
<li data-nav-item="4" hidden><a href="{{ page | relative }}/notebook/">Play!</a></li>
<li data-nav-item="5" hidden><a href="https://apps.colorjs.io/">Demos</a></li>
<li data-nav-item="6" hidden><a href="https://elements.colorjs.io/">Elements</a></li>
<li data-nav-item="8" hidden><a href="https://github.com/LeaVerou/color.js">GitHub</a></li>
<li data-nav-item="9" hidden><a href="https://discord.gg/K64FJBznq4">Discord</a></li>
<li data-nav-item="10" hidden><a href="https://opencollective.com/color">♡&nbsp;Sponsor</a></li>
</ul>
</div>
1 change: 1 addition & 0 deletions _includes/plain.njk
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<link rel="stylesheet" href="{{ page | relative }}/assets/css/style.css">

<script src="{{ page | relative }}/color.js" type="module"></script>
<script src="{{ page | relative }}/assets/js/nav.js" type="module"></script>

{% if has_mavo %}
<link rel="stylesheet" href="https://get.mavo.io/mavo.css">
Expand Down
37 changes: 33 additions & 4 deletions assets/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ body > footer {
animation: var(--rainbow-scroll);

& nav {
& > .menu {
& > :where(.menu, .hamburger-menu) {
flex: 1;
display: flex;
position: relative;
Expand All @@ -224,13 +224,14 @@ body > footer {
}
}

& a {
& a:where(:not([hidden])) {
display: block;
text-align: center;
}

& > .menu > a,
& a:not(.logo) {
& a:not(.logo),
& .hamburger-button {
flex: 1;
padding: .6em;
font-weight: 800;
Expand All @@ -243,6 +244,33 @@ body > footer {
text-decoration: none;
}
}

.hamburger-menu {
&:has(li:last-of-type[hidden]) {
/* All menu items are hidden; hide the menu */
display: none;
}

& .hamburger-button {
display: grid;
place-items: center;
border: none;
cursor: default;
text-align: center;
color: hsl(var(--gray) 40%);

> svg {
width: 100%;
}
}

& .hamburger-list {
min-inline-size: 15ch;
right: 0;
top: 100%;
transform-origin: top right;
}
}
}
}

Expand Down Expand Up @@ -295,6 +323,7 @@ body > header {

& img {
height: 2.15em;
min-width: 1.3em;
margin-bottom: -1.5em;
margin-left: -.3em;
}
Expand Down Expand Up @@ -426,7 +455,7 @@ pre[class*="language-"] {
}

@supports (-webkit-background-clip: text) and (not (-moz-margin-start: 0)) {
body > header nav a,
body > header nav :where(a, .hamburger-button),
body > footer nav a,
main h2,
main h2 > a {
Expand Down
77 changes: 77 additions & 0 deletions assets/js/nav.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Responsive Navigation Script
* Dynamically hides navigation items that don't fit on screen in the main nav
* and shows them in the hamburger menu instead. Uses ResizeObserver to monitor
* the nav element and checks items from right to left to determine which ones
* overflow. Items are matched between nav and hamburger menu using data-nav-item
* attributes.
*/

const nav = document.querySelector("header nav");
const menu = nav.querySelector(".hamburger-menu");
const [menuButton, menuList] = menu.children;

// Map to track nav items and their hamburger menu counterparts
let itemMap = new Map();

// Get all nav items (excluding the hamburger menu and the ones that are supposed to be shown in the footer)
let navItems = [...nav.children].filter(
child => !child.classList.contains("hamburger-menu") && !child.classList.contains("footer"),
);

let hamburgerMenuItems = [...menuList.children];
for (let navItem of navItems) {
let index = navItem.dataset.navItem;
let hamburgerItem = hamburgerMenuItems.find(item => item.dataset.navItem === index);
if (hamburgerItem) {
itemMap.set(navItem, hamburgerItem);
}
}

const resizeObserver = new ResizeObserver(checkFit);
resizeObserver.observe(nav);

function checkFit () {
// Reset: show all items in nav, hide all in hamburger
itemMap.forEach((hamburgerItem, navItem) => {
navItem.hidden = false;
hamburgerItem.hidden = true;
});

// Temporarily show hamburger menu to measure its width
menu.style.setProperty("display", "block");

let hamburgerButtonWidth = menuButton.offsetWidth ?? 50;
let navRect = nav.getBoundingClientRect();
let navRight = navRect.right;
let availableRight = navRight - hamburgerButtonWidth;

let items = [...itemMap.keys()];
let toHide = [];

// Check each item from right to left to see which ones overflow
// by comparing their right edge to available space
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i];
const itemRect = item.getBoundingClientRect();
const itemRight = itemRect.right;
// If this item's right edge exceeds available space, hide it
if (itemRight > availableRight) {
toHide.push(item);
}
else {
// Once we find an item that fits, we can stop
break;
}
}

menu.style.removeProperty("display");

// Hide items in nav and show corresponding items in hamburger menu
for (let navItem of toHide) {
let hamburgerItem = itemMap.get(navItem);

navItem.hidden = true;
hamburgerItem.hidden = false;
}
}