Skip to content

Commit ea370b3

Browse files
committed
Enhance H5P build page with step markers and improved styles for better user feedback
1 parent 8a24be5 commit ea370b3

5 files changed

Lines changed: 230 additions & 29 deletions

File tree

‎docs/h5p-build-page.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The source is not uploaded: the package is built in the browser. A link can carr
1111
| [src/h5p/build/build-source.ts](../src/h5p/build/build-source.ts) | What it does with a source with no DOM: the link's fragment, read and written, the files a source names, the content types it is written in, the text for the chat |
1212
| [src/h5p/build/editor.ts](../src/h5p/build/editor.ts) | The source's editor, CodeMirror 6, mounted in a shadow root and loaded apart from the page: see [The source editor](#the-source-editor) |
1313
| [src/h5p/build/json-scan.ts](../src/h5p/build/json-scan.ts) | What it reads of JSON text with no DOM: where a text stops being JSON and what is wrong there, where a problem's path points, and the source laid out |
14-
| [src/assets/site.css](../src/assets/site.css) | Its styles, under *H5P build page* |
14+
| [src/assets/site.css](../src/assets/site.css) | Its styles, under *H5P build page*, in the site's own colours: the steps as a timeline, a marker for each on a rail to the next, which shows a check or a warning from what its step says of itself (`#source-state`, the files' `data-chosen`, `#status`), with no script of its own |
1515
| [vite.config.ts](../vite.config.ts) | The page's script, `/h5p/build/build-page.js`, with the player, its runtime, the pack and their licence files in `/h5p/assets/`, and the type files in `/h5p/build/types/` |
1616
| [e2e/](../e2e/), [playwright.config.ts](../playwright.config.ts) | The browser tests: a pasted source, one from a link and one with a file of its own build, play and download; the source's editor marks a mistake and the problems the build finds, lays a whole paste out, and a link follows the source as it changes; `json-scan.spec.ts` tests the reading of JSON with no browser; a source's problems are listed, and copied for the chat with the type files; `/h5p/bundles/` serves every bundle and part, and its tool and a bundle build a quiz in an empty folder; `/h5p/skill/` serves the skill's files as the package carries them, and writes out every address |
1717

‎e2e/build.spec.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ async function expectPlays(page: Page): Promise<void> {
7878
await expect(page.frameLocator('h5p-player iframe').first().locator('.h5p-content, .h5p-container').first()).toBeVisible()
7979
}
8080

81+
/** What a step's marker shows, as site.css draws it from what the step says of itself: its number, a check or a warning. */
82+
const marker = (page: Page, heading: string) => page.locator(`.step[aria-labelledby="${heading}"]`).evaluate((step) => {
83+
const style = getComputedStyle(step)
84+
const icon = style.getPropertyValue('--step-icon')
85+
if (!icon) return 'number'
86+
return icon === style.getPropertyValue('--icon-check') ? 'check' : icon === style.getPropertyValue('--icon-alert') ? 'warning' : icon
87+
})
88+
8189
test('a pasted source builds, plays and downloads', async ({ page, baseURL }) => {
8290
const { problems, elsewhere } = watch(page, baseURL)
8391
await page.goto('/h5p/build/')
@@ -343,9 +351,12 @@ test('a source that names a file waits for it, then builds with it', async ({ pa
343351
await expect(page.locator('#build-number')).toHaveText('3')
344352
await expect(page.locator('#file-list li')).toHaveAttribute('data-chosen', 'false')
345353
await expect(page.locator('#status')).toHaveText('The source names files: choose them, then Build.')
354+
expect(await marker(page, 'files-heading')).toBe('number')
346355

347356
await page.locator('#files').setInputFiles({ name: 'delta.png', mimeType: 'image/png', buffer: PNG })
348357
await expect(page.locator('#file-list li')).toHaveAttribute('data-chosen', 'true')
358+
// Every file it names is chosen: the step is done.
359+
expect(await marker(page, 'files-heading')).toBe('check')
349360
await page.locator('#build').click()
350361
await expectPlays(page)
351362
await expect(page.locator('#status')).toContainText('1 media file')
@@ -354,6 +365,28 @@ test('a source that names a file waits for it, then builds with it', async ({ pa
354365
expect(problems, problems.join('\n')).toEqual([])
355366
})
356367

368+
test('each step’s marker says how the step stands: its number, a check once it is done, a warning where it went wrong', async ({ page, baseURL }) => {
369+
const { problems } = watch(page, baseURL)
370+
await page.goto('/h5p/build/')
371+
expect(await marker(page, 'source-heading')).toBe('number')
372+
expect(await marker(page, 'build-heading')).toBe('number')
373+
// The source is done once it is JSON, and wrong while it is not.
374+
await fillSource(page, '{ "type": ')
375+
await expect.poll(() => marker(page, 'source-heading')).toBe('warning')
376+
const { title: _title, ...untitled } = RIVERS
377+
await fillSource(page, JSON.stringify(untitled))
378+
await expect.poll(() => marker(page, 'source-heading')).toBe('check')
379+
// A build with problems went wrong; one that plays is done, and the play is the step at hand.
380+
await page.locator('#build').click()
381+
await expect.poll(() => marker(page, 'build-heading')).toBe('warning')
382+
await fillSource(page, JSON.stringify(RIVERS))
383+
await page.locator('#build').click()
384+
await expectPlays(page)
385+
expect(await marker(page, 'build-heading')).toBe('check')
386+
expect(await marker(page, 'play-heading')).toBe('number')
387+
expect(problems, problems.join('\n')).toEqual([])
388+
})
389+
357390
// The source's editor: CodeMirror, loaded apart, in place of the textarea, which stays under it.
358391

359392
test('a mistake in the JSON is marked as it is typed, with its line and column, and goes when it is put right', async ({ page, baseURL }) => {

‎src/assets/site.css‎

Lines changed: 176 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -550,23 +550,121 @@ tbody th a:hover code { text-decoration: underline; }
550550
.skill-files a { overflow-wrap: anywhere; }
551551

552552
/* H5P build page, /h5p/build/ */
553-
.build { --ok: var(--live); --bad: var(--t-tag); }
553+
.build {
554+
--ok: var(--live);
555+
--bad: var(--t-tag);
556+
/* Masked, like the preview pane's icons, so they take a color like text does. */
557+
--icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7.5'/%3E%3C/svg%3E");
558+
--icon-alert: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round'%3E%3Cpath d='M12 5v8.5M12 18.5v.01'/%3E%3C/svg%3E");
559+
}
554560
.build-brand { margin-bottom: 1.25rem; font-size: 0.9375rem; }
555561
/* A longer heading than the docs' package names, so it gives way on a phone. */
556562
.build .docs-header h1 { font-size: clamp(1.75rem, 1.2rem + 3vw, 2.25rem); }
557-
.build .step h2 { display: flex; align-items: center; gap: 0.6rem; margin: 2.5rem 0 1rem; }
558-
.build h3 { margin: 1.5rem 0 0.25rem; font-size: 1rem; }
559-
.step-number {
560-
display: inline-grid;
563+
564+
/* The steps are a timeline: a marker for each, on a rail that runs on to the next. A step the page
565+
hides, as the files of a source that names none, is not on it, and the rail ends at the last shown. */
566+
.build .step {
567+
--marker: 2rem;
568+
--indent: calc(var(--marker) + 1rem);
569+
/* How the step stands, which the rules under these set from what the page says of it. */
570+
--step-color: var(--accent);
571+
--step-fill: var(--bg);
572+
--step-rail: var(--border);
573+
--step-marked: 0;
574+
position: relative;
575+
padding: 0 0 1.5rem var(--indent);
576+
}
577+
.build .docs-header + .step { margin-top: 2.5rem; }
578+
/* The source is JSON, every file it names is chosen, the build is ready: the step is done. */
579+
.build .step:is(
580+
:has(#source-state[data-state="ok"]),
581+
:has(.file-list > li):not(:has(.file-list > [data-chosen="false"])),
582+
:has(.status[data-state="ready"])
583+
) {
584+
--step-fill: var(--accent);
585+
--step-rail: var(--accent);
586+
--step-marked: 1;
587+
--step-icon: var(--icon-check);
588+
}
589+
/* The text is not JSON, or nothing was built. */
590+
.build .step:has(#source-state[data-state="error"], .status[data-state="error"]) {
591+
--step-color: var(--bad);
592+
--step-fill: var(--bad);
593+
--step-marked: 1;
594+
--step-icon: var(--icon-alert);
595+
}
596+
597+
/* The rail, from under a step's marker to the next one's. It is a border, which forced colors keep. */
598+
.build .step::before {
599+
content: "";
600+
position: absolute;
601+
top: calc(var(--marker) + 0.5rem);
602+
bottom: 0.5rem;
603+
left: calc(var(--marker) / 2 - 1px);
604+
border-left: 2px solid var(--step-rail);
605+
transform-origin: top;
606+
transition: border-color 0.2s, opacity 0.3s, scale 0.3s ease-out;
607+
}
608+
/* None from the last step shown: it is drawn down to a step as that one joins. */
609+
.build .step:not(:has(~ .step:not([hidden])))::before { opacity: 0; }
610+
/* A step that joins the others, the files or the play, comes in. */
611+
#files-step, #play-step { transition: opacity 0.3s ease-out, translate 0.3s ease-out; }
612+
@starting-style {
613+
#files-step, #play-step { opacity: 0; }
614+
}
615+
616+
.build .step h2 {
617+
display: flex;
618+
align-items: center;
619+
min-height: var(--marker);
620+
margin: 0 0 0.75rem;
621+
padding: 0;
622+
border: 0;
623+
font-size: 1.25rem;
624+
}
625+
.build h3 { margin: 1.5rem 0 0.5rem; font-size: 1rem; }
626+
.step-marker {
627+
position: absolute;
628+
top: 0;
629+
left: 0;
630+
display: grid;
561631
place-items: center;
562-
flex: none;
563-
width: 1.75rem;
564-
height: 1.75rem;
632+
width: var(--marker);
633+
height: var(--marker);
634+
border: 2px solid var(--step-color);
635+
border-radius: 50%;
636+
background: var(--step-fill);
637+
color: var(--step-color);
638+
font-size: 0.875rem;
639+
line-height: 1;
640+
font-variant-numeric: tabular-nums;
641+
transition: border-color 0.2s, background-color 0.2s, color 0.2s;
642+
}
643+
/* A check or a warning takes the number's place. The number stays in the heading's name. */
644+
.step-number { opacity: calc(1 - var(--step-marked)); transition: opacity 0.2s; }
645+
.step-marker::after {
646+
content: "";
647+
position: absolute;
648+
inset: 0;
649+
background: var(--bg);
650+
mask: var(--step-icon, none) center / 1.125rem no-repeat;
651+
opacity: var(--step-marked);
652+
transition: opacity 0.2s;
653+
}
654+
/* While it builds, and until what was built plays, a ring around the marker, which turns. */
655+
.step-marker::before {
656+
content: "";
657+
position: absolute;
658+
inset: -0.3125rem;
659+
border: 2px solid var(--tint);
660+
border-top-color: var(--accent);
565661
border-radius: 50%;
566-
background: var(--accent);
567-
color: var(--bg);
568-
font-size: 0.9375rem;
662+
opacity: 0;
663+
transition: opacity 0.2s;
569664
}
665+
.build .step:has(.status[data-state="building"]) .step-marker::before { opacity: 1; }
666+
@keyframes turn { to { rotate: 1turn; } }
667+
570668
.build label { display: block; margin-bottom: 0.35rem; font-weight: 600; }
571669
.build label.inline-label { display: inline; margin-right: 0.5rem; }
572670
.build textarea {
@@ -584,7 +682,7 @@ tbody th a:hover code { text-decoration: underline; }
584682
.build textarea:hover { border-color: var(--text); }
585683
/* The source's editor hides its textarea, which stays under it as the field the page reads. */
586684
.build textarea[hidden] { display: none; }
587-
.build .field { margin: 1rem 0 0.5rem; }
685+
.build .field { margin: 0 0 0.5rem; }
588686
.source-editor { display: block; }
589687
/* A line of its own, kept when it is empty, so that what is under it does not move as a mistake comes and goes:
590688
two lines where the column is narrow enough for a message to wrap. */
@@ -608,7 +706,10 @@ tbody th a:hover code { text-decoration: underline; }
608706
}
609707
.build input[type="file"]::file-selector-button:hover { background: var(--tint); }
610708
.build a.button { text-decoration: none; }
611-
.button-large { padding: 0.45rem 1rem; font-size: 0.9375rem; }
709+
.button-large { padding: 0.55rem 1.1rem; font-size: 0.9375rem; }
710+
/* The page's two main actions, Build and Download, filled where the site's other buttons are outlined. */
711+
.button-solid, .button-solid:hover { border-color: var(--accent); background: var(--accent); color: var(--bg); font-weight: 600; }
712+
.button-solid:hover:not(:disabled) { border-color: transparent; background: color-mix(in oklab, var(--accent) 85%, var(--text)); }
612713
.build .button:disabled { opacity: 0.6; cursor: progress; }
613714
.build .status {
614715
padding: 0.6rem 0.85rem;
@@ -620,11 +721,70 @@ tbody th a:hover code { text-decoration: underline; }
620721
.build .status[data-state="building"] { border-left-color: var(--focus); background: var(--tint); }
621722
.build .status[data-state="ready"] { border-left-color: var(--ok); }
622723
.build .status[data-state="error"] { border-left-color: var(--bad); }
623-
.build .messages, .build .file-list { padding-left: 1.2rem; }
624-
.build .problems li::marker { color: var(--bad); }
625-
.build .file-list li[data-chosen="true"]::marker { color: var(--ok); }
724+
/* The problems, the notes and the files, a row each. */
725+
.build .messages, .build .file-list {
726+
padding: 0;
727+
border: 1px solid var(--border);
728+
border-radius: var(--radius);
729+
background: var(--raised);
730+
list-style: none;
731+
}
732+
.build .messages li, .build .file-list li { margin: 0; padding: 0.55rem 0.85rem; overflow-wrap: anywhere; }
733+
.build .messages li + li, .build .file-list li + li { border-top: 1px solid var(--border); }
734+
/* A file's name, then whether it is chosen: at the row's end where both fit on a line, under the name where they do not. */
735+
.build .file-list li {
736+
position: relative;
737+
display: flex;
738+
flex-wrap: wrap;
739+
align-items: baseline;
740+
justify-content: space-between;
741+
gap: 0 0.6rem;
742+
padding-left: 2.45rem;
743+
white-space: nowrap;
744+
}
745+
.build .file-list code { white-space: normal; }
746+
/* Beside the name's first line: a ring to fill, then a check. */
747+
.build .file-list li::before {
748+
content: "";
749+
position: absolute;
750+
top: 0.85rem;
751+
left: 0.85rem;
752+
width: 1rem;
753+
height: 1rem;
754+
border: 2px dotted var(--control);
755+
border-radius: 50%;
756+
}
757+
.build .file-list li[data-chosen="true"]::before { border: 0; border-radius: 0; background: var(--accent); mask: var(--icon-check) center / contain no-repeat; }
626758
.build .file-list li[data-chosen="false"] { color: var(--muted); }
627759
.build .hint { color: var(--muted); font-size: 0.875rem; }
628760
/* H5P content draws on white, whatever the page's theme. */
629761
.player-box { margin: 1rem 0; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; background: #ffffff; }
630762
.player-box h5p-player { display: block; }
763+
764+
@media (max-width: 480px) {
765+
.build .step { --marker: 1.75rem; --indent: calc(var(--marker) + 0.75rem); }
766+
.step-marker { font-size: 0.8125rem; }
767+
/* The last step has no rail beside it, so on a phone what plays takes the column's whole width. */
768+
.player-box { margin-left: calc(-1 * var(--indent)); }
769+
}
770+
771+
/* A finger's target. */
772+
@media (pointer: coarse) {
773+
.button-large { min-height: 2.75rem; }
774+
.build input[type="file"]::file-selector-button { padding-block: 0.6rem; }
775+
}
776+
777+
/* What moves, only where motion is not asked to be reduced: the rail drawn down, a step coming up, the ring turning. */
778+
@media (prefers-reduced-motion: no-preference) {
779+
.build .step:not(:has(~ .step:not([hidden])))::before { scale: 1 0; }
780+
@starting-style {
781+
#files-step, #play-step { translate: 0 0.5rem; }
782+
}
783+
.build .step:has(.status[data-state="building"]) .step-marker::before { animation: turn 0.9s linear infinite; }
784+
}
785+
786+
@media (forced-colors: active) {
787+
/* Masked icons paint with their background, which forced colors would turn into Canvas. */
788+
.step-marker::after, .build .file-list li[data-chosen="true"]::before { forced-color-adjust: none; background: CanvasText; }
789+
.step-marker::before { border-color: GrayText; border-top-color: Highlight; }
790+
}

‎src/h5p/build/build-page.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,14 @@ player.addEventListener('error', (event) => {
276276
setStatus(`It was built, but does not play: ${message}`, 'error')
277277
})
278278

279-
/** Says where the source stops being JSON, if it does, as it is typed, and what it names. */
279+
/**
280+
* Says where the source stops being JSON, if it does, as it is typed. Its state is `error` then,
281+
* `ok` for text that is JSON and none for no text, which is what the step's marker is drawn from.
282+
*/
280283
function checkSource(): void {
281-
const problem = jsonProblem(sourceField.value)
282-
sourceState.dataset.state = problem ? 'error' : ''
284+
const text = sourceField.value
285+
const problem = jsonProblem(text)
286+
sourceState.dataset.state = problem ? 'error' : text.trim() ? 'ok' : ''
283287
if (!problem) {
284288
sourceState.replaceChildren()
285289
return

‎src/h5p/build/index.njk‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
2929
</p>
3030
</header>
3131

32+
{#- The steps are drawn as a timeline by site.css: a marker for each, on a rail that runs on to
33+
the next. It reads how a step stands from what the step says of itself, so the markers need no
34+
script: #source-state, the files in #file-list and #status, each by its data attribute. -#}
3235
<section class="step" aria-labelledby="source-heading">
33-
<h2 id="source-heading"><span class="step-number">1</span> Source</h2>
36+
<h2 id="source-heading"><span class="step-marker"><span class="step-number">1</span></span> Source</h2>
3437
<div class="field">
3538
<label for="source" id="source-label">The source, as JSON</label>
3639
<textarea id="source" rows="12" spellcheck="false" autocomplete="off" placeholder='{ "type": "H5P.Blanks", "title": "Rivers", "language": "en", "license": "CC BY", "authors": [{ "name": "Ann Author", "role": "Author" }], "params": { "questions": ["The *Danube* flows into the Black Sea."] } }'></textarea>
@@ -50,21 +53,22 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
5053
</section>
5154

5255
<section class="step" id="files-step" aria-labelledby="files-heading" hidden>
53-
<h2 id="files-heading"><span class="step-number">2</span> Files</h2>
56+
<h2 id="files-heading"><span class="step-marker"><span class="step-number">2</span></span> Files</h2>
5457
<p>The source names these files. Choose them from your computer; each is matched by its name.</p>
55-
<ul class="file-list" id="file-list"></ul>
58+
{#- The lists are drawn without bullets, and Safari then reads one as a list only if it is told. -#}
59+
<ul class="file-list" id="file-list" role="list"></ul>
5660
<p>
5761
<label for="files" class="inline-label">Choose files</label>
5862
<input type="file" id="files" multiple>
5963
</p>
6064
</section>
6165

6266
<section class="step" aria-labelledby="build-heading">
63-
<h2 id="build-heading"><span class="step-number" id="build-number">2</span> Build</h2>
64-
<p><button type="button" id="build" class="button button-primary button-large">Build</button></p>
67+
<h2 id="build-heading"><span class="step-marker"><span class="step-number" id="build-number">2</span></span> Build</h2>
68+
<p><button type="button" id="build" class="button button-solid button-large">Build</button></p>
6569
<p class="status" id="status" role="status" aria-live="polite"></p>
6670
<div id="problems-box" hidden>
67-
<ul class="messages problems" id="problems"></ul>
71+
<ul class="messages problems" id="problems" role="list"></ul>
6872
<p>
6973
<button type="button" id="copy-problems" class="button button-large">Copy the problems for the chat</button>
7074
<span class="hint" id="copy-result" role="status"></span>
@@ -74,17 +78,17 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
7478
</div>
7579
<div id="notes-box" hidden>
7680
<h3>Notes</h3>
77-
<ul class="messages" id="notes"></ul>
81+
<ul class="messages" id="notes" role="list"></ul>
7882
</div>
7983
</section>
8084

8185
<section class="step" id="play-step" aria-labelledby="play-heading" hidden>
82-
<h2 id="play-heading"><span class="step-number" id="play-number">3</span> Play and download</h2>
86+
<h2 id="play-heading"><span class="step-marker"><span class="step-number" id="play-number">3</span></span> Play and download</h2>
8387
<p>
8488
Work through all of it, wrong answers included, and check the answers and feedback against
8589
your material. The build checks the structure, not whether the content is right.
8690
</p>
87-
<p><a class="button button-primary button-large" id="download" download>Download the .h5p</a></p>
91+
<p><a class="button button-solid button-large" id="download" download>Download the .h5p</a></p>
8892
<div class="player-box">
8993
<h5p-player id="player"></h5p-player>
9094
</div>

0 commit comments

Comments
 (0)