heatmaps: part 7

This commit is contained in:
nym21
2026-05-31 12:05:48 +02:00
parent b2345db279
commit 5df399d2f7
4 changed files with 171 additions and 218 deletions
+35 -42
View File
@@ -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);
+130 -160
View File
@@ -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;
}
}
+1 -16
View File
@@ -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;
}
}
}
+5
View File
@@ -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);