diff --git a/challenges/birdsbirdbirds/challenge.json b/challenges/birdsbirdbirds/challenge.json index 303414a..2d4703d 100644 --- a/challenges/birdsbirdbirds/challenge.json +++ b/challenges/birdsbirdbirds/challenge.json @@ -19,7 +19,7 @@ { "id": "draw-bird", "title": "Draw your bird", - "hint": "Draw a bird on the page and press Save.", + "hint": "Draw a bird on the page and press Save my bird.", "done_when": "The file bird.png exists in the workspace." }, { diff --git a/challenges/birdsbirdbirds/page/app.mjs b/challenges/birdsbirdbirds/page/app.mjs index 05040db..df8dc41 100644 --- a/challenges/birdsbirdbirds/page/app.mjs +++ b/challenges/birdsbirdbirds/page/app.mjs @@ -42,6 +42,11 @@ const toSkyButton = document.getElementById('to-sky-button') const drawAgainButton = document.getElementById('draw-again-button') const sky = document.getElementById('sky') +// The starting prompt is written once, in index.html, and read back here. +const INITIAL_PROMPT = statusLine.textContent +const DRAWN_PROMPT = 'Looking good! Press Save my bird when it’s done.' +const EMPTY_CANVAS_ERROR = 'The canvas is empty. Draw your bird first!' + const pen = { colour: COLOURS[0].value, width: PEN_SIZES[1].width, erasing: false } const stroke = { pointerId: undefined, lastPoint: undefined } const undoSnapshots = [] @@ -191,6 +196,17 @@ function endStroke(event) { stroke.pointerId = undefined stroke.lastPoint = undefined + promptToSave() +} + +/** + * Swaps the initial prompt, or the empty-canvas error, for a nudge to save + * once there is a drawing. Any other message (Undone, Cleared, Saved, a save + * failure) is left alone, since it reports something the attendee just did. + */ +function promptToSave() { + const showsPreDrawingMessage = statusLine.textContent === INITIAL_PROMPT || statusLine.textContent === EMPTY_CANVAS_ERROR + if (showsPreDrawingMessage && !isCanvasBlank()) setStatus(DRAWN_PROMPT) } /** Clears the canvas to transparent, undoably. */ @@ -219,7 +235,7 @@ function canvasToPng() { /** Saves the drawing to the workspace as bird.png and reports how it went. */ async function saveBird() { - if (isCanvasBlank()) return setStatus('The canvas is empty. Draw your bird first!', 'error') + if (isCanvasBlank()) return setStatus(EMPTY_CANVAS_ERROR, 'error') saveButton.disabled = true setStatus('Saving…') @@ -281,6 +297,7 @@ function showDraw() { skyMode.hidden = true drawMode.hidden = false sky.replaceChildren() + setStatus(isCanvasBlank() ? INITIAL_PROMPT : DRAWN_PROMPT) toSkyButton.hidden = flight.entry === undefined } diff --git a/challenges/birdsbirdbirds/page/index.html b/challenges/birdsbirdbirds/page/index.html index 4217111..43853c2 100644 --- a/challenges/birdsbirdbirds/page/index.html +++ b/challenges/birdsbirdbirds/page/index.html @@ -18,28 +18,37 @@

Draw your bird

- diff --git a/challenges/birdsbirdbirds/page/style.css b/challenges/birdsbirdbirds/page/style.css index 4d8a632..77b2aae 100644 --- a/challenges/birdsbirdbirds/page/style.css +++ b/challenges/birdsbirdbirds/page/style.css @@ -44,39 +44,51 @@ button:disabled { opacity: 0.45; } +/* The booth station's Chrome is about 1000x597, so the whole drawing page, + Save button included, must fit one screen. The floor keeps shorter or + narrower windows usable by letting the page scroll instead of squashing. */ #draw-mode { - max-width: 1000px; + display: grid; + grid-template-rows: auto minmax(0, 1fr); + max-width: 1280px; + height: max(100dvh, 560px); margin: 0 auto; - padding: 24px 20px 40px; + padding: 10px 20px 14px; } .draw-header h1 { - margin: 0 0 6px; - font-size: 2.4rem; + margin: 0 0 2px; + font-size: 1.7rem; + line-height: 1.15; } .lead { - margin: 0 0 20px; - font-size: 1.15rem; + margin: 0 0 8px; + font-size: 1rem; } .draw-layout { display: flex; - flex-wrap: wrap; gap: 24px; - align-items: flex-start; + min-height: 0; +} + +.side-column { + display: flex; + flex-direction: column; + gap: 8px; + flex: 0 0 280px; } .toolbar { display: flex; flex-direction: column; - gap: 16px; - flex: 0 0 220px; + gap: 8px; } .tool-group { margin: 0; - padding: 12px; + padding: 4px 8px 8px; border: 2px solid var(--slide-text); border-radius: 14px; background: #fff; @@ -91,28 +103,29 @@ button:disabled { .sizes, .tools { display: flex; - flex-wrap: wrap; - gap: 8px; + gap: 4px; } .swatch { - width: 52px; - height: 52px; + flex: 1; + min-width: 0; + aspect-ratio: 1; + max-height: 44px; border: 3px solid #fff; border-radius: 50%; box-shadow: 0 0 0 2px var(--slide-text); } .swatch[aria-pressed="true"] { - box-shadow: 0 0 0 4px var(--slide-text), 0 0 0 7px var(--slide-lime); + box-shadow: 0 0 0 3px var(--slide-text), 0 0 0 5px var(--slide-lime); } .size { display: flex; + flex: 1; align-items: center; justify-content: center; - width: 56px; - height: 56px; + height: 44px; border: 2px solid var(--slide-text); border-radius: 12px; background: #fff; @@ -132,12 +145,12 @@ button:disabled { .tool-button, .action-button { flex: 1; - min-height: 52px; - padding: 8px 14px; + min-height: 44px; + padding: 4px 14px; border: 2px solid var(--slide-text); border-radius: 12px; background: #fff; - font-size: 1.1rem; + font-size: 1.05rem; font-weight: 700; } @@ -146,16 +159,27 @@ button:disabled { gap: 8px; } -.canvas-column { +/* Beside the tools, so Save is never below the fold. */ +.save-area { display: flex; flex-direction: column; - align-items: center; - gap: 14px; - flex: 1 1 512px; + gap: 6px; + align-items: stretch; +} + +/* Sized from the space left over, so the square canvas never overflows. */ +.canvas-column { + display: flex; + flex: 1; + align-items: flex-start; + justify-content: center; + min-width: 0; + container-type: size; } .canvas-frame { - width: min(512px, 100%); + /* Room for the frame's 6px offset shadow. */ + width: min(100cqw - 6px, 100cqh - 6px, 640px); aspect-ratio: 1; border: 3px solid var(--slide-text); border-radius: 16px; @@ -168,6 +192,29 @@ button:disabled { background-size: 32px 32px; } +/* Too narrow for tools beside the canvas: draw first, tools below, and let the + page scroll. The canvas column needs a definite height for its container + query sizing. */ +@media (max-width: 40rem) { + #draw-mode { + height: auto; + } + + .draw-layout { + flex-direction: column; + } + + .canvas-column { + flex: none; + order: -1; + height: min(100vw - 40px, 640px); + } + + .side-column { + flex-basis: auto; + } +} + #drawing { display: block; width: 100%; @@ -181,14 +228,13 @@ button:disabled { } .save-button { - min-width: 260px; - min-height: 64px; - padding: 12px 32px; + min-height: 56px; + padding: 8px 16px; border: 3px solid var(--slide-text); border-radius: 16px; background: var(--slide-lime); box-shadow: 4px 4px 0 var(--slide-text); - font-size: 1.5rem; + font-size: 1.4rem; font-weight: 800; } @@ -198,9 +244,9 @@ button:disabled { } .status { - min-height: 1.5em; + min-height: 2.6em; margin: 0; - font-size: 1.2rem; + font-size: 1rem; font-weight: 600; text-align: center; } @@ -215,14 +261,16 @@ button:disabled { .waiting { margin: 0; - font-size: 1rem; + font-size: 0.9rem; + text-align: center; opacity: 0.7; } .link-button { + padding: 2px; border: none; background: none; - font-size: 1.1rem; + font-size: 1rem; font-weight: 700; text-decoration: underline; } diff --git a/challenges/birdsbirdbirds/server.mjs b/challenges/birdsbirdbirds/server.mjs index d046b5d..52404c4 100644 --- a/challenges/birdsbirdbirds/server.mjs +++ b/challenges/birdsbirdbirds/server.mjs @@ -116,7 +116,7 @@ function sendSavedBird(response, workspace) { body = readWorkspaceFile(path.join(workspace, 'bird.png'), MAX_PNG_BYTES) } catch (error) { if (error.code === 'ENOENT') return sendText(response, 404, 'no bird.png yet') - if (error instanceof RefusedFileError) return sendText(response, 422, `${error.message}; press Save on the drawing page again`) + if (error instanceof RefusedFileError) return sendText(response, 422, `${error.message}; press Save my bird on the drawing page again`) throw error } diff --git a/challenges/birdsbirdbirds/skills/start-challenge/SKILL.md b/challenges/birdsbirdbirds/skills/start-challenge/SKILL.md index 8f7a871..2cde03c 100644 --- a/challenges/birdsbirdbirds/skills/start-challenge/SKILL.md +++ b/challenges/birdsbirdbirds/skills/start-challenge/SKILL.md @@ -41,15 +41,19 @@ starts (after `session_info`, when you have it): call `show_preview` with `http://127.0.0.1:4322/` without asking first. Without `show_preview`, give them that URL to open right away. +Whenever `show_preview` returns `offered`, a dialog is still asking the +participant whether to open the page. Say "Click **Open preview** to start +drawing." Never say the page is open until a call returns `navigated`. + When you resume with `draw-bird` not yet complete, first use `bash` to check whether `bird.png` exists in the current directory: a save made just before a restart sends no message. If it exists, go straight to step 2. -Tell them to draw any bird, then press Save. Saving tells you on its own, as -a `[birdsbirdbirds]` message. If they say they saved but no message came, use -`bash` to check that `bird.png` exists in the current directory; if it -doesn't, ask them to press Save and look again. When it exists, go on to -step 2. +Tell them to draw any bird, then press **Save my bird**; always call the +button that. Saving tells you on its own, as a `[birdsbirdbirds]` message. If +they say they saved but no message came, use `bash` to check that `bird.png` +exists in the current directory; if it doesn't, ask them to press **Save my +bird** and look again. When it exists, go on to step 2. ## 2. Meet the bird @@ -111,18 +115,22 @@ checker or edit it. When it passes, report `animate`. The drawing page switches to the sky on its own once a valid `bird.json` exists. Tell the participant to look at it (call `show_preview` again if the -page was closed). They can press "Draw again" to redraw; if they do, start +page was closed; on `offered`, say "Click **Open preview** to watch it +fly."). They can press "Draw again" to redraw; if they do, start again at step 1. Ask how the flight looks and offer one tweak, such as faster, higher, or more wobbly. If they want it, edit `bird.json`, rerun the checker, and let -them watch again. When they say they've watched it fly, report `fly`. +them watch again. When they say they've watched it fly, go on to step 5; +`fly` is the last step, and reporting it moves the participant's focus to +Finish, so it waits for the gallery answer. ## 5. Gallery -Only with `publish_gallery_item`. Use the display name from `session_info` -and ask: "Want this on the booth screens? It'll show with your name, -[display name], and staff can remove it at any time." +Only with `publish_gallery_item`; without it, or if you already published +this bird in this conversation, skip to step 6. Use the display +name from `session_info` and ask: "Want this on the booth screens? It'll show +with your name, [display name], and staff can remove it at any time." On a yes, run the checker once more, then `read` `bird.json` and call `publish_gallery_item` with: @@ -134,3 +142,9 @@ On a yes, run the checker once more, then `read` `bird.json` and call - `payload`: exactly the object you just read from `bird.json` On a no, move on without publishing. + +## 6. Finish + +Report `fly`. With `report_step`, end with the finish line, naming the +points: "All steps are done. Press Enter, or choose Finish, to claim your 150 +pts." Finish closes the page, so don't invite them to keep watching it. diff --git a/test/birdsbirdbirds.test.mjs b/test/birdsbirdbirds.test.mjs index dae489a..7141bee 100644 --- a/test/birdsbirdbirds.test.mjs +++ b/test/birdsbirdbirds.test.mjs @@ -721,6 +721,7 @@ test('the server does not serve a symlinked bird.png', (testContext) => withWork const response = await sendRequest({ port, requestPath: '/bird.png' }) assert.equal(response.status, 422) + assert.match(response.body.toString(), /press Save my bird on the drawing page again/) })) test('the server does not serve a bird.png over the size cap', (testContext) => withWorkspace(async ({ workspace }) => {