autogenerating sidebar links

This commit is contained in:
Christoph Haas 2024-10-27 22:58:23 +01:00
parent 1546fa8199
commit 6f5cbd21aa
13 changed files with 522 additions and 34 deletions

79
src/components/Box.astro Normal file
View file

@ -0,0 +1,79 @@
---
import { getImage } from "astro:assets";
import HoustonGrad from "~/assets/houston_grad.png";
export interface Props {
icon?: "puzzle-piece" | "question-mark" | "check-list";
}
const { icon } = Astro.props;
---
<div
class={`highlight-box content ${icon}`}
style={icon === "question-mark" ? `--box-icon-url: url('nah');` : ""}
>
<section>
<slot />
</section>
</div>
<style>
:global(section > *) + .highlight-box {
margin: 4rem 0;
}
.highlight-box {
position: relative;
padding-block: 2rem;
padding-inline-start: 2rem;
padding-inline-end: 4rem;
border: 1px solid var(--sl-color-blue);
background-color: var(--sl-color-blue-low);
margin-top: 2.5rem !important;
}
.highlight-box.check-list {
border: 1px solid var(--sl-color-green);
background-color: var(--sl-color-green-low);
}
@supports (mask: var(--box-icon-url) no-repeat center bottom / 100%) or
(-webkit-mask: var(--box-icon-url) no-repeat center bottom / 100%) {
.highlight-box::before {
content: "";
position: absolute;
top: -2.5rem;
inset-inline-end: 1.5rem;
width: 8rem;
height: 8rem;
background: var(--box-icon-url) no-repeat center center / 100%;
}
}
:global(ol) {
padding-left: 1.5rem;
}
.highlight-box :global(details) {
--sl-details-border-color: var(--sl-color-purple);
--sl-details-border-color--hover: var(--sl-color-purple-high);
}
@media (max-width: 50em) {
.highlight-box::before {
width: 5rem;
height: 5rem;
}
}
</style>
<style is:global>
.highlight-box.question-mark .heading-wrapper:first-child {
margin-inline-end: 6rem !important;
}
@media (max-width: 50em) {
.highlight-box.question-mark .heading-wrapper:first-child {
margin-inline-end: 3rem !important;
}
}
</style>

View file

@ -0,0 +1,108 @@
<div class="checklist"><slot /></div>
{
Astro.slots.has('alternative') && (
<>
<p class="or">
foo
</p>
<div class="checklist alternative">
<slot name="alternative" />
</div>
</>
)
}
<style>
check-list > * + * {
margin-top: 1rem;
}
.checklist > :global(ul) {
list-style: none;
padding-inline-start: 0;
}
check-list :global(label) {
display: block;
position: relative;
margin: 0 -1rem !important;
border-radius: 0.5rem;
padding-block: 0.5rem;
padding-inline: 3.5rem 0.5rem;
cursor: pointer;
font-size: var(--sl-text-lg);
color: var(--sl-color-green-high);
}
check-list :global(label span) {
display: inline-block;
}
check-list :global(input[type='checkbox']) {
-webkit-appearance: none;
appearance: none;
}
check-list :global(input[type='checkbox']:focus) {
outline: none;
}
check-list :global(input[type='checkbox']::after) {
content: '';
position: absolute;
inset-inline-start: 1rem;
top: 50%;
transform: translateY(-50%);
transform-origin: center;
text-align: center;
line-height: 1.25;
width: 1.25em;
height: 1.25em;
font-size: 1.25em;
border: 1px solid var(--sl-color-green-high);
border-radius: 0.25rem;
color: var(--sl-color-green);
cursor: pointer;
}
check-list :global(input[type='checkbox']:focus::after) {
outline: 3px solid var(--sl-color-green);
outline-offset: 0.5rem;
}
check-list :global(input[type='checkbox']:not(:focus-visible)::after) {
outline: none;
}
check-list :global(label:has(> input[type='checkbox']:checked)) {
color: var(--sl-color-white);
}
check-list :global(input[type='checkbox']:checked::after) {
content: url("data:image/svg+xml,%3Csvg width='28' height='28' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='24' height='24' rx='4' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M20.0946 6.02558C20.6992 6.592 20.7301 7.54124 20.1636 8.14578L10.3868 18.5806C9.55548 19.468 8.14726 19.468 7.3159 18.5806L4.02558 15.0689C3.45917 14.4643 3.49008 13.5151 4.09461 12.9487C4.69915 12.3822 5.6484 12.4132 6.21481 13.0177L8.85137 15.8317L17.9744 6.09461C18.5408 5.49007 19.4901 5.45917 20.0946 6.02558Z' fill='%232F4D22'/%3E%3C/svg%3E%0A");
border: none;
/* color: var(--sl-color-green-low); */
/* background-color: var(--sl-color-gray-1); */
}
.or {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1em;
text-transform: uppercase;
font-weight: bold;
color: var(--theme-text-lighter);
}
.or::before,
.or::after {
--divider-width: 4px;
content: '';
background-image: linear-gradient(
to bottom,
transparent calc(50% - var(--divider-width) / 2),
var(--theme-divider) calc(50% - var(--divider-width) / 2) calc(50% + var(--divider-width) / 2),
transparent calc(50% + var(--divider-width) / 2) 100%
);
}
</style>

View file

@ -6,7 +6,7 @@ import Default from "@astrojs/starlight/components/Footer.astro";
<footer>
<Default {...Astro.props}><slot /></Default>
<div class="myfooter">
Copyright © 2003-2024 Christoph Haas. All content on workaround.org can be used under the terms of the
This guide is maintained by Christoph Haas since 2003. All content on workaround.org can be used under the terms of the
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank">Creative Commons BY-NC-SA license</a>. Penguin images
based on AI art because I suck at drawing.
</div>

View file

@ -0,0 +1,167 @@
<multiple-choice data-correct-label={"Correct"} data-incorrect-label={"Wrong"}>
<form onsubmit="event.preventDefault()">
<ol class="opt-list not-content"><slot /></ol>
<div class="footer">
<button class="submit" type="submit" disabled>
{"Submit"}
</button>
<div class="answer sr-only" role="alert"></div>
</div>
</form>
</multiple-choice>
<style>
multiple-choice {
display: block;
padding-bottom: 1rem;
color: var(--sl-color-text-accent);
}
form > * + * {
margin-top: 1rem;
}
.opt-list {
list-style: none;
padding-inline-start: 0;
}
/* .footer {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
} */
.submit,
.answer {
padding: 0.8rem;
line-height: 1.4;
max-width: 15em;
border: 2px solid var(--sl-color-gray-4);
}
.submit {
cursor: pointer;
color: var(--sl-color-accent);
background-color: white;
}
.submit:focus {
background-color: hsl(224, 20%, 94%);
}
.submit:not(:focus-visible) {
outline: none;
}
.submit:hover {
background: var(--sl-color-text-accent);
color: var(--sl-color-black);
/* border-color: transparent; */
}
.submit:disabled,
.submit:active {
/* transform: translateY(0.25rem); */
}
.submit:disabled {
background-color: hsl(224, 20%, 94%);
color: hsl(224, 7%, 36%);
opacity: 0.65;
cursor: not-allowed;
}
.answer {
text-align: center;
border-color: transparent;
}
.correct {
color: var(--sl-color-green-low);
background-color: var(--sl-color-green);
}
.wrong {
color: var(--sl-color-red-low);
background-color: var(--sl-color-red);
}
</style>
<script>
class MultipleChoice extends HTMLElement {
#defaultCorrectLabel: string;
#defaultIncorrectLabel: string;
#key: string;
#submitEl: HTMLButtonElement;
#answerEl: HTMLParagraphElement;
constructor() {
super();
this.#defaultCorrectLabel = this.dataset.correctLabel!;
this.#defaultIncorrectLabel = this.dataset.incorrectLabel!;
this.#key = Math.random().toString();
this.#submitEl = this.querySelector(".submit")!;
this.#answerEl = this.querySelector(".answer")!;
this.querySelectorAll(".opt-list > li").forEach((li) => this.#upgradeListItem(li));
}
#upgradeListItem(li: Element) {
const option = li.querySelector('input[type="radio"]') as HTMLInputElement | null;
if (!option) return;
option.removeAttribute("disabled");
option.setAttribute("name", this.#key);
option.addEventListener("change", () => {
this.#clearAnswer();
this.#enableSubmit();
});
if (option.checked) this.#enableSubmit();
}
/** Clear the answer text and hide its container visually. */
#clearAnswer() {
this.#answerEl.innerText = "";
this.#answerEl.classList.remove("wrong", "correct");
this.#answerEl.classList.add("sr-only");
}
/** Show the answer result to the user. */
#setAnswer(isCorrect: boolean) {
const answerTemplate = this.querySelector("input:checked ~ template") as HTMLTemplateElement | null;
if (answerTemplate) {
this.#answerEl.replaceChildren(answerTemplate.content.cloneNode(true));
} else {
this.#answerEl.innerText = isCorrect ? this.#defaultCorrectLabel : this.#defaultIncorrectLabel;
}
this.#answerEl.classList.remove("sr-only", "wrong", "correct");
this.#answerEl.classList.add(isCorrect ? "correct" : "wrong");
}
/** Activate the submit button, preparing it to evaluate the form when clicked. */
#enableSubmit() {
this.#submitEl.removeAttribute("disabled");
this.#submitEl.classList.remove("sr-only");
this.#submitEl.onclick = () => this.#submitAnswer();
}
/** Disable the submit button and hide it visually. */
#disableSubmit() {
this.#submitEl.setAttribute("disabled", "");
this.#submitEl.classList.add("sr-only");
this.#submitEl.onclick = null;
}
/** Check if the selected option is correct and display the result. */
#submitAnswer() {
const selection = this.querySelector("input:checked") as HTMLInputElement | null;
if (!selection) return;
this.#disableSubmit();
const isCorrect = selection.dataset.isCorrect !== undefined && ["", "true"].includes(selection.dataset.isCorrect);
this.#setAnswer(isCorrect);
}
}
customElements.define("multiple-choice", MultipleChoice);
</script>

View file

@ -0,0 +1,87 @@
---
export interface Props {
isCorrect?: boolean;
}
const { isCorrect } = Astro.props as Props;
---
<li>
<label>
<input type="radio" disabled data-is-correct={isCorrect} /><span><slot /></span>
{
Astro.slots.has('feedback') && (
<template>
<slot name="feedback" />
</template>
)
}
</label>
</li>
<style>
label {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem;
align-items: center;
border-radius: 1rem;
margin-inline: -0.5rem;
padding: 0.5rem;
cursor: pointer;
}
label:hover {
background-color: var(--sl-color-accent-low);
}
input[type='radio'] {
appearance: none;
cursor: pointer;
}
input[type='radio'] ~ * {
color: var(--sl-color-text-accent);
}
input[type='radio']:checked ~ * {
color: var(--sl-color-white);
}
input[type='radio']:focus::after {
outline: 3px solid var(--sl-color-text-accent);
outline-offset: 0.5rem;
}
input[type='radio']:focus,
input[type='radio']:not(:focus-visible)::after {
outline: none;
}
input[type='radio']::after {
display: block;
content: '';
text-align: center;
line-height: 1.5;
width: 1em;
height: 1em;
font-size: 1em;
font-weight: 900;
border: 2px solid var(--sl-color-accent-high);
border-radius: 100%;
color: var(--sl-color-white);
box-shadow: -3px 3px var(--theme-shade-subtle);
}
input[type='radio']:checked::after {
color: hsl(var(--color-base-white), 100%);
background-image: radial-gradient(var(--sl-color-white) 50%, transparent 0%, transparent);
border-color: var(--sl-color-white);
}
@media (forced-colors: active) {
input[type='radio']:checked::after {
background-color: Highlight;
}
}
</style>

View file

@ -0,0 +1,10 @@
---
import type { UIDictionaryKeys } from '../i18n/translation-checkers';
import { useTranslations } from '../i18n/util';
export interface Props {
key: UIDictionaryKeys;
}
---
{useTranslations(Astro)(Astro.props.key)}