From 5df399d2f7fe1134dbf975a47d58222cd13c440e Mon Sep 17 00:00:00 2001 From: nym21 Date: Sun, 31 May 2026 12:05:48 +0200 Subject: [PATCH] heatmaps: part 7 --- website/scripts/utils/dom.js | 77 ++++----- website/styles/chart.css | 290 +++++++++++++++------------------- website/styles/components.css | 17 +- website/styles/elements.css | 5 + 4 files changed, 171 insertions(+), 218 deletions(-) diff --git a/website/scripts/utils/dom.js b/website/scripts/utils/dom.js index 3b5b1e110..617fad2bc 100644 --- a/website/scripts/utils/dom.js +++ b/website/scripts/utils/dom.js @@ -32,6 +32,15 @@ export function onFirstIntersection(element, callback) { observer.observe(element); } +/** + * @param {string} text + */ +export function createSpan(text) { + const span = window.document.createElement("span"); + span.textContent = text; + return span; +} + /** * @param {string} name */ @@ -192,7 +201,6 @@ export function createLabeledInput({ * @param {Object} args * @param {T} args.initialValue * @param {string} [args.id] - * @param {string} [args.legend] * @param {readonly T[]} args.choices * @param {(value: T) => void} [args.onChange] * @param {(choice: T) => string} [args.toKey] @@ -201,7 +209,6 @@ export function createLabeledInput({ */ export function createRadios({ id, - legend, choices, initialValue, onChange, @@ -209,7 +216,7 @@ export function createRadios({ toLabel = /** @type {(choice: T) => string} */ ((c) => String(c)), toTitle, }) { - const field = window.document.createElement("fieldset"); + const fieldset = window.document.createElement("fieldset"); const initialKey = toKey(initialValue); @@ -218,41 +225,32 @@ export function createRadios({ choices.find((c) => toKey(c) === key) ?? initialValue; if (choices.length === 1) { - const span = window.document.createElement("span"); - span.textContent = toLabel(choices[0]); - field.append(span); + fieldset.append(createSpan(toLabel(choices[0]))); } else { - if (legend) { - const legendElement = window.document.createElement("legend"); - legendElement.textContent = legend; - field.append(legendElement); - } - - const fieldId = id ?? ""; + const groupId = id ?? ""; choices.forEach((choice) => { - const choiceKey = toKey(choice); - const choiceLabel = toLabel(choice); + const key = toKey(choice); const { label } = createLabeledInput({ - inputId: `${fieldId}-${choiceKey.toLowerCase()}`, - inputName: fieldId, - inputValue: choiceKey, - inputChecked: choiceKey === initialKey, + inputId: `${groupId}-${key.toLowerCase()}`, + inputName: groupId, + inputValue: key, + inputChecked: key === initialKey, title: toTitle?.(choice), type: "radio", }); - const text = window.document.createTextNode(choiceLabel); + const text = window.document.createTextNode(toLabel(choice)); label.append(text); - field.append(label); + fieldset.append(label); }); - field.addEventListener("change", (event) => { - if (!(event.target instanceof HTMLInputElement)) return; - onChange?.(fromKey(event.target.value)); - }); + fieldset.addEventListener("change", (event) => { + if (!(event.target instanceof HTMLInputElement)) return; + onChange?.(fromKey(event.target.value)); + }); } - return field; + return fieldset; } /** @@ -291,33 +289,30 @@ export function createSelect({ choices.find((c) => toKey(c) === key) ?? initialValue; if (choices.length === 1) { - const span = window.document.createElement("span"); - span.textContent = toLabel(choices[0]); return { - element: span, + element: createSpan(toLabel(choices[0])), get: () => initialValue, set: () => {}, }; } - const field = window.document.createElement("label"); + const element = window.document.createElement("label"); if (label) { - const span = window.document.createElement("span"); - span.textContent = label; - field.append(span); + element.append(createSpan(label)); } const select = window.document.createElement("select"); select.id = id ?? ""; select.name = id ?? ""; - field.append(select); + element.append(select); /** @param {T} choice */ const createOption = (choice) => { + const key = toKey(choice); const option = window.document.createElement("option"); - option.value = toKey(choice); + option.value = key; option.textContent = toLabel(choice); - if (toKey(choice) === initialKey) { + if (key === initialKey) { option.selected = true; } return option; @@ -342,13 +337,11 @@ export function createSelect({ if (remaining > 0) { const small = window.document.createElement("small"); small.textContent = `+${remaining}`; - field.append(small); - const arrow = window.document.createElement("span"); - arrow.textContent = "↓"; - field.append(arrow); + element.append(small); + element.append(createSpan("↓")); } - field.addEventListener("click", (e) => { + element.addEventListener("click", (e) => { if (e.target !== select && "showPicker" in select) { e.preventDefault(); select.showPicker(); @@ -356,7 +349,7 @@ export function createSelect({ }); return { - element: field, + element, get: () => fromKey(select.value), set: (choice) => { select.value = toKey(choice); diff --git a/website/styles/chart.css b/website/styles/chart.css index 4d3dc0eca..bfdbda598 100644 --- a/website/styles/chart.css +++ b/website/styles/chart.css @@ -16,134 +16,134 @@ background: none; } - legend { - position: absolute; - left: 0; - right: 0; - z-index: 20; - font-size: var(--font-size-xs); - line-height: var(--line-height-xs); - pointer-events: none; - - &::before, - &::after { - content: ""; - position: absolute; - top: 0; - bottom: 0; - width: var(--main-padding); - z-index: 1; - pointer-events: none; - } - - &::before { - left: 0; - background-image: linear-gradient( - to left, - transparent, - var(--background-color) - ); - } - - &::after { - right: 0; - background-image: linear-gradient( - to right, - transparent, - var(--background-color) - ); - } - - > div { - display: flex; - align-items: center; - overflow-x: auto; - padding: 0 var(--main-padding); - padding-top: 0.375rem; - - @media (pointer: coarse) { - pointer-events: auto; - } - - > * { - pointer-events: auto; - } - - > *:nth-child(2) { - color: var(--gray); - padding: 0 0.75rem; - } - } - - padding: 0; - top: 0; - text-transform: lowercase; - - select { - text-transform: lowercase; - } - - > div { - padding-bottom: 0.75rem; - - small { - flex-shrink: 0; - } - - > div:last-child { - display: flex; - align-items: center; - gap: 1rem; - flex-shrink: 0; - - > div { - flex: 0; - height: 100%; - display: flex; - align-items: center; - - > label { - > span { - display: flex; - } - - &:has(input:not(:checked)) { - > span.main > span.name { - text-decoration: line-through 1.5px var(--color); - } - - &:hover { - * { - color: var(--off-color); - } - - > span.main > span.name { - text-decoration-color: var(--orange); - } - } - - &:active { - color: var(--orange); - } - } - } - - > a { - padding-inline: 0.375rem; - margin-inline: -0.375rem; - margin-top: 0.1rem; - } - } - } - } - } - > div { min-height: 0; height: 100%; margin-right: var(--negative-main-padding); margin-left: var(--negative-main-padding); + & > legend, + & table > tr > td:not(:last-child) legend { + position: absolute; + left: 0; + right: 0; + z-index: 20; + font-size: var(--font-size-xs); + line-height: var(--line-height-xs); + pointer-events: none; + padding: 0; + top: 0; + text-transform: lowercase; + + &::before, + &::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + width: var(--main-padding); + z-index: 1; + pointer-events: none; + } + + &::before { + left: 0; + background-image: linear-gradient( + to left, + transparent, + var(--background-color) + ); + } + + &::after { + right: 0; + background-image: linear-gradient( + to right, + transparent, + var(--background-color) + ); + } + + select { + text-transform: lowercase; + } + + > div { + display: flex; + align-items: center; + overflow-x: auto; + padding-inline: var(--main-padding); + padding-block-start: 0.375rem; + + @media (pointer: coarse) { + pointer-events: auto; + } + + > * { + pointer-events: auto; + } + + > *:nth-child(2) { + color: var(--gray); + padding-inline: 0.75rem; + } + + small { + flex-shrink: 0; + } + + > div:last-child { + display: flex; + align-items: center; + gap: 1rem; + flex-shrink: 0; + + > div { + flex: 0; + height: 100%; + display: flex; + align-items: center; + + > label { + > span { + display: flex; + } + + &:has(input:not(:checked)) { + > span.main > span.name { + text-decoration: line-through 1.5px var(--color); + } + + &:hover { + * { + color: var(--off-color); + } + + > span.main > span.name { + text-decoration-color: var(--orange); + } + } + + &:active { + color: var(--orange); + } + } + } + + > a { + padding-inline: 0.375rem; + margin-inline: -0.375rem; + margin-top: 0.1rem; + } + } + } + } + } + + & table > tr > td:not(:last-child) legend > div { + padding-bottom: 0.75rem; + } + :is(fieldset:has(> label > input[type="radio"]), label:has(> select)) { display: flex; flex-shrink: 0; @@ -174,41 +174,6 @@ &:last-child > td { border-top: 1px; - - &:nth-child(2) { - position: relative; - - > fieldset:has(> label > input[type="radio"]) { - position: absolute; - left: 0; - top: 0; - bottom: 0; - z-index: 10; - display: flex; - align-items: center; - pointer-events: auto; - font-size: var(--font-size-xs); - line-height: var(--line-height-xs); - text-transform: uppercase; - background-color: var(--background-color); - padding-left: var(--main-padding); - padding-right: 0.25rem; - - &::after { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 100%; - width: var(--main-padding); - background-image: linear-gradient( - to right, - var(--background-color), - transparent - ); - } - } - } } } @@ -218,11 +183,14 @@ z-index: 50; display: inline-flex; font-size: var(--font-size-xs); + line-height: var(--line-height-xs); align-items: center; text-transform: uppercase; } - tr:not(:last-child) > td:last-child > fieldset:has(> label > input[type="radio"]) { + tr:not(:last-child) + > td:last-child + > fieldset:has(> label > input[type="radio"]) { top: 0; right: 0; gap: 0.375rem; @@ -304,10 +272,12 @@ } @keyframes chart-hint { - 0%, 100% { + 0%, + 100% { opacity: 0; } - 15%, 85% { + 15%, + 85% { opacity: 0.85; } } diff --git a/website/styles/components.css b/website/styles/components.css index 127f8e1b3..78cfb4372 100644 --- a/website/styles/components.css +++ b/website/styles/components.css @@ -38,33 +38,18 @@ } fieldset { - border: 0; display: flex; align-items: center; gap: 0.5rem; - min-inline-size: 0; - padding: 0; &:has(> label > input[type="radio"]) { - text-transform: lowercase; - display: flex; - align-items: center; gap: 1rem; - > legend, - > div { + > label { flex-shrink: 0; - } - - label { padding: 0.5rem; margin: -0.5rem; } - - > div { - display: flex; - gap: 1.5rem; - } } } diff --git a/website/styles/elements.css b/website/styles/elements.css index be091563c..55327ca78 100644 --- a/website/styles/elements.css +++ b/website/styles/elements.css @@ -105,6 +105,11 @@ button { user-select: none; } +fieldset { + min-inline-size: 0; + padding: 0; +} + h1 { font-size: 2rem; line-height: var(--line-height-xl);