Skip to content

Commit

Permalink
feat: venn-ish diagram (#539)
Browse files Browse the repository at this point in the history
  • Loading branch information
fkakatie authored Nov 13, 2024
1 parent f0ab93b commit 2037329
Show file tree
Hide file tree
Showing 4 changed files with 394 additions and 41 deletions.
193 changes: 193 additions & 0 deletions blocks/venn/venn.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
.venn {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
position: relative;
max-width: 90%;
margin: auto;
}

.venn .venn-content {
position: relative;
}

.venn .venn-content > div {
box-sizing: border-box;
padding: var(--image-border-radius-xxl);
}

.venn .venn-content .venn-content-left {
background-color: #F4FADE;
border: 2px solid var(--color-accent-lightgreen-content);
border-bottom: 0;
border-radius: 50vw 50vw 0 0;
padding-top: 18vw;
}

.venn .venn-content .venn-content-right {
background-color: var(--color-accent-purple-content);
border: 2px solid var(--color-accent-purple-bg);
border-top: 0;
border-radius: 0 0 50vw 50vw;
padding-bottom: 18vw;
}

.venn .venn-content .venn-content-intersection {
border: 2px solid #3576ae;
background-color: #def3fc;
}

.venn .venn-content h2 {
margin-bottom: 0.5em;
font-size: var(--type-heading-xl-size);
line-height: var(--type-heading-xl-lh);
text-align: center;
}

.venn .venn-content ul {
--venn-progress: 50%;

list-style-type: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5em;
text-align: center;
}

.venn .venn-content li {
border: 2px solid var(--color-white);
border-radius: var(--image-border-radius-m);
background-color: #fff6;
font-size: var(--type-body-xs-size);
line-height: 1.3;
transition: transform 0.4s, opacity 0.2s;
}

.venn .venn-content li.below-range {
transform: scale(0.95);
opacity: 0.6;
}

.venn .venn-content li.in-range {
background: linear-gradient(105deg, var(--color-accent-pink-bg), #f0b2f244 var(--venn-progress), #fff6 var(--venn-progress));
}

.venn .venn-content li.exceeds-range {
background-color: var(--color-accent-pink-bg);
}

.venn .venn-content li:hover {
opacity: 1;
}

.venn .venn-content a {
display: block;
width: 100%;
border-radius: var(--image-border-radius-s);
padding: calc(0.5em - 4px) 1em;
background-color: transparent;
color: var(--color-black);
font-style: normal;
font-weight: 600;
text-decoration: none;
transition: background-color .2s;
}

.venn .venn-content a:hover {
background-color: #fff8;
}

@media (width >= 900px) {
.venn .venn-content {
display: grid;
grid-template-columns: repeat(2, 1fr);
position: relative;
}

.venn .venn-content > div {
border-radius: var(--image-border-radius-xxl);
padding: var(--image-border-radius-xxl);
}

.venn .venn-content .venn-content-left {
border: 2px solid var(--color-accent-lightgreen-content);
border-right-width: 1px;
border-radius: var(--image-border-radius-xxl) 0 0 var(--image-border-radius-xxl);
padding-top: var(--image-border-radius-xxl);
padding-right: calc(33% + var(--image-border-radius-xl));
}

.venn .venn-content .venn-content-right {
border: 2px solid var(--color-accent-purple-bg);
border-left-width: 1px;
border-radius: 0 var(--image-border-radius-xxl) var(--image-border-radius-xxl) 0;
padding-bottom: var(--image-border-radius-xxl);
padding-left: calc(33% + var(--image-border-radius-xl));
}

.venn .venn-content .venn-content-intersection {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: calc(100% / 3);
border-radius: var(--image-border-radius-xxl);
}
}

/* slider */
.venn .venn-slider {
box-sizing: border-box;
font-weight: var(--type-detail-all-weight);
}

.venn .venn-slider input {
appearance: none;
position: relative;
width: 100%;
height: var(--circular-icon-tag-size);
margin: 0;
border-radius: var(--image-border-radius-xxl);
background-color: var(--bg-color-grey);
cursor: pointer;
overflow: hidden;
transition: outline 0.2s;
}

.venn .venn-slider input:hover,
.venn .venn-slider input:focus {
outline: 2px solid var(--color-accent-pink-content);
}

.venn .venn-slider input::-webkit-slider-thumb {
appearance: none;
width: var(--circular-icon-tag-size);
height: var(--circular-icon-tag-size);
border-radius: var(--image-border-radius-xxl) 0 0 var(--image-border-radius-xxl);
background-color: var(--color-accent-pink-bg);
cursor: ew-resize;
box-shadow: -100vw 0 0 100vw var(--color-accent-pink-bg);
}

.venn .venn-slider label {
display: block;
text-align: center;
margin-bottom: 0.5rem;
}

.venn .venn-slider .venn-ticks {
display: flex;
justify-content: space-between;
font-size: var(--type-detail-l-size);
line-height: var(--type-detail-l-lh);
text-transform: var(--type-detail-m-transform);
}

@media (width >= 900px) {
.venn .venn-slider {
order: 1;
}
}
89 changes: 89 additions & 0 deletions blocks/venn/venn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { createTag } from '../../scripts/scripts.js';

/**
* Updates the 'level' query parameter in the URL.
* @param {number} value Value for the 'level' parameter.
*/
function updateLevelParam(value) {
const params = new URLSearchParams(window.location.search);
params.set('level', value);
const url = `${window.location.pathname}?${params.toString()}`;
window.history.replaceState({ path: url }, '', url);
}

/**
* Updates the venn content display based on the value of the range.
* @param {HTMLInputElement} range Range input element.
* @param {number} value Value of the range.
* @param {NodeListOf<HTMLLIElement>} lis List of <li> elements to update.
*/
function updateVennDisplay(value, lis) {
lis.forEach((li) => {
li.removeAttribute('style');
const min = parseInt(li.dataset.min, 10);
const max = parseInt(li.dataset.max, 10);
if (value < min) li.className = 'below-range';
else if (value >= min && value < max) {
li.className = 'in-range';
// calculate the percentage position of 'value' within the range min - max
li.style.setProperty('--venn-progress', `${((value - min + 1) / (max - min + 1)) * 100}%`);
} else li.className = 'exceeds-range';
});
}

export default async function decorate(block) {
const content = block.firstElementChild;
content.className = 'venn-content';
const segments = ['left', 'intersection', 'right'];
[...content.children].forEach((segment, i) => {
const layout = segments[i];
if (layout) segment.className = `venn-content-${layout}`;
});

// establish skill level ranges
let sliderMin = 0;
let sliderMax = 0;
const lis = block.querySelectorAll('li');
lis.forEach((wrapper) => {
// identify skill from <a> tag
const skill = wrapper.querySelector('a');
// separate skill level range from skill
const levels = wrapper.textContent.replace(skill.textContent, '').trim();
wrapper.innerHTML = skill.outerHTML;
if (levels) {
// extract min and max from 'levels' string range
const [min, max] = levels.replace('(', '').split(',').map((n) => parseInt(n, 10));
if (sliderMin === 0 || min < sliderMin) sliderMin = min;
if (sliderMax === 0 || max > sliderMax) sliderMax = max;
wrapper.dataset.min = min;
wrapper.dataset.max = max;
}
});

// build skill slider
const slider = createTag('div', { class: 'venn-slider' });
const range = createTag('input', {
type: 'range', name: 'venn-slider', id: 'venn-slider', step: 1,
});
// set range min and max based on skill level values
range.min = sliderMin;
range.max = sliderMax;
range.addEventListener('input', () => {
updateLevelParam(range.value);
updateVennDisplay(parseInt(range.value, 10), lis);
});
const label = createTag('label', { type: 'range', for: 'venn-slider' });
label.textContent = 'Skill Level';
const ticks = createTag('div', { class: 'venn-ticks' });
// TODO: remove hard-coded text
ticks.innerHTML = `<span>Beginner</span>
<span>Expert</span>`;
slider.append(label, range, ticks);
block.prepend(slider);

// retrieve level query param
const params = new URLSearchParams(window.location.search);
const level = params.get('level') || sliderMin;
range.value = level;
updateVennDisplay(level, lis);
}
8 changes: 8 additions & 0 deletions scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,13 @@ export function decorateGuideTemplate(main) {
decorateGuideTemplateLinks(main);
}

export function decoratesSkillTemplate(main) {
if (!document.body.classList.contains('skills-template') || !main) return;
decorateGuideTemplateHeadings(main);
decorateGuideTemplateHero(main);
decorateGuideTemplateLinks(main);
}

/**
* Clean up variant classes
* Ex: marquee--small--contained- -> marquee small contained
Expand Down Expand Up @@ -608,6 +615,7 @@ export function decorateMain(main) {
customDecorateButtons(main);
decorateHeadings(main);
decorateGuideTemplate(main);
decoratesSkillTemplate(main);
decorateBlocks(main);
decorateTitleSection(main);
decorateSVGs(main);
Expand Down
Loading

0 comments on commit 2037329

Please sign in to comment.