Skip to content

Commit 86701b7

Browse files
committed
Add guide link to H5P activity, enhance skill.js for content types, and improve PDF viewer layout
- Added a guide link for building H5P activities with AI in site.js. - Updated skill.js to enhance content type handling and improve error messages. - Refactored pdfjs-viewer.njk to streamline the layout and footer integration. - Created a new topbar.njk for consistent navigation across pages. - Introduced a new guide page for creating H5P content with AI, detailing prompts and types. - Improved site.css for better responsiveness and layout adjustments. - Enhanced accessibility features in site.js for screen reader announcements.
1 parent 6522f3c commit 86701b7

12 files changed

Lines changed: 394 additions & 65 deletions

File tree

‎src/_data/site.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default {
4848
name: "Build an H5P activity with AI",
4949
description: "Describe a lesson to an AI chat and get an H5P activity, from a quiz to a branching scenario: play it, and download a .h5p file for Moodle or any other H5P site. Free, and nothing is uploaded.",
5050
page: "/h5p/build/",
51+
guide: "/h5p/create-with-ai/",
5152
repo: "https://github.com/missing-elements/missing-elements.github.io/tree/main/src/h5p/build",
5253
},
5354
],

‎src/_data/skill.js‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,33 @@ const references = readdirSync(`${author}/references`, { recursive: true, withFi
1515
.sort();
1616
const types = references.filter((path) => path.startsWith("references/types/"));
1717

18-
// The content types SKILL.md's table offers the user to choose from (its step 1), which the build
19-
// page counts and links to.
20-
const offered = [...readFileSync(`${author}/SKILL.md`, "utf8").matchAll(/^\| (H5P\.\w+) \|/gm)].map(([, type]) => type);
21-
if (!offered.length) throw new Error(`@missing-elements/h5p-author ${version}: SKILL.md has no table of content types`);
18+
// The rows of SKILL.md's table of the content types it offers the user to choose from (its step 1):
19+
// a type, and what it is for, with the media it plays in brackets where it needs the user's.
20+
const rows = [...readFileSync(`${author}/SKILL.md`, "utf8").matchAll(/^\| (H5P\.\w+) \| (.+?) \|$/gm)];
21+
if (!rows.length) throw new Error(`@missing-elements/h5p-author ${version}: SKILL.md has no table of content types`);
22+
const sentence = (text) => text[0].toUpperCase() + text.slice(1);
2223

2324
// What a build where npm cannot be reached fetches (SKILL.md's step 3), which vite.config.ts
2425
// writes to /h5p/bundles/ after this page: the command line, and a bundle per content type, named
2526
// by the author package's own list of them, the one its `writeBundles` writes from.
2627
const pack = await openPack();
27-
let bundles;
28+
let bundles, titles;
2829
try {
29-
bundles = (await contentTypes(pack)).map((part) => `${part.library.machineName}.zip`);
30+
const parts = await contentTypes(pack);
31+
bundles = parts.map((part) => `${part.library.machineName}.zip`);
32+
// The name H5P's editor gives each type, such as "Fill in the Blanks" for H5P.Blanks.
33+
titles = new Map(parts.map(({ library }) => [library.machineName, library.title]));
3034
} finally {
3135
await pack.close();
3236
}
3337

38+
// The types the skill offers, which the build page counts and the guide lists: each by its name in
39+
// the pack, what it is for, and the media the user gives it, where it needs any ("pictures").
40+
const offered = rows.map(([, type, text]) => {
41+
const [, use, media] = text.match(/^(.+?)(?: \((?:needs )?(.+)\))?$/);
42+
return { type, title: titles.get(type) ?? type, use: sentence(use), media };
43+
});
44+
3445
export default {
3546
version,
3647
// What SKILL.md names and the type files, apart, as the page lists them; `files` is every one.

‎src/_includes/pdfjs-viewer.njk‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,16 @@ bodyClass: docs-page
1010
{%- endfor %}
1111
</ol>
1212
{%- endmacro -%}
13+
{% include "topbar.njk" %}
14+
1315
<div class="docs">
1416
<div class="toc">
15-
<nav aria-label="Site"><a class="brand toc-home" href="/"><img src="/assets/logo.svg" alt="" width="24" height="24">{{ site.name }}</a></nav>
1617
<nav aria-label="On this page">{{ tocList(toc) }}</nav>
1718
</div>
1819

1920
<main class="content" id="main">
2021
<header class="docs-header">
21-
<a class="brand docs-brand" href="/"><img src="/assets/logo.svg" alt="" width="24" height="24">{{ site.name }}</a>
22-
<div class="docs-header-row">
23-
<h1>pdfjs-viewer</h1>
24-
{% include "theme-switch.njk" %}
25-
</div>
22+
<h1>pdfjs-viewer</h1>
2623
<p class="lead">A self-hostable PDF viewer web component, based on the <a href="https://mozilla.github.io/pdf.js/web/viewer.html">PDF.js default viewer</a>.</p>
2724
<ul class="meta">
2825
<li><a href="{{ pdfjsViewer.npm }}">npm</a></li>
@@ -36,10 +33,6 @@ bodyClass: docs-page
3633
</header>
3734

3835
{{ content | safe }}
39-
40-
<footer class="docs-footer">
41-
<p>Found a mistake? <a href="https://github.com/missing-elements/missing-elements.github.io/blob/main/{{ page.inputPath | replace('./', '') }}">Edit this page</a> or <a href="{{ pdfjsViewer.repo }}/issues">open an issue</a>.</p>
42-
</footer>
4336
</main>
4437

4538
<div class="preview" id="preview" role="complementary" aria-label="Live preview" data-preview>
@@ -56,4 +49,10 @@ bodyClass: docs-page
5649
</div>
5750
<button type="button" class="button preview-open" data-preview-open aria-controls="preview" aria-expanded="false" hidden>Preview</button>
5851
</div>
52+
53+
<footer class="site-foot">
54+
<div class="wrap">
55+
<p>Found a mistake? <a href="https://github.com/missing-elements/missing-elements.github.io/blob/main/{{ page.inputPath | replace('./', '') }}">Edit this page</a> or <a href="{{ pdfjsViewer.repo }}/issues">open an issue</a>.</p>
56+
</div>
57+
</footer>
5958
<script type="module" src="/pdfjs-viewer/docs.js"></script>

‎src/_includes/theme-switch.njk‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{#- The theme switch, which site.js wires up. Indented for where pdfjs-viewer.njk includes it, so that page is as it was. -#}
1+
{#- The theme switch, which site.js wires up, at the end of every page's top bar (topbar.njk). -#}
22
<label class="theme-switch" data-theme-toggle hidden>
33
<input type="checkbox" role="switch" aria-label="Dark theme">
44
<span class="theme-switch-knob" aria-hidden="true"></span>

‎src/_includes/topbar.njk‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{#- The bar at the top of every page, as the build page's first was: the site's name, which goes
2+
home, then links to the page's own sections where it lists them (`topbarLinks` in its front
3+
matter, each `{ href, text }`, shown only to the visitor `topbarLinksClass` names where it names
4+
one), and the theme switch. -#}
5+
<header class="topbar">
6+
<div class="wrap topbar-row">
7+
<a class="brand" href="/"{% if page.url == "/" %} aria-current="page"{% endif %}><img src="/assets/logo.svg" alt="" width="24" height="24">{{ site.name }}</a>
8+
<div class="topbar-end">
9+
{%- if topbarLinks %}
10+
<nav class="topbar-links{% if topbarLinksClass %} {{ topbarLinksClass }}{% endif %}" aria-label="On this page">
11+
{%- for link in topbarLinks %}
12+
<a href="{{ link.href }}">{{ link.text }}</a>
13+
{%- endfor %}
14+
</nav>
15+
{%- endif %}
16+
{% include "theme-switch.njk" %}
17+
</div>
18+
</div>
19+
</header>

‎src/assets/site.css‎

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,6 @@ tbody th a:hover code { text-decoration: underline; }
343343
.brand { display: inline-flex; align-items: center; gap: 0.5rem; color: var(--text); font-weight: 600; text-decoration: none; }
344344
.brand img { width: 1.5rem; height: 1.5rem; }
345345
.brand:hover { text-decoration: underline; }
346-
.toc-home { margin-bottom: 1.25rem; }
347-
.docs-brand { margin-bottom: 1.25rem; font-size: 0.9375rem; }
348346
.toc ol, .toc-inline ol { margin: 0; padding: 0; list-style: none; }
349347
.toc li + li, .toc-inline li + li { margin-top: 0; }
350348
.toc ol a, .toc-inline a {
@@ -368,7 +366,6 @@ tbody th a:hover code { text-decoration: underline; }
368366

369367
.docs-header { margin-bottom: 2rem; }
370368
.docs-header h1 { margin: 0; font-size: 2.25rem; letter-spacing: -0.01em; }
371-
.docs-header-row { display: flex; align-items: center; gap: 1rem; justify-content: space-between; }
372369
.lead { margin: 0.5rem 0 0.75rem; font-size: 1.125rem; color: var(--muted); }
373370
.meta { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; margin: 0 0 1rem; padding: 0; list-style: none; font-size: 0.875rem; color: var(--muted); }
374371
.meta li + li { margin-top: 0; }
@@ -377,7 +374,6 @@ tbody th a:hover code { text-decoration: underline; }
377374
.toc-inline summary { padding: 0.5rem 0.75rem; cursor: pointer; font-weight: 600; }
378375
.toc-inline ol { padding: 0 0.5rem 0.5rem; }
379376

380-
.docs-footer { margin-top: 4rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.875rem; color: var(--muted); }
381377

382378
/* Theme switch: the knob sits on the sun when light and on the moon when dark. */
383379
.theme-switch {
@@ -485,7 +481,20 @@ tbody th a:hover code { text-decoration: underline; }
485481
@media (min-width: 1280px) {
486482
.docs { grid-template-columns: 15rem minmax(0, 1fr) var(--preview-width); }
487483
.toc { display: block; }
488-
.toc-inline, .docs-brand { display: none; }
484+
.toc-inline { display: none; }
485+
}
486+
487+
/* The docs' three columns take the window's width, so its top bar and footer do too, their
488+
contents at the gutter, and at the contents' edge where they are beside it. Where the panes stay
489+
in view beside the text, the bar stays above them, and they fill the window under it. */
490+
.docs-page .wrap { inline-size: auto; margin-inline: var(--gutter); }
491+
@media (min-width: 1000px) {
492+
:root:has(.docs-page) { scroll-padding-top: var(--topbar-size); }
493+
.docs-page .topbar { position: sticky; top: 0; z-index: 30; background: var(--bg); }
494+
.docs-page :is(.toc, .preview) { top: var(--topbar-size); height: calc(100dvh - var(--topbar-size)); }
495+
}
496+
@media (min-width: 1280px) {
497+
.docs-page .wrap { margin-inline: 1.5rem; }
489498
}
490499

491500
/* Below 1000px the preview becomes a full-screen panel opened from a floating button. */
@@ -537,8 +546,11 @@ tbody th a:hover code { text-decoration: underline; }
537546
.flash { animation: none; }
538547
}
539548

549+
/* The pages of one column, the org home, the skill page and the guide: in the build page's
550+
container, under its top bar. */
551+
:is(.home, .skill, .guide) { padding-block: clamp(2.5rem, 1.5rem + 4vw, 4.5rem) clamp(3.5rem, 2rem + 5vw, 6rem); }
552+
540553
/* Org home */
541-
.home { max-width: var(--page-width); margin: 0 auto; padding: 4rem clamp(1rem, 4vw, 3rem); }
542554
.home h1 { margin: 0 0 0.5rem; font-size: 2rem; }
543555
.brand-title { display: flex; align-items: center; gap: 0.75rem; }
544556
.brand-title img { width: 3rem; height: 3rem; }
@@ -563,25 +575,46 @@ tbody th a:hover code { text-decoration: underline; }
563575
.cards p { margin: 0 0 0.5rem; color: var(--muted); }
564576
.cards .links { display: flex; gap: 1rem; margin-top: auto; font-size: 0.875rem; }
565577
/* The home page's footer: where else the elements are, in columns of links. */
566-
.site-footer { margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--border); font-size: 0.875rem; }
567578
.footer-links { display: flex; flex-wrap: wrap; gap: 1.5rem 4rem; }
568-
.footer-links h2 { margin: 0 0 0.5rem; padding: 0; border: 0; font-size: 0.875rem; }
579+
.footer-links h2 { margin: 0 0 0.5rem; padding: 0; border: 0; color: var(--text); font-size: 0.875rem; }
569580
.footer-links ul { margin: 0; padding: 0; list-style: none; }
570581
.footer-links li + li { margin-top: 0.35rem; }
571582
.footer-links a { color: var(--muted); text-decoration: none; }
572583
.footer-links a:hover { color: var(--text); text-decoration: underline; }
573584

574585
/* The skill page: one wide column, its prose at --measure, and its sections as steps. */
575-
.skill { max-width: var(--page-width); }
576-
.build-brand { margin-bottom: 1.25rem; font-size: 0.9375rem; }
586+
.skill > * { max-inline-size: var(--page-width); }
577587
/* A longer heading than the docs' package names, so it gives way on a phone. */
578-
.skill .docs-header h1 { font-size: clamp(1.75rem, 1.2rem + 3vw, 2.25rem); }
588+
:is(.skill, .guide) .docs-header h1 { font-size: clamp(1.75rem, 1.2rem + 3vw, 2.25rem); }
579589
/* Prose keeps a line a reader can follow; the source, the lists and what plays take the whole width. */
580-
:is(.build, .skill) p:not(.status, .sample-note) { max-width: var(--measure); }
590+
:is(.build, .skill, .guide) p:not(.status, .sample-note) { max-width: var(--measure); }
581591

582592
/* The skill page's note for a person who opened it, no wider than its prose and the note's padding. */
583593
.for-person { max-width: calc(var(--measure) + 2rem); }
584594

595+
/* The guide, /h5p/create-with-ai/: an article on the container's left edge, as the build page's
596+
steps are, its steps on the skill page's timeline, its prompts in code blocks to copy, and the
597+
types it lists by name and use. */
598+
.guide > * { max-inline-size: 48rem; }
599+
.guide a.button { text-decoration: none; }
600+
.guide h3 + .code { margin-top: 0.75rem; }
601+
/* A row each, the name beside its use where the list is wide enough for both, over it where not. */
602+
.type-list {
603+
margin: 0 0 1.5rem;
604+
border: 1px solid var(--border);
605+
border-radius: var(--radius);
606+
background: var(--raised);
607+
container-type: inline-size;
608+
}
609+
.type-list > div { padding: 0.6rem 0.85rem; }
610+
.type-list > div + div { border-top: 1px solid var(--border); }
611+
.type-list dt { font-weight: 600; }
612+
.type-list dd { margin: 0; color: var(--muted); }
613+
.type-media { color: var(--text); }
614+
@container (width >= 30rem) {
615+
.type-list > div { display: grid; grid-template-columns: 11rem minmax(0, 1fr); gap: 1rem; }
616+
}
617+
585618
/* The build page, /h5p/build/: a landing page for a visitor who came with no source, and the build
586619
at once for one a chat sent with the source in the link. What is said to one alone is not shown
587620
to the other; the class is on <html> before the page paints (index.njk). */
@@ -592,6 +625,8 @@ tbody th a:hover code { text-decoration: underline; }
592625
--wide: 85rem;
593626
--gutter: clamp(1rem, 4vw, 3rem);
594627
--band-space: clamp(3.5rem, 2rem + 6vw, 7rem);
628+
/* The top bar's height, a row and its rule, for what sits under it where it stays (the docs). */
629+
--topbar-size: calc(4rem + 1px);
595630
}
596631
.wrap { inline-size: min(100% - 2 * var(--gutter), var(--wide)); margin-inline: auto; }
597632
.band { padding-block: var(--band-space); }

‎src/assets/site.js‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
// Screen reader announcements through a polite status region.
1+
// Screen reader announcements through a polite status region. VoiceOver skips a message the same as
2+
// the last, even with the region emptied between, so a repeat ends in a no-break space, not read.
3+
let lastAnnounced = ''
24
export function announce(message, region = document.querySelector('[data-status="page"]')) {
5+
if (message === lastAnnounced) message += ' '
6+
lastAnnounced = message
37
region.textContent = ''
48
// Cleared first so that repeating the same message is announced again.
59
setTimeout(() => (region.textContent = message), 50)
@@ -80,17 +84,27 @@ for (const pre of document.querySelectorAll('pre[class*="language-"]')) {
8084
const copy = document.createElement('button')
8185
copy.type = 'button'
8286
copy.className = 'button'
83-
copy.textContent = 'Copy'
87+
// What it says, and for a screen reader what it copies, where the block says (data-copy-name):
88+
// a page of several bare Copy buttons cannot be told apart by ear.
89+
const state = document.createElement('span')
90+
state.textContent = 'Copy'
91+
copy.append(state)
92+
if (pre.dataset.copyName) {
93+
const name = document.createElement('span')
94+
name.className = 'visually-hidden'
95+
name.textContent = ` ${pre.dataset.copyName}`
96+
copy.append(name)
97+
}
8498
copy.addEventListener('click', async () => {
8599
try {
86100
await navigator.clipboard.writeText(pre.textContent)
87-
copy.textContent = 'Copied'
101+
state.textContent = 'Copied'
88102
announce('Copied to clipboard')
89103
} catch {
90-
copy.textContent = 'Copy failed'
104+
state.textContent = 'Copy failed'
91105
announce('Copy failed')
92106
}
93-
setTimeout(() => (copy.textContent = 'Copy'), 1500)
107+
setTimeout(() => (state.textContent = 'Copy'), 1500)
94108
})
95109
bar.append(copy)
96110
}

‎src/h5p/build/index.njk‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ socialImage:
1212
width: 1200
1313
height: 630
1414
alt: "The build page: its headline, Build an H5P activity with AI, and a course presentation playing beside it."
15+
# The top bar's links to the page's own sections (topbar.njk), for a visitor who came with no source.
16+
topbarLinks:
17+
- { href: "#make", text: "Make your own" }
18+
- { href: "#build-it", text: "Build" }
19+
- { href: "#cost", text: "Build cost" }
20+
topbarLinksClass: when-unlinked
1521
# The page's only policy: GitHub Pages sends no headers. base.njk writes it first in <head>, and the
1622
# page's inline scripts, the site's color-scheme script and the one below, are added to script-src
1723
# by their hashes at build (eleventy.config.js).
@@ -26,19 +32,7 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
2632
when-unlinked, and site.css shows it by the class this line puts on <html> before the page
2733
paints; build-page.ts keeps the class in step with the address from then on. -#}
2834
<script>if (new URLSearchParams(location.hash.slice(1)).has("source")) document.documentElement.classList.add("linked")</script>
29-
<header class="topbar">
30-
<div class="wrap topbar-row">
31-
<a class="brand" href="/"><img src="/assets/logo.svg" alt="" width="24" height="24">{{ site.name }}</a>
32-
<div class="topbar-end">
33-
<nav class="topbar-links when-unlinked" aria-label="On this page">
34-
<a href="#make">Make your own</a>
35-
<a href="#build-it">Build</a>
36-
<a href="#cost">Build cost</a>
37-
</nav>
38-
{% include "theme-switch.njk" %}
39-
</div>
40-
</div>
41-
</header>
35+
{% include "topbar.njk" %}
4236

4337
{#- The markup build-page.ts wires up, built into /h5p/build/build-page.js by Vite (vite.config.ts). -#}
4438
<main class="build" id="main">
@@ -47,11 +41,11 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
4741
<div class="hero-words">
4842
<h1 id="page-title">Build an H5P activity with AI</h1>
4943
{#- The types are those the skill's table offers to choose from, in the release the page runs:
50-
counted from it (src/_data/skill.js), and linked at its tag. -#}
44+
counted from it (src/_data/skill.js), and listed by their names in the guide. -#}
5145
<p class="lead when-unlinked">
5246
Describe a lesson to an AI chat, and get an interactive activity for your students: a
5347
quiz, flash cards, a branching scenario, an interactive book, or any of
54-
<a href="https://github.com/missing-elements/h5p-offline-player/blob/author-v{{ skill.version }}/skills/h5p-author/SKILL.md#1-choose-the-content-type-then-the-author-and-licence-with-the-user">{{ skill.offered.length }} H5P types</a>.
48+
<a href="/h5p/create-with-ai/#types">{{ skill.offered.length }} H5P types</a>.
5549
</p>
5650
<div class="hero-actions when-unlinked">
5751
<a class="button button-solid button-large" href="#make">Make your own</a>
@@ -96,7 +90,7 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
9690
<li class="feature">
9791
<span class="feature-icon icon-browser" aria-hidden="true"></span>
9892
<h3>Built in your browser</h3>
99-
<p>The page builds and plays the activity on your computer, free and with no account. Nothing you make is sent to this site.</p>
93+
<p>The page builds and plays the activity on your computer. It is free and <a href="https://github.com/missing-elements/missing-elements.github.io/tree/main/src/h5p/build">open source</a>, needs no account, and sends nothing you make to this site.</p>
10094
</li>
10195
<li class="feature">
10296
<span class="feature-icon icon-school" aria-hidden="true"></span>
@@ -142,6 +136,10 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
142136
The address in the prompt is <a href="/h5p/skill/">the h5p-author skill's page</a>: the
143137
instructions that tell a chat how to write an H5P activity, as text anyone can read.
144138
</p>
139+
<p>
140+
More prompts, every type, and what to do when something goes wrong:
141+
<a href="/h5p/create-with-ai/">how to create H5P content with AI, step by step</a>.
142+
</p>
145143
</div>
146144
</div>
147145
</section>

0 commit comments

Comments
 (0)