From 26ba5ae55ecf5cef74e09b5aa21f8179393ccc06 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 18:33:53 +0100 Subject: [PATCH 1/7] Draw the project artwork and editor canvas instead of grey-boxing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three biggest grey boxes were the project illustration, the project preview and the editor canvas — the last of which sits in the middle of the screen every import flow ends on. Grey boxes are right for a testing session and wrong for a stakeholder review, where they read as unfinished. Two SVG components in kit/, both drawn rather than photographic so they stay honest about being stand-ins: - ProjectThumbnail generates artwork from a project's id, so the same project looks the same in a catalogue and on a class page. The live site has bespoke illustrations we cannot use — our projects are invented, so real artwork would attach a real project's picture to a made-up title. - EditorCanvas is a still of the editor: blocks for Scratch, a text editor otherwise. It is drawn portrait because the pane in this workbench is portrait; wide art letterboxed into it read as a broken image. Applied to ProjectPage, ProjectEditor and ClassroomProjectEditor. The other seven Placeholders are untouched so far. Co-Authored-By: Claude Opus 5 --- .claude/launch.json | 10 +- src/kit/EditorCanvas.tsx | 195 ++++++++++++++++++ src/kit/ProjectThumbnail.tsx | 186 +++++++++++++++++ src/kit/index.ts | 2 + src/screens/ccp/ProjectEditor.tsx | 6 +- src/screens/ccp/ProjectPage.tsx | 6 +- .../classroom/ClassroomProjectEditor.tsx | 6 +- src/styles/global.css | 16 ++ 8 files changed, 415 insertions(+), 12 deletions(-) create mode 100644 src/kit/EditorCanvas.tsx create mode 100644 src/kit/ProjectThumbnail.tsx diff --git a/.claude/launch.json b/.claude/launch.json index 1fb40d3..9890a0d 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -4,8 +4,12 @@ { "name": "c4-user-flows-prototype", "runtimeExecutable": "npm", - "runtimeArgs": ["run", "dev"], - "port": 5173 + "runtimeArgs": [ + "run", + "dev" + ], + "port": 5173, + "autoPort": true } ] -} +} \ No newline at end of file diff --git a/src/kit/EditorCanvas.tsx b/src/kit/EditorCanvas.tsx new file mode 100644 index 0000000..9807349 --- /dev/null +++ b/src/kit/EditorCanvas.tsx @@ -0,0 +1,195 @@ +/** + * A still of the code editor, drawn in SVG. + * + * The biggest grey box in the repo was the editor canvas, and it sat in the + * middle of the screen every import flow ends on. The real thing cannot be + * embedded — the live blocks editor is inside a shadow DOM, and this repo does + * not add dependencies — so this is a picture of one. + * + * It does not pretend to work. Nothing here is clickable and there is no code + * you can read closely; at a glance it says "this is where the editor is", + * which is all the surrounding flow needs it to say. A prototype that needs a + * working editor is asking a different question and should say so in its notes. + * + * Drawn PORTRAIT, because the editor pane in this workbench is portrait + * (roughly 290x510) — the real editor is wide, but art drawn wide letterboxes + * into a tall pane and a still with bars down the side reads as a broken + * image. It crops rather than letterboxes if the pane is a different shape. + */ + +interface Props { + /** Blocks for Scratch, a text editor for anything else. */ + language: 'Scratch' | 'Python' | 'HTML' + label: string +} + +const BLOCK_COLOURS = ['#4C97FF', '#9966FF', '#CF63CF', '#FFAB19', '#40BF4A'] +const SYNTAX = ['#C678DD', '#61AFEF', '#98C379', '#E5C07B', '#56B6C2'] + +export function EditorCanvas({ language, label }: Props) { + return ( + + {language === 'Scratch' ? : } + + ) +} + +function BlocksEditor() { + return ( + <> + + + {/* Stage on top, as the editor lays out when the window is narrow. */} + + + + + + {/* Two sprites having the conversation most projects are built around. */} + + + + + + + + + {/* Sprite tray. */} + {[0, 1, 2, 3].map((i) => ( + + ))} + + + {/* Category rail. */} + + {BLOCK_COLOURS.map((colour, i) => ( + + + + + ))} + + {/* Palette of draggable blocks. */} + + + {[0, 1, 2, 3, 4, 5].map((i) => ( + + ))} + + {/* Script area, with one stack snapped together. */} + + + {[0, 1, 2, 3, 4].map((i) => ( + + ))} + + + ) +} + +function TextEditor() { + /* Indentation and token widths are fixed, not random: a picture of code + should look like code, and real code has structure. */ + const lines = [ + [0, [30, 52]], + [0, [22, 40, 24]], + [0, []], + [0, [34, 58]], + [1, [26, 44, 22]], + [1, [40, 30]], + [0, []], + [0, [24, 38]], + [1, [32, 50, 26]], + [2, [40, 24]], + [2, [28, 46]], + [1, [34, 22]], + [0, []], + [0, [26, 42, 20]], + ] as const + + return ( + <> + + + {/* Tab bar, so it reads as an editor rather than a terminal. */} + + + + + {/* Gutter. */} + + {lines.map((_, i) => ( + + ))} + + {lines.map(([indent, tokens], row) => { + let x = 30 + indent * 12 + return ( + + {tokens.map((width, i) => { + const rect = ( + + ) + x += width + 7 + return rect + })} + + ) + })} + + {/* Cursor, parked at the end of the last line. */} + + + {/* Output pane. */} + + + + {[56, 88, 44, 70].map((width, i) => ( + + ))} + + ) +} diff --git a/src/kit/ProjectThumbnail.tsx b/src/kit/ProjectThumbnail.tsx new file mode 100644 index 0000000..1e37874 --- /dev/null +++ b/src/kit/ProjectThumbnail.tsx @@ -0,0 +1,186 @@ +/** + * Illustrative artwork for a project, drawn in SVG. + * + * The live Projects site gives every project a bespoke illustration. We cannot + * ship those — our projects are invented, so real artwork would attach a real + * project's picture to a made-up title — and a grey box reads as unfinished in + * a stakeholder review. So each project gets generated artwork instead: + * language-themed, deterministic from its id, and obviously drawn rather than + * photographic, which keeps it honest about being a stand-in. + * + * Deterministic matters. The same project must look the same everywhere it + * appears, or a catalogue and a class page will disagree about which project + * is which. + */ + +/* Illustration colours, deliberately NOT --rpf-* tokens. These are Scratch's + and Python's own brand colours plus scene colours for the artwork; they are + not UI colours and there are no design tokens for them. Same reasoning as + the literal hues in surfaces/tokens.css. */ +const PALETTES = { + Scratch: { + sky: '#E8F4FF', + ground: '#9BD35A', + ink: '#4C97FF', + accents: ['#4C97FF', '#9966FF', '#CF63CF', '#FFAB19'], + }, + Python: { + sky: '#1B2A57', + ground: '#2B4171', + ink: '#FFD43B', + accents: ['#4B8BBE', '#FFD43B', '#646464', '#306998'], + }, + HTML: { + sky: '#FFF1E8', + ground: '#F2734C', + ink: '#E44D26', + accents: ['#E44D26', '#F06529', '#EBEBEB', '#264DE4'], + }, +} as const + +export type ThumbnailLanguage = keyof typeof PALETTES + +/** Small stable hash, so one project always gets the same arrangement. */ +function seedOf(key: string) { + let hash = 0 + for (let i = 0; i < key.length; i += 1) { + hash = (hash * 31 + key.charCodeAt(i)) | 0 + } + return Math.abs(hash) +} + +interface Props { + /** Project id — the seed. Two projects with the same id look identical. */ + seed: string + language: ThumbnailLanguage + /** CSS height. A number is pixels. Defaults to filling its container. */ + height?: number | string + /** Decorative by default. Pass a title if the image carries meaning alone. */ + label?: string +} + +export function ProjectThumbnail({ seed, language, height = '100%', label }: Props) { + const palette = PALETTES[language] ?? PALETTES.Scratch + const n = seedOf(seed) + + /* Three motifs, picked by seed, so a catalogue of six projects does not look + like the same picture six times. */ + const motif = n % 3 + + return ( + + + + {/* Rolling ground, offset by seed so the horizon is not always level. */} + + + {/* Soft shapes in the background, placed by seed. */} + {[0, 1, 2].map((i) => ( + > (i * 3)) % 130)} + cy={14 + ((n >> (i * 2)) % 34)} + r={5 + ((n >> i) % 9)} + fill={palette.accents[(n + i) % palette.accents.length]} + opacity="0.35" + /> + ))} + + {motif === 0 && } + {motif === 1 && } + {motif === 2 && } + + ) +} + +type Palette = (typeof PALETTES)[ThumbnailLanguage] + +/** Three snapped-together code blocks, notches and all. */ +function StackedBlocks({ palette, seed }: { palette: Palette; seed: number }) { + return ( + + {[0, 1, 2].map((i) => ( + > i) % 14} + a3 3 0 0 1 3 3 v9 a3 3 0 0 1 -3 3 h-${46 - (seed >> i) % 14} + a3 3 0 0 1 -6 0 h-12 a3 3 0 0 1 -3 -3 v-9 a3 3 0 0 1 3 -3 Z`} + fill={palette.accents[(seed + i) % palette.accents.length]} + /> + ))} + + ) +} + +/** A little app window — stands in for a running project. */ +function Window({ palette, seed }: { palette: Palette; seed: number }) { + return ( + + + + + {[0, 1, 2].map((i) => ( + + ))} + {[0, 1, 2, 3].map((i) => ( + > i) % 44)} + height="5" + rx="2.5" + fill={palette.accents[(seed + i) % palette.accents.length]} + opacity="0.75" + /> + ))} + + ) +} + +/** A character on the stage — the Scratch mental model, roughly. */ +function Sprite({ palette, seed }: { palette: Palette; seed: number }) { + const body = palette.accents[seed % palette.accents.length] + return ( + + {/* Speech bubble — projects are nearly always something talking. */} + + {[0, 1].map((i) => ( + > i) % 22)} + height="4" + rx="2" + fill={palette.ink} + opacity="0.6" + /> + ))} + {/* The character. */} + + + + + + + + ) +} diff --git a/src/kit/index.ts b/src/kit/index.ts index df9b89c..387282c 100644 --- a/src/kit/index.ts +++ b/src/kit/index.ts @@ -18,4 +18,6 @@ export * from '@raspberrypifoundation/design-system-react' // Ours, because the design system has no equivalent. export { Card } from './Card' +export { EditorCanvas } from './EditorCanvas' export { Placeholder } from './Placeholder' +export { ProjectThumbnail } from './ProjectThumbnail' diff --git a/src/screens/ccp/ProjectEditor.tsx b/src/screens/ccp/ProjectEditor.tsx index f5eedf0..818eaf1 100644 --- a/src/screens/ccp/ProjectEditor.tsx +++ b/src/screens/ccp/ProjectEditor.tsx @@ -1,4 +1,4 @@ -import { Button, Placeholder, Tag } from '../../kit' +import { Button, EditorCanvas, Placeholder, Tag } from '../../kit' import type { Project } from '../../fixtures' import type { ScreenMeta } from '../types' import { Markdown } from './Markdown' @@ -150,9 +150,9 @@ export function ProjectEditor({
-
diff --git a/src/screens/ccp/ProjectPage.tsx b/src/screens/ccp/ProjectPage.tsx index c5f3529..5e317e7 100644 --- a/src/screens/ccp/ProjectPage.tsx +++ b/src/screens/ccp/ProjectPage.tsx @@ -1,4 +1,4 @@ -import { Button, Placeholder } from '../../kit' +import { Button, ProjectThumbnail } from '../../kit' import type { Project } from '../../fixtures' import type { ScreenMeta } from '../types' import { Markdown } from './Markdown' @@ -47,7 +47,7 @@ export function ProjectPage({ project, onStart, onImport }: Props) {
- +
@@ -69,7 +69,7 @@ export function ProjectPage({ project, onStart, onImport }: Props) {
- +
)} diff --git a/src/screens/classroom/ClassroomProjectEditor.tsx b/src/screens/classroom/ClassroomProjectEditor.tsx index c531ed2..477cc49 100644 --- a/src/screens/classroom/ClassroomProjectEditor.tsx +++ b/src/screens/classroom/ClassroomProjectEditor.tsx @@ -1,4 +1,4 @@ -import { Button, CheckboxInput, Placeholder, Tag } from '../../kit' +import { Button, CheckboxInput, EditorCanvas, Tag } from '../../kit' import type { Project } from '../../fixtures' import { Markdown } from '../ccp/Markdown' import type { ScreenMeta } from '../types' @@ -111,9 +111,9 @@ export function ClassroomProjectEditor({
-
diff --git a/src/styles/global.css b/src/styles/global.css index 075d918..9eb49f4 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -943,6 +943,22 @@ hr.divider { display: flex; min-height: 0; } +/* The drawn editor fills its pane and crops rather than letterboxing — a + still of an editor with bars down the side reads as a broken image. */ +.editor-canvas-art { + flex: 1 1 auto; + width: 100%; + min-height: 0; + border-radius: var(--radius-sm); + border: 1px solid var(--rpf-grey-150); +} + +/* Generated project artwork. Cropped to its box, like a real thumbnail. */ +.project-thumb { + display: block; + border-radius: var(--radius-sm); + object-fit: cover; +} /* Project instructions markdown */ .md { From a7647412d30bf0d2b2ca512deed2ba09bb5df2e9 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 18:45:52 +0100 Subject: [PATCH 2/7] Embed the real Code Editor instead of a drawing of one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit editor-ui ships as a web component on a CDN rather than an npm package, so this is a script tag and a custom element — no dependency added, which is what makes using the actual editor possible at all. preview="true" with no auth_key gives a genuinely working editor that talks to no API, saves nothing and cannot reach a real account: Python runs locally through Pyodide. The rule against real API calls is about not moving real data, and this moves none. The editor's own sidebar stays off. ProjectEditor's Steps/Save rail is the finding for the import lane — a young person with only a Classroom account cannot save — and handing that to the editor's chrome would bury it. The component exposes editor-logIn and editor-signUp so a prototype still decides what happens when someone tries. Scratch cannot be embedded. The bundle loads Scratch in an iframe keyed to a project the editor API already holds (project_type code_editor_scratch); there is no way to pass one inline the way `code` passes Python, and our projects are invented so there is no identifier. Scratch projects keep the drawn still, as does any project when the CDN is unreachable. The editor mounts into a node React deliberately leaves empty. Sharing one with React crashes the app on removeChild the moment the loading message is cleared. Co-Authored-By: Claude Opus 5 --- src/kit/EditorEmbed.tsx | 155 ++++++++++++++++++ src/kit/index.ts | 1 + src/screens/ccp/ProjectEditor.tsx | 9 +- .../classroom/ClassroomProjectEditor.tsx | 7 +- src/styles/global.css | 22 +++ 5 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 src/kit/EditorEmbed.tsx diff --git a/src/kit/EditorEmbed.tsx b/src/kit/EditorEmbed.tsx new file mode 100644 index 0000000..043259d --- /dev/null +++ b/src/kit/EditorEmbed.tsx @@ -0,0 +1,155 @@ +import { useEffect, useRef, useState } from 'react' +import { EditorCanvas } from './EditorCanvas' + +/** + * The real Raspberry Pi Code Editor, embedded. + * + * `editor-ui` ships as a web component on a CDN rather than an npm package, so + * this is a script tag and a custom element — no dependency added, which is + * what lets us use the actual editor at all. + * + * https://github.com/RaspberryPiFoundation/editor-ui + * + * It runs Python locally through Pyodide, so `preview` mode with no `auth_key` + * gives a genuinely working editor that talks to no API, saves nothing and + * cannot touch a real account. That matters here: the repo's rule against real + * API calls is about not moving real data, and this moves none. + * + * WHAT IT DOES NOT DO: Scratch. The bundle loads Scratch in an iframe keyed to + * a project the editor API already holds (`project_type: code_editor_scratch`) + * — there is no way to hand it a Scratch project inline the way `code` hands + * it Python. Our projects are invented, so there is no identifier to pass. + * Scratch projects therefore fall back to the drawn still, as does any project + * if the CDN is unreachable. + */ + +/* Pinned, not `latest` — `releases/latest` 404s, and a prototype that silently + changes editor version between one review and the next is a bad prototype. + Bump deliberately. */ +const VERSION = 'v0.38.1' +const SCRIPT_URL = `https://editor-static.raspberrypi.org/releases/${VERSION}/web-component.js` + +/* The bundle is ~10MB. Load it once per session, and only when a screen + actually shows an editor — not on the landing page. */ +let loading: Promise | undefined + +function loadEditorScript() { + if (!loading) { + loading = new Promise((resolve, reject) => { + if (customElements.get('editor-wc')) { + resolve() + return + } + const script = document.createElement('script') + script.src = SCRIPT_URL + script.async = true + script.onload = () => resolve() + script.onerror = () => reject(new Error(`Could not load the editor from ${SCRIPT_URL}`)) + document.head.appendChild(script) + }) + } + return loading +} + +interface Props { + language: 'Scratch' | 'Python' | 'HTML' + /** Starter code. Overrides the editor's own empty main.py / index.html. */ + code: string + /** Described to screen readers, and used on the drawn fallback. */ + label: string + /** + * Instruction steps, if the editor should show its own instructions panel. + * Most prototypes here render steps themselves in the left rail, so this is + * usually left off — pass it only if the flow is about the editor's sidebar. + */ + steps?: { title: string; body: string }[] + /** The editor asks the host page to handle sign-in; it does not do it itself. */ + onLogIn?: () => void + onSignUp?: () => void +} + +export function EditorEmbed({ language, code, label, steps, onLogIn, onSignUp }: Props) { + const host = useRef(null) + const [failed, setFailed] = useState(false) + const [ready, setReady] = useState(false) + + /* Scratch can never load inline, so do not even fetch 10MB to find out. */ + const embeddable = language !== 'Scratch' + + /* Serialised here rather than in the effect, so the effect depends on a + string. `steps` is a fresh array on every render; depending on it directly + would tear down and rebuild a 10MB editor on every parent render. */ + const instructions = steps + ? JSON.stringify({ + project: { steps: steps.map((step) => ({ content: `

${step.title}

${step.body}` })) }, + }) + : undefined + + useEffect(() => { + if (!embeddable) return + let cancelled = false + const mount = host.current + + loadEditorScript().then( + () => { + if (cancelled || !mount) return + + /* Built imperatively rather than as JSX. React sets custom element + attributes inconsistently across versions, and every attribute here + has to arrive as a string — getting that wrong silently gives you an + empty editor rather than an error. + + It goes into a node React deliberately leaves empty. Appending into + a node React also renders into gets you a removeChild crash the + moment React tries to tidy up a child we already replaced. */ + const editor = document.createElement('editor-wc') + editor.setAttribute('preview', 'true') + editor.setAttribute('code', code) + editor.setAttribute('aria-label', label) + if (instructions) { + editor.setAttribute('with_sidebar', 'true') + editor.setAttribute('sidebar_options', JSON.stringify(['instructions'])) + editor.setAttribute('instructions', instructions) + } + + mount.replaceChildren(editor) + setReady(true) + }, + () => { + if (!cancelled) setFailed(true) + }, + ) + + return () => { + cancelled = true + mount?.replaceChildren() + } + }, [embeddable, code, label, instructions]) + + /* The editor asks the page to handle sign-in rather than doing it itself, + which is the whole point for the import lane: the prototype decides what + happens, so a variant can send someone somewhere different. */ + useEffect(() => { + if (!onLogIn && !onSignUp) return + const logIn = () => onLogIn?.() + const signUp = () => onSignUp?.() + document.addEventListener('editor-logIn', logIn) + document.addEventListener('editor-signUp', signUp) + return () => { + document.removeEventListener('editor-logIn', logIn) + document.removeEventListener('editor-signUp', signUp) + } + }, [onLogIn, onSignUp]) + + if (!embeddable || failed) { + return + } + + return ( +
+ {!ready &&

Loading the editor…

} + {/* React never renders into this one — the editor is mounted here. */} +
+
+ ) +} diff --git a/src/kit/index.ts b/src/kit/index.ts index 387282c..a03e9a0 100644 --- a/src/kit/index.ts +++ b/src/kit/index.ts @@ -19,5 +19,6 @@ export * from '@raspberrypifoundation/design-system-react' // Ours, because the design system has no equivalent. export { Card } from './Card' export { EditorCanvas } from './EditorCanvas' +export { EditorEmbed } from './EditorEmbed' export { Placeholder } from './Placeholder' export { ProjectThumbnail } from './ProjectThumbnail' diff --git a/src/screens/ccp/ProjectEditor.tsx b/src/screens/ccp/ProjectEditor.tsx index 818eaf1..a37d538 100644 --- a/src/screens/ccp/ProjectEditor.tsx +++ b/src/screens/ccp/ProjectEditor.tsx @@ -1,4 +1,4 @@ -import { Button, EditorCanvas, Placeholder, Tag } from '../../kit' +import { Button, EditorEmbed, Placeholder, Tag } from '../../kit' import type { Project } from '../../fixtures' import type { ScreenMeta } from '../types' import { Markdown } from './Markdown' @@ -150,9 +150,12 @@ export function ProjectEditor({
-
diff --git a/src/screens/classroom/ClassroomProjectEditor.tsx b/src/screens/classroom/ClassroomProjectEditor.tsx index 477cc49..3b6203a 100644 --- a/src/screens/classroom/ClassroomProjectEditor.tsx +++ b/src/screens/classroom/ClassroomProjectEditor.tsx @@ -1,4 +1,4 @@ -import { Button, CheckboxInput, EditorCanvas, Tag } from '../../kit' +import { Button, CheckboxInput, EditorEmbed, Tag } from '../../kit' import type { Project } from '../../fixtures' import { Markdown } from '../ccp/Markdown' import type { ScreenMeta } from '../types' @@ -111,9 +111,10 @@ export function ClassroomProjectEditor({
-
diff --git a/src/styles/global.css b/src/styles/global.css index 9eb49f4..5fa526d 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -943,6 +943,28 @@ hr.divider { display: flex; min-height: 0; } +/* The real editor, when it loads. It brings its own chrome inside a shadow + DOM, so there is nothing to style here beyond giving it the whole pane. */ +.editor-embed { + flex: 1 1 auto; + display: flex; + min-width: 0; + min-height: 0; +} +.editor-embed-host { + flex: 1 1 auto; + display: flex; + min-width: 0; + min-height: 0; +} +.editor-embed editor-wc { + flex: 1 1 auto; + min-width: 0; +} +.editor-embed-loading { + margin: auto; +} + /* The drawn editor fills its pane and crops rather than letterboxing — a still of an editor with bars down the side reads as a broken image. */ .editor-canvas-art { From d9307d56f16e634416fea97a97dcbc7bb7b305b2 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 22:33:51 +0100 Subject: [PATCH 3/7] Build the Code Classroom class and school pages from the designs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pages come from the Code Editor Figma file, recorded in DESIGNS.md: "Individual class" (7355:37981) and "Dashboard Owner" (6759:78797). The structural finding is that a Code Classroom page is two bands — a white block carrying the breadcrumb, title, description and page actions, then the content on a warm off-white below. Screens render those bands themselves; the surface stops padding classroom pages. EducatorSchoolHome is new, and replaces a known gap rather than adding a screen. Both import prototypes had grown their own guess at the mentor's school page, each built by mirroring the young person's version, each carrying a comment saying nobody had checked it. This one is checked: the login code is the largest thing on the page after the title, because a mentor reads it out across a room, and the share link beside it is the route that skips the school-code screen entirely. Also from the designs, and correcting guesses: - surfaces/tokens.css had the classroom accent and page colour marked as a guess. They are editor-color-theme and editor-color-layer-accent. - Breadcrumbs use a chevron and a bold underlined link, not a slash. - The footer carries the safeguarding line, Accessibility and Cookies. Only on Code Classroom: the other products very likely have it too, but nobody has checked, and a footer borrowed from a sister product is how an invention quietly becomes "what the product looks like". - Boxed list rows are white with hairline dividers, not pale fills. One real bug found on the way: .cc-section-head let its text block size itself, so a section's action wrapped onto its own line however much room there was. It needs an explicit basis of 0. That affected all four lists, including two that were already there. The topbar logo is still the words "Raspberry Pi Foundation" — the designs use the colour wordmark, and nothing here ships an image asset. Co-Authored-By: Claude Opus 5 --- DESIGNS.md | 10 + src/ScreenGallery.tsx | 15 ++ src/screens/README.md | 13 ++ src/screens/classroom/EducatorClassPage.tsx | 156 ++++++++------- src/screens/classroom/EducatorSchoolHome.tsx | 169 ++++++++++++++++ src/screens/index.ts | 4 + src/styles/global.css | 193 +++++++++++++++++-- src/styles/tokens.css | 5 + src/surfaces/Surface.tsx | 77 ++++++-- src/surfaces/surfaces.ts | 11 ++ src/surfaces/tokens.css | 15 +- 11 files changed, 561 insertions(+), 107 deletions(-) create mode 100644 src/screens/classroom/EducatorSchoolHome.tsx diff --git a/DESIGNS.md b/DESIGNS.md index e680015..0a13839 100644 --- a/DESIGNS.md +++ b/DESIGNS.md @@ -13,6 +13,8 @@ You need a Figma seat with Dev Mode access to open the Figma links. | Code Club Projects | [Live blocks editor](https://projects.raspberrypi.org/en/projects/editor-neil-the-seal/editor) | `ProjectEditor` | | Code Classroom | [Young people creating their own projects (FigJam)](https://www.figma.com/board/EuQrBiiwuk5wTc5NJ0ip7q/Young-people-students-can-create-their-own-projects-in-Code-Classroom?node-id=67-49) | `EducatorClassPage`, `EducatorProjectPage`, `YoungPersonSchoolHome`, `YoungPersonClassPage`, `ClassroomProjectEditor` | | Code Classroom | [Code Editor — Designs, student user flow](https://www.figma.com/design/SrINjD1MCT6VWYf1Yc9fV1/Code-Editor---Designs?node-id=19655-360604) | Background on the Google SSO join route | +| Code Classroom | [Code Editor — Designs, "Individual class"](https://www.figma.com/design/SrINjD1MCT6VWYf1Yc9fV1/Code-Editor---Designs?node-id=7355-37981) | `EducatorClassPage`, and the Code Classroom page furniture — title band, breadcrumb, footer | +| Code Classroom | [Code Editor — Designs, "Dashboard Owner"](https://www.figma.com/design/SrINjD1MCT6VWYf1Yc9fV1/Code-Editor---Designs?node-id=6759-78797) | `EducatorSchoolHome` | | Pi Accounts | Live: classroom.raspberrypi.org/login | `RoleChooser`, `SchoolCodeEntry`, `StudentSignIn` | ## Notes for anyone pulling from these @@ -27,3 +29,11 @@ You need a Figma seat with Dev Mode access to open the Figma links. reach it. Screenshot it. - **`MentorSignIn` is not verified** — it is behind a login and was built from the pattern. Correct it if you can see the real thing. +- **The Code Classroom pages are two bands.** A white block carrying the + breadcrumb, title, description and page actions, then the content on + `#fbf9f6` below. Both values come from the Code Editor file + (`editor-color-layer-accent`, `editor-color-theme`) and replace what were + guesses in `surfaces/tokens.css`. +- **The topbar logo is still the words "Raspberry Pi Foundation".** The designs + use the colour wordmark. Nothing in this repo ships an image asset yet, so + this is the one piece of that chrome not reproduced. diff --git a/src/ScreenGallery.tsx b/src/ScreenGallery.tsx index 2a1cb61..98057b6 100644 --- a/src/ScreenGallery.tsx +++ b/src/ScreenGallery.tsx @@ -6,6 +6,7 @@ import { ManageClub, MentorDashboard, EducatorClassPage, + EducatorSchoolHome, EducatorProjectPage, MentorSignIn, ProjectEditor, @@ -20,6 +21,7 @@ import { manageClubMeta, mentorDashboardMeta, educatorClassPageMeta, + educatorSchoolHomeMeta, educatorProjectPageMeta, mentorSignInMeta, projectEditorMeta, @@ -209,6 +211,7 @@ export function ScreenGallery() { classGroup={scratchGroup} projects={classProjects} memberCount={scratchGroup.studentIds.length} + description="Our Thursday after-school Code Club. We work through Scratch projects at our own pace — come along whenever you can." onAddProject={noop} onOpenProject={noop} onCopyLink={noop} @@ -232,6 +235,18 @@ export function ScreenGallery() { /> + + + + void onOpenProject: (projectId: string) => void onCopyLink: () => void @@ -54,6 +60,7 @@ export function EducatorClassPage({ classGroup, projects, memberCount, + description, onAddProject, onOpenProject, onCopyLink, @@ -63,82 +70,89 @@ export function EducatorClassPage({ onSettings, }: Props) { return ( -
-

{classGroup.name}

- -
- {/* Primary on the live page. Getting young people into the class is - the job a mentor comes here to do. */} -
+ <> +
+
+

{classGroup.name}

+ {description &&

{description}

} +
- -
-
-

Projects

-

- Projects are shared with students and contain starter code created by a teacher. -

-
+
+ {/* Primary on the live page. Getting young people into the class is + the job a mentor comes here to do. */}
+
- {projects.length === 0 ? ( -

No projects yet

- ) : ( -
    - {projects.map((project) => { - const hidden = hiddenProjectIds.includes(project.id) - return ( -
  • - - {hidden && ( - // Labelled rather than aria-hidden: the icon is the only - // thing on the row carrying this, so it has to be read. - - visibility_off - - )} - - -
  • - ) - })} -
- )} -
-
+
+ +
+
+

Projects

+

+ Projects are shared with students and contain starter code created by a teacher. +

+
+
+ + {projects.length === 0 ? ( +

No projects yet

+ ) : ( +
    + {projects.map((project) => { + const hidden = hiddenProjectIds.includes(project.id) + return ( +
  • + + {hidden && ( + // Labelled rather than aria-hidden: the icon is the only + // thing on the row carrying this, so it has to be read. + + visibility_off + + )} + + +
  • + ) + })} +
+ )} +
+
+ ) } diff --git a/src/screens/classroom/EducatorSchoolHome.tsx b/src/screens/classroom/EducatorSchoolHome.tsx new file mode 100644 index 0000000..d96c2b0 --- /dev/null +++ b/src/screens/classroom/EducatorSchoolHome.tsx @@ -0,0 +1,169 @@ +import { Button, Card } from '../../kit' +import type { ClassGroup, School } from '../../fixtures' +import type { ScreenMeta } from '../types' + +export const meta: ScreenMeta = { + surface: 'classroom', + existsToday: true, + verified: true, + note: 'A mentor’s home in Code Classroom: the school, its login code, and the classes under it. The top of the tree — every mentor flow starts here.', +} + +interface Props { + school: School + classes: ClassGroup[] + /** The share link a mentor can hand out instead of the code. */ + joinLink: string + onOpenClass: (classId: string) => void + onCreateClass: () => void + onManageMembers: () => void + onCopyLink: () => void + /** The gear beside "Manage members". Renders whether or not you wire it. */ + onSettings?: () => void + /** The per-row overflow menu. Renders whether or not you wire it. */ + onClassMenu?: (classId: string) => void + /** The back control above the title. Renders whether or not you wire it. */ + onHome?: () => void +} + +/** + * The school page a mentor lands on, built from the Code Editor Figma file + * (node 6759:78797, "Dashboard Owner"). + * + * This screen has existed in the product all along and was never in here — the + * import prototypes each grew their own guess at it, both built by mirroring + * the young person's version and both marked as probably wrong. This is the + * checked one. + * + * Two things worth noticing for Code Club: + * + * - **The login code is the most prominent thing on the page after the title.** + * A mentor reads it out across a room, and the page is built around that. + * The share link beside it is the other route in, and it is the one that + * skips the school-code screen entirely. + * - **"All the classes you manage and in your school"** — a mentor at a school + * that already uses Code Classroom sees classes they had nothing to do with, + * sitting beside their club. There is nothing on a row to tell one from the + * other. + */ +export function EducatorSchoolHome({ + school, + classes, + joinLink, + onOpenClass, + onCreateClass, + onManageMembers, + onCopyLink, + onSettings, + onClassMenu, + onHome, +}: Props) { + return ( + <> +
+ {/* A back control, not a breadcrumb: the school is the top of the tree, + so there is no trail to sit above it. */} +
+
+ + +
+
+
+ +

School login code

+

{school.schoolCode}

+

+ Student will need the school code to log in. Alternatively you can share the log in + link with them. +

+
+ {/* Readonly rather than plain text: it is a thing you copy, and + the live page lets you select it. */} + +
+
+
+ +
+ +
+
+

Classes

+

+ All the classes you manage and in your school. Classes are used to group + students and assign lessons to them. +

+
+
+ + {classes.length === 0 ? ( +

No classes yet

+ ) : ( +
    + {classes.map((group) => ( +
  • + + + +
  • + ))} +
+ )} +
+
+
+
+ + ) +} diff --git a/src/screens/index.ts b/src/screens/index.ts index dcd132b..0b96d3d 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -22,6 +22,10 @@ export { meta as educatorProjectPageMeta, type StudentWork, } from './classroom/EducatorProjectPage' +export { + EducatorSchoolHome, + meta as educatorSchoolHomeMeta, +} from './classroom/EducatorSchoolHome' export { YoungPersonSchoolHome, meta as youngPersonSchoolHomeMeta, diff --git a/src/styles/global.css b/src/styles/global.css index 5fa526d..e5f9998 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -546,6 +546,11 @@ hr.divider { flex: 1 0 auto; padding: var(--space-3); } +/* Code Classroom pages are built as two full-width bands — a white title block + over content on the page colour — so they pad their own bands instead. */ +.surface-classroom > .surface-body { + padding: 0; +} /* Pi Accounts and the Code Classroom sign-in are a coloured page with one centred card. */ .surface-body.centred { @@ -561,17 +566,40 @@ hr.divider { .surface-footer { display: flex; - align-items: center; - flex-wrap: wrap; - gap: var(--space-2); - justify-content: space-between; - padding: var(--space-1) var(--space-2); + flex-direction: column; + gap: var(--space-1); + padding: var(--space-2); background: var(--rpf-white); border-top: 1px solid var(--rpf-grey-150); font-size: var(--fs-075); line-height: var(--lh-1); color: var(--rpf-text-secondary); } +.surface-footer-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-1) var(--space-2); + justify-content: space-between; +} +.surface-footer-concern { + display: flex; + align-items: center; + gap: var(--space-05); +} +/* The flag sits at body size beside 12px text, so it needs bringing down. */ +.surface-footer-concern .material-symbols-sharp { + font-size: var(--fs-1); +} +.surface-footer-links { + display: flex; + flex-wrap: wrap; + gap: var(--space-05) var(--space-2); +} +.surface-footer-links > span { + font-weight: var(--fw-bold); + text-decoration: underline; +} /* ---------------- Surface gallery ---------------- */ .surface-demo { @@ -704,15 +732,37 @@ hr.divider { display: flex; align-items: center; flex-wrap: wrap; - gap: var(--space-1); padding: var(--space-1) var(--space-2); background: var(--surface-nav); border-bottom: 1px solid var(--surface-nav-border); font-size: var(--fs-075); } +.surface-crumb { + display: flex; + align-items: center; +} +/* Bold and underlined, the weight the Code Editor designs give it. Code + Classroom has no nav bar, so the breadcrumb is the navigation. */ +.surface-breadcrumbs .link-button { + font-weight: var(--fw-bold); + text-decoration: underline; +} +.surface-crumb[aria-current='page'], +.surface-crumb > span[aria-current='page'] { + font-weight: var(--fw-bold); +} .surface-crumb-sep { - color: var(--rpf-grey-300); - margin-right: var(--space-1); + color: var(--rpf-grey-500); + font-size: var(--fs-1); +} + +/* The breadcrumb sits directly above the white title band on Code Classroom, + so the two read as one block rather than two stacked bars. */ +.surface-classroom .surface-breadcrumbs { + padding: var(--space-2) var(--space-3) 0; + background: var(--surface-header); + border-bottom: none; + font-size: var(--fs-1); } .surface-account { color: var(--rpf-text-secondary); @@ -725,12 +775,23 @@ hr.divider { gap: var(--space-1); } +/* Heading and blurb on the left, the section's one action hard right — the + Code Editor designs put them on the same row. The text block needs an + explicit basis of 0: left to size itself it takes the full width and pushes + the button onto its own line however much room there is. */ .cc-section-head { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; flex-wrap: wrap; - gap: var(--space-2); + gap: var(--space-3); +} +.cc-section-head > :first-child { + flex: 1 1 0; + min-width: 200px; +} +.cc-section-head > :last-child { + flex: 0 0 auto; } .cc-list { @@ -761,9 +822,12 @@ hr.divider { border-radius: var(--radius-sm); overflow: hidden; } +/* White rows, not pale ones: the Code Editor designs draw the group as a white + panel divided by hairlines. */ .cc-list-boxed .cc-row { - padding: var(--space-2); - background: var(--rpf-off-white); + padding: var(--space-1) var(--space-1) var(--space-1) var(--space-2); + background: var(--rpf-white); + min-height: 48px; } /* Icon and name travel together on the left; the overflow menu stays right. */ .cc-row-main { @@ -1369,3 +1433,108 @@ body.full-screen-prototype .surface-footer { color: var(--rpf-grey-500); } +/* ---------------- Code Classroom page header ---------------- */ +/* The white band carrying the title, and the content band below it. */ +.cc-page-header { + display: flex; + flex-direction: column; + gap: var(--space-4); + padding: var(--space-2) var(--space-3) var(--space-4); + background: var(--surface-header); + border-bottom: 1px solid var(--surface-nav-border); +} +.cc-page-main { + display: flex; + flex-direction: column; + gap: var(--space-4); + padding: var(--space-4) var(--space-3); +} + +/* The title band's contents: breadcrumb, then title and description, then the + page actions. Taken from the Code Editor designs — 40px/56 title, 20px + description held to a readable measure, actions on their own row. */ +.cc-page-title { + display: flex; + flex-direction: column; + gap: var(--space-1); +} +.cc-page-title h1 { + font-size: var(--fs-4); + line-height: var(--lh-35); + margin: 0; +} +.cc-page-desc { + margin: 0; + max-width: 800px; + font-size: var(--fs-15); + line-height: var(--lh-15); +} +/* Title on the left, actions hard right on the same row — the school page. + The class page stacks them instead, which is why this is its own class. */ +.cc-page-titlebar { + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: var(--space-3); + justify-content: space-between; +} +.cc-page-titlebar .cc-page-title { + flex: 1 1 320px; +} + +/* "Code Classroom home" — a back control, not a breadcrumb. The school page is + the top of the tree, so there is nothing to put a trail above. Drawn as a + grey chip in the designs rather than as a bare link. */ +.cc-back-button { + align-self: flex-start; +} +.cc-back-button.rpf-button, +.cc-back-button .rpf-button { + background: var(--rpf-off-white); + border-radius: var(--radius-sm); + padding: var(--space-1) var(--space-2); + color: var(--rpf-text); +} + +/* Two columns on the school page: the login code card, then the class list. */ +.cc-split { + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: var(--space-4); +} +.cc-split-aside { + flex: 1 1 280px; + max-width: 400px; +} +.cc-split-main { + flex: 3 1 420px; + min-width: 0; +} + +/* The school login code card. The code itself is the largest thing on the + page after the title — a mentor reads it out across a room. */ +.cc-code-value { + margin: 0; + font-size: var(--fs-3); + line-height: var(--lh-25); + font-weight: var(--fw-bold); +} +.cc-code-copy { + display: flex; + align-items: stretch; + gap: var(--space-1); +} +.cc-code-link { + flex: 1 1 auto; + min-width: 0; + border: 2px solid var(--rpf-grey-150); + border-radius: var(--radius-sm); + padding: var(--space-1) 14px; + background: var(--rpf-white); + font: inherit; + color: var(--rpf-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 8ca0adb..f6813a4 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -21,10 +21,14 @@ --fs-15: 20px; --fs-2: 24px; --fs-3: 32px; + /* Code Classroom page titles: font-size-4 / line-height-3-5 in the Code + Editor Figma file. */ + --fs-4: 40px; --lh-1: 16px; --lh-15: 24px; --lh-2: 32px; --lh-25: 40px; + --lh-35: 56px; --fw-regular: 400; --fw-bold: 700; @@ -33,6 +37,7 @@ --space-1: 8px; --space-2: 16px; --space-3: 24px; + --space-4: 32px; --space-5: 40px; --space-7: 56px; diff --git a/src/surfaces/Surface.tsx b/src/surfaces/Surface.tsx index 72c189c..c9457ee 100644 --- a/src/surfaces/Surface.tsx +++ b/src/surfaces/Surface.tsx @@ -64,6 +64,31 @@ export function Surface({ const navItems = nav ?? surface.nav const effectiveLayout = layout ?? surface.layout + const crumbs = breadcrumbs && breadcrumbs.length > 0 && ( + + ) + return (
@@ -79,24 +104,7 @@ export function Surface({ )}
- {breadcrumbs && breadcrumbs.length > 0 && ( - - )} + {crumbs} {effectiveLayout !== 'centred' && navItems.length > 0 && (
- Raspberry Pi Foundation UK registered charity 1129409 - Help · Terms & Conditions · Safeguarding · Privacy + {surface.safeguardingFooter ? ( + <> +
+ + + Do you have a safeguarding concern? Contact us + + + Help + Terms & Conditions + Safeguarding + Accessibility + Privacy + Cookies + +
+ Raspberry Pi Foundation UK registered charity 1129409 + + ) : ( +
+ Raspberry Pi Foundation UK registered charity 1129409 + + Help + Terms & Conditions + Safeguarding + Privacy + +
+ )}
) diff --git a/src/surfaces/surfaces.ts b/src/surfaces/surfaces.ts index 3e3c79a..cba4868 100644 --- a/src/surfaces/surfaces.ts +++ b/src/surfaces/surfaces.ts @@ -33,6 +33,16 @@ export interface SurfaceInfo { layout: 'app' | 'centred' /** One line on what this surface is for, shown in the gallery. */ note: string + /** + * Whether the footer carries the "Do you have a safeguarding concern?" line + * and the full link set, rather than the short one. + * + * On only where a design has been read: Code Classroom, from the Code Editor + * Figma file. The other products very likely have it too, but nobody has + * checked, and a footer invented from a sister product is the kind of detail + * that quietly becomes "what the product looks like". + */ + safeguardingFooter?: boolean } export const SURFACES: Record = { @@ -64,6 +74,7 @@ export const SURFACES: Record = { nav: [], layout: 'app', note: 'Mentor and young-person facing. Student accounts work only here. Navigation is by breadcrumb, not a nav bar.', + safeguardingFooter: true, }, 'pi-accounts': { id: 'pi-accounts', diff --git a/src/surfaces/tokens.css b/src/surfaces/tokens.css index e305cd9..e0d5097 100644 --- a/src/surfaces/tokens.css +++ b/src/surfaces/tokens.css @@ -17,6 +17,7 @@ --surface-bar-text: var(--rpf-text); --surface-accent: var(--rpf-text); --surface-nav: var(--rpf-white); + --surface-header: var(--rpf-white); --surface-nav-border: var(--rpf-grey-150); /* How wide a product page is allowed to get. Screens read badly stretched across a large monitor — line lengths go past what anyone scans and a @@ -43,11 +44,17 @@ --surface-page: var(--rpf-white); } -/* classroom.raspberrypi.org — the sign-in is deep maroon; the signed-in app is - behind a login so this is a guess. */ +/* classroom.raspberrypi.org. The sign-in is deep maroon. The signed-in app was + a guess until the Code Editor Figma file was read for the class and school + pages — these are its values now, not an approximation: + editor-color-theme for the accent, editor-color-layer-accent for the page. + + The pages sit on that warm off-white, with the title block on a white band + above it. `--surface-header` is what paints the band. */ .surface-classroom { - --surface-accent: hsl(340, 78%, 36%); - --surface-page: var(--rpf-off-white); + --surface-accent: #cd2355; + --surface-page: #fbf9f6; + --surface-header: var(--rpf-white); } /* my.raspberrypi.org — raspberry page with a light grey card and a navy From 47f28e3b206abd9de827ba5849afb2ea307eff4d Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Tue, 15 Sep 2026 08:03:37 +0100 Subject: [PATCH 4/7] Build the young person's Code Classroom pages from the designs too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mentor pages went high-fidelity and the young person's did not, so Natalie's flow crossed from one to the other and looked like it had changed product halfway through. The fix belongs in `screens/`: her prototype reuses YoungPersonSchoolHome, so nothing in her folder is touched. YoungPersonSchoolHome now comes from node 6775:88858 ("Dashboard Student") rather than from the educator page with the actions taken off. That frame settles three things a guess had wrong: - There is a "Sort by" control above the list, right-aligned. Name order is the only sort it offers. - Rows carry an optional "New feedback" tag, and it is the ONLY per-row signal — no project counts, no dates, nothing about what is waiting. - A class name is bold text, not a link. An earlier pass here reasoned that a young person has no overflow menu to fall back on so the row should look clickable; the design disagrees, and the design wins. It also has no breadcrumb and no back control: the student page is the top of their tree. Worth knowing that both young-person prototypes pass `breadcrumbs` to the surface anyway, which is navigation the product does not have — their call, not this screen's. YoungPersonClassPage gets the two-band layout so it does not clash with the page it is reached from, but there is no student class frame to check it against and its comment says so. Co-Authored-By: Claude Opus 5 --- .../classroom/YoungPersonClassPage.tsx | 61 ++++---- .../classroom/YoungPersonSchoolHome.tsx | 130 ++++++++++++++---- src/styles/global.css | 17 +++ 3 files changed, 158 insertions(+), 50 deletions(-) diff --git a/src/screens/classroom/YoungPersonClassPage.tsx b/src/screens/classroom/YoungPersonClassPage.tsx index 3d28e3e..7ad75e1 100644 --- a/src/screens/classroom/YoungPersonClassPage.tsx +++ b/src/screens/classroom/YoungPersonClassPage.tsx @@ -37,33 +37,48 @@ interface Props { * adult. There is no "start something of your own" here — which is exactly * the question the Code Classroom FigJam is asking, and exactly what a Code * Club session is often about. + * + * Page furniture inferred from the educator pages, as on + * `YoungPersonSchoolHome` — see the note there. */ export function YoungPersonClassPage({ classGroup, projects, onOpenProject }: Props) { return ( -
-

{classGroup.name}

+ <> +
+
+

{classGroup.name}

+
+
- -

Projects

-

- Projects are shared with students and contain starter code created by a teacher. -

+
+ +
+
+

Projects

+

+ Projects are shared with students and contain starter code created by a teacher. +

+
+
- {projects.length === 0 ? ( -

No projects yet

- ) : ( -
    - {projects.map(({ project, status }) => ( -
  • - - -
  • - ))} -
- )} -
-
+ {projects.length === 0 ? ( +

No projects yet

+ ) : ( +
    + {projects.map(({ project, status }) => ( +
  • + + + + +
  • + ))} +
+ )} +
+
+ ) } diff --git a/src/screens/classroom/YoungPersonSchoolHome.tsx b/src/screens/classroom/YoungPersonSchoolHome.tsx index 780d3ef..b47965e 100644 --- a/src/screens/classroom/YoungPersonSchoolHome.tsx +++ b/src/screens/classroom/YoungPersonSchoolHome.tsx @@ -1,4 +1,5 @@ -import { Card } from '../../kit' +import { useState } from 'react' +import { Card, SelectInput, Tag } from '../../kit' import type { ClassGroup, School } from '../../fixtures' import type { ScreenMeta } from '../types' @@ -9,42 +10,117 @@ export const meta: ScreenMeta = { note: 'Where a young person lands after signing in. The page is titled with the SCHOOL name, which for a community club is the club name.', } +/** Name order is the only sort the design shows. */ +type Sort = 'a-z' | 'z-a' + +const SORT_OPTIONS = [ + { key: 'a-z', value: 'Name (A — Z)' }, + { key: 'z-a', value: 'Name (Z — A)' }, +] + interface Props { school: School classes: ClassGroup[] onOpenClass: (classId: string) => void + /** + * Classes where the mentor has left work for the young person to look at. + * The real page marks these "New feedback", and it is the only thing on a + * row that distinguishes one class from another — so a flow about feedback + * getting back to a young person needs it, and one that is not can leave it + * out. + */ + classesWithFeedback?: string[] } /** * A young person's home in Code Classroom: the list of classes they are in. * - * Worth watching in testing: this is titled with the school name, and at - * St Aidan's a young person in both a lesson and the club sees both listed - * together with nothing distinguishing "school work" from "club". + * Built from the Code Editor Figma file (node 6775:88858, "Dashboard + * Student"), so this is the checked version of the page rather than the + * educator page with the actions taken off. + * + * Three things worth knowing: + * + * - **It is titled with the school name**, and at St Aidan's a young person in + * both a lesson and the club sees both listed together with nothing + * distinguishing "school work" from "club". + * - **There is no breadcrumb and no back control on it.** The student page is + * the top of their tree; a prototype that gives it a crumb trail is adding + * navigation the product does not have. + * - **"New feedback" is the only per-row signal.** No project counts, no + * dates, nothing about whether anything is waiting — one tag, or nothing. */ -export function YoungPersonSchoolHome({ school, classes, onOpenClass }: Props) { +export function YoungPersonSchoolHome({ + school, + classes, + onOpenClass, + classesWithFeedback = [], +}: Props) { + /* Display state, not flow state — the screen still decides nothing about + what comes next, it just orders a list it was handed. */ + const [sort, setSort] = useState('a-z') + + const ordered = [...classes].sort((a, b) => + sort === 'a-z' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name), + ) + return ( -
-

{school.name}

- - -

Your classes

-

Here are all the classes you have been added to.

- - {classes.length === 0 ? ( -

You are not in any classes yet

- ) : ( -
    - {classes.map((group) => ( -
  • - -
  • - ))} -
- )} -
-
+ <> +
+
+

{school.name}

+
+
+ +
+ +
+
+

Your classes

+ {/* The design's wording, kept as it is written there — including + the missing "to". Same rule as "teacher" and "student": the + product's words are the product's words. */} +

+ Here you can see an overview of what classes you have been added. +

+
+
+ + {classes.length === 0 ? ( +

You are not in any classes yet

+ ) : ( + <> +
+ setSort(event.target.value as Sort)} + /> +
+ +
    + {ordered.map((group) => ( +
  • + + {/* Bold text, not a link — the design gives the row no + other affordance, so the whole name is the control. */} + + + {classesWithFeedback.includes(group.id) && ( + + )} +
  • + ))} +
+ + )} +
+
+ ) } diff --git a/src/styles/global.css b/src/styles/global.css index e5f9998..8f84a15 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -829,6 +829,23 @@ hr.divider { background: var(--rpf-white); min-height: 48px; } +/* The student class list gives its rows more height than the educator one — + 60px against 48px. */ +.cc-list-boxed .cc-row-tall { + min-height: 60px; +} + +/* A list's own controls — sorting, filtering — sit right-aligned above it. */ +.cc-list-controls { + display: flex; + justify-content: flex-end; +} +.cc-list-controls .rpf-input-field, +.cc-list-controls .rpf-select-input { + width: 240px; + max-width: 100%; +} + /* Icon and name travel together on the left; the overflow menu stays right. */ .cc-row-main { display: flex; From 189bca7b637a6e392e63e4b22fd53c7c6d9edfe2 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Tue, 15 Sep 2026 08:16:44 +0100 Subject: [PATCH 5/7] Draw the blocks editor from the real one, not from imagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Scratch still was a generic picture of an editor: grey bars where block labels go, three categories, no tabs. Redrawn from a screenshot of the live Neil the Seal starter, so the parts a mentor would recognise are the real parts — Scratch's own nine category colours in the real order with their labels, real Motion block labels, the Code/Costumes/Sounds tabs, green flag and stop, a stage over a sprite tray with the selected sprite ringed, and a snapped stack of the four blocks the instructions panel beside it is actually asking for. Still portrait, following the layout the real editor uses in a narrow window, because the pane in this workbench is portrait. Still not interactive, and the comment says so. Also recorded in DESIGNS.md why Scratch is a drawing rather than the real editor, since the reason is not obvious and is not fixable here: editor-api.raspberrypi.org sends `Access-Control-Allow-Origin: https://projects.raspberrypi.org`, so the project fetch Scratch needs is refused from localhost and from wherever this deploys. Python and HTML are unaffected — they need no API call, and they get the real editor. Co-Authored-By: Claude Opus 5 --- DESIGNS.md | 9 ++ src/kit/EditorCanvas.tsx | 206 +++++++++++++++++++++++++++------------ 2 files changed, 150 insertions(+), 65 deletions(-) diff --git a/DESIGNS.md b/DESIGNS.md index 0a13839..4d8e84b 100644 --- a/DESIGNS.md +++ b/DESIGNS.md @@ -27,6 +27,15 @@ You need a Figma seat with Dev Mode access to open the Figma links. rather than trying to extract text. - **The blocks editor is in a shadow DOM**, so page-text extraction will not reach it. Screenshot it. +- **The real editor cannot load Scratch from here, and the blocker is not in + this repo.** `editor-ui` embeds fine as a web component and Python runs + locally through Pyodide, but Scratch loads in an iframe keyed to a project + the editor API holds, and `editor-api.raspberrypi.org` answers with + `Access-Control-Allow-Origin: https://projects.raspberrypi.org`. So the + project fetch is refused from localhost and from wherever this deploys. It + would need the editor team to allow this prototype's origin. Until then the + blocks editor is `EditorCanvas`, drawn from a screenshot of the Neil the Seal + starter. - **`MentorSignIn` is not verified** — it is behind a login and was built from the pattern. Correct it if you can see the real thing. - **The Code Classroom pages are two bands.** A white block carrying the diff --git a/src/kit/EditorCanvas.tsx b/src/kit/EditorCanvas.tsx index 9807349..891ca38 100644 --- a/src/kit/EditorCanvas.tsx +++ b/src/kit/EditorCanvas.tsx @@ -1,20 +1,25 @@ /** * A still of the code editor, drawn in SVG. * - * The biggest grey box in the repo was the editor canvas, and it sat in the - * middle of the screen every import flow ends on. The real thing cannot be - * embedded — the live blocks editor is inside a shadow DOM, and this repo does - * not add dependencies — so this is a picture of one. + * Used for Scratch, where the real editor cannot be embedded, and as the + * fallback anywhere `EditorEmbed` cannot reach the CDN. Python and HTML get + * the real editor instead — see `EditorEmbed`. * - * It does not pretend to work. Nothing here is clickable and there is no code - * you can read closely; at a glance it says "this is where the editor is", - * which is all the surrounding flow needs it to say. A prototype that needs a - * working editor is asking a different question and should say so in its notes. + * The blocks half is drawn from a screenshot of the live editor (the Neil the + * Seal starter on Code Club Projects), so the parts a mentor would recognise + * are the real parts: Scratch's own category colours in the real order, real + * Motion block labels, the Code/Costumes/Sounds tabs, a stage over a sprite + * tray, and a snapped-together stack of the blocks the instructions ask for. + * + * It still does not pretend to work — nothing here is clickable. The aim is + * that it reads as THIS editor rather than as some editor, because in a + * stakeholder review a generic grey rectangle is what gets noticed. * * Drawn PORTRAIT, because the editor pane in this workbench is portrait * (roughly 290x510) — the real editor is wide, but art drawn wide letterboxes * into a tall pane and a still with bars down the side reads as a broken - * image. It crops rather than letterboxes if the pane is a different shape. + * image. So it follows the narrow-window layout the real editor uses, with the + * stage above the palette rather than beside it. */ interface Props { @@ -23,9 +28,24 @@ interface Props { label: string } -const BLOCK_COLOURS = ['#4C97FF', '#9966FF', '#CF63CF', '#FFAB19', '#40BF4A'] +/* Scratch's own category colours, so a mentor who knows Scratch recognises + the palette at a glance rather than reading it. */ +const MOTION = '#4C97FF' +const LOOKS = '#9966FF' const SYNTAX = ['#C678DD', '#61AFEF', '#98C379', '#E5C07B', '#56B6C2'] +const CATEGORIES: Array<[string, string]> = [ + ['Motion', MOTION], + ['Looks', LOOKS], + ['Sound', '#CF63CF'], + ['Events', '#FFBF00'], + ['Control', '#FFAB19'], + ['Sensing', '#5CB1D6'], + ['Operators', '#40BF4A'], + ['Variables', '#FF8C1A'], + ['My Blocks', '#FF6680'], +] + export function EditorCanvas({ language, label }: Props) { return ( = [ + ['move 10 steps', MOTION], + ['turn ↻ 15 degrees', MOTION], + ['turn ↺ 15 degrees', MOTION], + ['go to random position', MOTION], + ['go to x: 55 y: -50', MOTION], + ['glide 1 secs to x: 55', MOTION], + ['point in direction 90', MOTION], + ['change x by 10', MOTION], + ['set x to 55', MOTION], + ] + return ( <> - - - {/* Stage on top, as the editor lays out when the window is narrow. */} - - - - - - {/* Two sprites having the conversation most projects are built around. */} - - - - - - - - - {/* Sprite tray. */} + + + {/* ---- Stage, on top, as the editor lays out when the window is narrow. */} + + {/* Green flag and stop, and the layout controls beside them. */} + + + + + + {/* The stage itself: a road scene with a title card, which is what a + Code Club Scratch starter almost always looks like. */} + + + + + + + {/* Two sprites on the road. */} + + + + + + {/* Sprite tray. Neil is selected, so his tile carries the ring. */} + {[0, 1, 2, 3].map((i) => ( ))} - - - {/* Category rail. */} - - {BLOCK_COLOURS.map((colour, i) => ( - - - + {/* Stage / Backdrops column. */} + + + + + {/* ---- Code / Costumes / Sounds. */} + + + + Code + + + Costumes + + + Sounds + + + {/* ---- Category rail: the nine block groups, in the real order. */} + + {/* Nine categories have to fit 174 units without "My Blocks" dropping off + the bottom or "Operators" spilling out of a 30-wide rail. */} + {CATEGORIES.map(([name, colour], i) => ( + + + + {name} + ))} - {/* Palette of draggable blocks. */} - - - {[0, 1, 2, 3, 4, 5].map((i) => ( - + {/* ---- Palette. */} + + + + Motion + + {palette.map(([label, colour], i) => ( + + + + {label} + + ))} - {/* Script area, with one stack snapped together. */} - + {/* ---- Workspace, with one stack snapped together. The blocks are the + ones the instructions panel is asking for. */} + + - {[0, 1, 2, 3, 4].map((i) => ( - + + when ⚑ clicked + + {[ + ['go to x: 0 y: -40', MOTION], + ['point in direction 90', MOTION], + ['show', LOOKS], + ].map(([label, colour], i) => ( + + + + {label} + + ))} From 355497ce872bbf3a1df0c2829214c906f6376969 Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Tue, 15 Sep 2026 08:30:05 +0100 Subject: [PATCH 6/7] Give the editor the whole window, and redraw the still landscape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The editor is the entire page in the real product — no margin, nothing else on screen beside it. Here it was a panel in a 1100px column, and in the workbench an 800px card, which left the code pane a portrait sliver about 290 wide. Selected with `:has(.editor)` rather than a new layout prop, so it applies wherever an editor is rendered — including the three prototypes whose folders this change must not reach into. The surface breaks out to the viewport width and takes 100vh; full screen stops measuring the content column, which would otherwise pin the editor into a 1100px strip on a wide monitor. Two sizing bugs fixed on the way: - `.editor-main` had no `min-height: 0`, so the grid row grew to fit the canvas instead of the canvas fitting the row. - `.editor-canvas-art` set only a width. An SVG given one dimension reports an intrinsic height from its own aspect ratio, so the pane pushed 800px tall inside a 640px editor and the footer went off the bottom. Both dimensions now, and it crops. With the pane landscape, the portrait argument for the blocks still is gone, so it is redrawn in the real editor's own arrangement: categories and palette left, workspace centre, stage and sprite tray right. The tray is named and drawn rather than left as blank tiles — a Scratch starter always arrives with its cast in place. Co-Authored-By: Claude Opus 5 --- src/kit/EditorCanvas.tsx | 290 ++++++++++++++++++++++----------------- src/styles/global.css | 49 ++++++- 2 files changed, 208 insertions(+), 131 deletions(-) diff --git a/src/kit/EditorCanvas.tsx b/src/kit/EditorCanvas.tsx index 891ca38..a4874b4 100644 --- a/src/kit/EditorCanvas.tsx +++ b/src/kit/EditorCanvas.tsx @@ -7,19 +7,20 @@ * * The blocks half is drawn from a screenshot of the live editor (the Neil the * Seal starter on Code Club Projects), so the parts a mentor would recognise - * are the real parts: Scratch's own category colours in the real order, real - * Motion block labels, the Code/Costumes/Sounds tabs, a stage over a sprite - * tray, and a snapped-together stack of the blocks the instructions ask for. + * are the real parts: Scratch's own nine category colours in the real order + * with their labels, real Motion block labels, the Code/Costumes/Sounds tabs, + * the workspace, and a stage over a sprite tray with the selected sprite + * ringed. * * It still does not pretend to work — nothing here is clickable. The aim is * that it reads as THIS editor rather than as some editor, because in a * stakeholder review a generic grey rectangle is what gets noticed. * - * Drawn PORTRAIT, because the editor pane in this workbench is portrait - * (roughly 290x510) — the real editor is wide, but art drawn wide letterboxes - * into a tall pane and a still with bars down the side reads as a broken - * image. So it follows the narrow-window layout the real editor uses, with the - * stage above the palette rather than beside it. + * Drawn LANDSCAPE, in the real editor's own arrangement: categories and + * palette on the left, workspace in the middle, stage and sprites on the + * right. That follows the pane, which is landscape now the editor takes the + * whole window. It crops rather than letterboxes if the pane is a different + * shape. */ interface Props { @@ -28,12 +29,21 @@ interface Props { label: string } -/* Scratch's own category colours, so a mentor who knows Scratch recognises - the palette at a glance rather than reading it. */ +/* Scratch's own category colours, so a mentor who knows Scratch recognises the + palette at a glance rather than reading it. */ const MOTION = '#4C97FF' const LOOKS = '#9966FF' const SYNTAX = ['#C678DD', '#61AFEF', '#98C379', '#E5C07B', '#56B6C2'] +/* The starter's cast, as the tray shows it. */ +const SPRITES = [ + { name: 'Neil', colour: '#7E8C99', round: true }, + { name: 'Ranger', colour: '#E8A33D', round: true }, + { name: 'Sign', colour: '#EC5959', round: false }, + { name: 'Car', colour: '#4C97FF', round: false }, + { name: 'Barrier', colour: '#FF8C1A', round: false }, +] + const CATEGORIES: Array<[string, string]> = [ ['Motion', MOTION], ['Looks', LOOKS], @@ -50,7 +60,7 @@ export function EditorCanvas({ language, label }: Props) { return ( = [ - ['move 10 steps', MOTION], - ['turn ↻ 15 degrees', MOTION], - ['turn ↺ 15 degrees', MOTION], - ['go to random position', MOTION], - ['go to x: 55 y: -50', MOTION], - ['glide 1 secs to x: 55', MOTION], + const palette = [ + 'move 10 steps', + 'turn ↻ 15 degrees', + 'turn ↺ 15 degrees', + 'go to random position', + 'go to x: 55 y: -50', + 'glide 1 secs to x: 55', + 'point in direction 90', + 'point towards mouse-pointer', + 'change x by 10', + 'set x to 55', + 'change y by 10', + ] + + /* The stack the instructions panel beside this is asking for. */ + const stack: Array<[string, string]> = [ + ['go to x: 0 y: -40', MOTION], ['point in direction 90', MOTION], - ['change x by 10', MOTION], - ['set x to 55', MOTION], + ['show', LOOKS], ] return ( <> - - - {/* ---- Stage, on top, as the editor lays out when the window is narrow. */} - - {/* Green flag and stop, and the layout controls beside them. */} - - - - - - {/* The stage itself: a road scene with a title card, which is what a - Code Club Scratch starter almost always looks like. */} - - - - - - - {/* Two sprites on the road. */} - - - - - - {/* Sprite tray. Neil is selected, so his tile carries the ring. */} - - {[0, 1, 2, 3].map((i) => ( - - ))} - {/* Stage / Backdrops column. */} - - - + {/* ---- Code / Costumes / Sounds. */} - - - + + + Code - + Costumes - + Sounds - {/* ---- Category rail: the nine block groups, in the real order. */} - - {/* Nine categories have to fit 174 units without "My Blocks" dropping off - the bottom or "Operators" spilling out of a 30-wide rail. */} + {/* ---- Category rail. */} + {CATEGORIES.map(([name, colour], i) => ( - - + + {name} ))} {/* ---- Palette. */} - - - + + + Motion - {palette.map(([label, colour], i) => ( + {palette.map((label, i) => ( - - + + {label} ))} - {/* ---- Workspace, with one stack snapped together. The blocks are the - ones the instructions panel is asking for. */} - - + {/* ---- Workspace. */} + + - + when ⚑ clicked - {[ - ['go to x: 0 y: -40', MOTION], - ['point in direction 90', MOTION], - ['show', LOOKS], - ].map(([label, colour], i) => ( + {stack.map(([label, colour], i) => ( - + {label} ))} + {/* Zoom controls, bottom right of the workspace as in the real editor. */} + {[0, 1, 2].map((i) => ( + + ))} + + {/* ---- Stage and sprites. */} + + + {/* Green flag, stop, and the layout toggles. */} + + + + + + {/* The stage: a road scene under a title card, which is what a Code Club + Scratch starter almost always looks like. */} + + + + + + + + + + + + {/* Selected sprite's x and y, which the real panel shows under the stage. */} + + + Neil + + + x 55 + + + y -50 + + + {/* Sprite tray. The first is selected, so it carries the ring. Named and + drawn rather than left as empty tiles — a Scratch starter always + arrives with its cast already in place, and blank squares read as a + project nobody has set up. */} + + {SPRITES.map(({ name, colour, round }, i) => { + const x = 236 + (i % 3) * 33 + const y = 148 + Math.floor(i / 3) * 34 + return ( + + + {round ? ( + + ) : ( + + )} + + {name} + + + ) + })} + {/* Add sprite / add backdrop. */} + + ) } @@ -201,39 +244,38 @@ function TextEditor() { /* Indentation and token widths are fixed, not random: a picture of code should look like code, and real code has structure. */ const lines = [ - [0, [30, 52]], - [0, [22, 40, 24]], - [0, []], [0, [34, 58]], - [1, [26, 44, 22]], - [1, [40, 30]], + [0, [24, 44, 26]], [0, []], - [0, [24, 38]], - [1, [32, 50, 26]], - [2, [40, 24]], - [2, [28, 46]], - [1, [34, 22]], + [0, [38, 62]], + [1, [28, 48, 24]], + [1, [44, 32]], [0, []], - [0, [26, 42, 20]], + [0, [26, 42]], + [1, [36, 54, 28]], + [2, [44, 26]], + [2, [30, 50]], + [1, [38, 24]], + [0, []], + [0, [28, 46, 22]], ] as const return ( <> - + {/* Tab bar, so it reads as an editor rather than a terminal. */} - - - + + + - {/* Gutter. */} - + {/* Gutter and code. */} + {lines.map((_, i) => ( - + ))} - {lines.map(([indent, tokens], row) => { - let x = 30 + indent * 12 + let x = 28 + indent * 13 return ( {tokens.map((width, i) => { @@ -241,7 +283,7 @@ function TextEditor() { ) })} + - {/* Cursor, parked at the end of the last line. */} - - - {/* Output pane. */} - - - - {[56, 88, 44, 70].map((width, i) => ( - + {/* Output beside the code, which is where it sits at this width. */} + + + + {[64, 92, 48, 78, 56].map((width, i) => ( + ))} ) diff --git a/src/styles/global.css b/src/styles/global.css index 8f84a15..d91c071 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -891,18 +891,47 @@ hr.divider { } /* ---------------- Project editor (two pane) ---------------- */ +/* The editor IS the page, edge to edge and the full height of the window — + the real one has no margin and nothing else on screen beside it. + Selected with :has() rather than a layout prop so it applies wherever an + editor is rendered, including in prototypes this rule cannot reach into. */ +.surface:has(.editor) { + max-width: none; + height: 100vh; + /* Breaks out of whatever column it was placed in — the workbench card, the + gallery list — so the editor is the size of the window there too, not just + in full screen. Without this it inherits an 800px card and the editor gets + a portrait sliver to draw in. */ + width: 100vw; + margin-inline: calc(50% - 50vw); + border-radius: 0; + border-inline: none; +} +.surface:has(.editor) > .surface-body { + display: flex; + padding: 0; + /* flex-basis 0 with min-height 0, or the body refuses to shrink below its + content and the footer is pushed off the bottom. */ + flex: 1 1 0; + min-height: 0; +} +/* Full screen normally measures the content column; the editor is the one page + that must not be, or it sits in a 1100px strip on a wide monitor. */ +body.full-screen-prototype .surface:has(.editor) > .surface-body { + max-width: none; + margin-inline: 0; +} + .editor { display: grid; /* Stacked on small screens; rail, instructions and editor side by side once there is room, which is how the real editor lays out. */ grid-template-columns: 1fr; gap: var(--space-2); - /* The editor IS the page it is on, not a panel sitting in the middle of one. - Sized from the viewport so it fills a large monitor, with 420px kept as a - floor for short windows. `flex: 1` alone would not do it: in the workbench - the surface is only as tall as its content, so there is no spare height to - grow into. */ - min-height: max(420px, 68vh); + flex: 1 1 auto; + min-width: 0; + /* Fills the body it was given. The 420px floor stays for short windows. */ + min-height: max(420px, 100%); } @media (min-width: 860px) { .editor { @@ -992,6 +1021,10 @@ hr.divider { background: var(--rpf-white); border: 1px solid var(--rpf-grey-150); border-radius: var(--radius-sm); + /* Or the grid row grows to fit the canvas instead of the canvas fitting the + row, and the editor spills past the bottom of the window. */ + min-height: 0; + overflow: hidden; } @media (min-width: 860px) { .editor-main { @@ -1048,9 +1081,13 @@ hr.divider { /* The drawn editor fills its pane and crops rather than letterboxing — a still of an editor with bars down the side reads as a broken image. */ +/* Both dimensions set, so the SVG fills the pane and crops. Given only a + width it reports an intrinsic height from its own aspect ratio and pushes + the pane taller than the window. */ .editor-canvas-art { flex: 1 1 auto; width: 100%; + height: 100%; min-height: 0; border-radius: var(--radius-sm); border: 1px solid var(--rpf-grey-150); From e4fe3464e697a8660eeb6608f13b919dbb91f01d Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Tue, 15 Sep 2026 08:40:55 +0100 Subject: [PATCH 7/7] Stop the editor collapsing to a sliver in Code Classroom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Viewing project code for a Scratch project gave the editor about a third of the window. Three separate causes, all of them mine from the last commit. The wrappers. `ClassroomProjectEditor` puts its editor inside a `.section-stack`, and a prototype wraps the screen in another one. A flex item with no grow shrinks to its content, so each unstretched wrapper took the editor further from the window width — 1024 down to 679, then the grid's 320px instructions column left the code pane 359. Stretching only the body's direct child fixed one of two levels; `:has(.editor)` matches every wrapper between the body and the editor however many there are. The height. `.editor` had `min-height: max(420px, 100%)`, and the 100% resolved against the wrapper and then grew past it, so the editor stood 805px tall in a 768px window and clipped its own palette. Height comes from the flex parent now, with 420px kept only as a floor. The drawing. An SVG in normal flow reports a height from its aspect ratio whenever its parent has no definite one — `height: 100%` against an indefinite parent resolves to exactly that — so the canvas grew to fit the picture instead of the picture fitting the canvas. It is positioned out of flow now, with both dimensions explicit. With those fixed the pane turned out to be about 2:1, and the drawing was 1.17:1, so `slice` was cutting the tabs and half the sprite tray while `meet` left it adrift in the middle. The drawing is 560x290 now, the workspace taking the extra width as it does in the real editor, and fitted rather than cropped so a pane of any shape shows all of it. Co-Authored-By: Claude Opus 5 --- src/kit/EditorCanvas.tsx | 39 ++++++++++++++++++++------------ src/styles/global.css | 49 ++++++++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/kit/EditorCanvas.tsx b/src/kit/EditorCanvas.tsx index a4874b4..73cf037 100644 --- a/src/kit/EditorCanvas.tsx +++ b/src/kit/EditorCanvas.tsx @@ -60,8 +60,13 @@ export function EditorCanvas({ language, label }: Props) { return ( - + {/* ---- Code / Costumes / Sounds. */} - + Code @@ -139,8 +144,9 @@ function BlocksEditor() { ))} - {/* ---- Workspace. */} - + {/* ---- Workspace. The widest part of the real editor, and the part that + takes the extra room when the window is wide. */} + {/* Zoom controls, bottom right of the workspace as in the real editor. */} {[0, 1, 2].map((i) => ( - + ))} + {/* ---- Stage and sprites, moved to the right of the wider workspace. + Translated rather than recoordinated, so the panel stays one + readable block. */} + {/* ---- Stage and sprites. */} @@ -236,6 +246,7 @@ function BlocksEditor() { {/* Add sprite / add backdrop. */} + ) } @@ -262,10 +273,10 @@ function TextEditor() { return ( <> - + {/* Tab bar, so it reads as an editor rather than a terminal. */} - + @@ -300,11 +311,11 @@ function TextEditor() { {/* Output beside the code, which is where it sits at this width. */} - - - - {[64, 92, 48, 78, 56].map((width, i) => ( - + + + + {[96, 132, 72, 112, 84].map((width, i) => ( + ))} ) diff --git a/src/styles/global.css b/src/styles/global.css index d91c071..bb8afcb 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -915,6 +915,19 @@ hr.divider { flex: 1 1 0; min-height: 0; } +/* Every wrapper between the body and the editor has to stretch too, however + many there are. `:has(.editor)` matches exactly those ancestors and nothing + else. A flex item with no grow shrinks to its content, so a single + unstretched wrapper is enough to leave the editor a fraction of the window — + and there can be more than one: a screen wraps its editor in a + `.section-stack`, and a prototype wraps the screen in another. */ +.surface:has(.editor) > .surface-body :has(.editor) { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + min-height: 0; +} /* Full screen normally measures the content column; the editor is the one page that must not be, or it sits in a 1100px strip on a wide monitor. */ body.full-screen-prototype .surface:has(.editor) > .surface-body { @@ -930,8 +943,11 @@ body.full-screen-prototype .surface:has(.editor) > .surface-body { gap: var(--space-2); flex: 1 1 auto; min-width: 0; - /* Fills the body it was given. The 420px floor stays for short windows. */ - min-height: max(420px, 100%); + /* A floor for short windows, and nothing more. `100%` here resolved against + the wrapper and then grew past it, pushing the editor taller than the + window so the palette and stage were clipped instead of fitted. Height + comes from the flex parent now. */ + min-height: 420px; } @media (min-width: 860px) { .editor { @@ -1051,7 +1067,10 @@ body.full-screen-prototype .surface:has(.editor) > .surface-body { } .editor-canvas { flex: 1 1 auto; - padding: var(--space-2); + padding: 0; + /* The drawing is fitted rather than cropped, so this shows through around + it; matching its own base keeps the margin from reading as a gap. */ + background: #f9f9f9; /* So the thing standing in for the editor can stretch to the whole pane rather than sitting as a short box with dead space beneath it. */ display: flex; @@ -1081,16 +1100,22 @@ body.full-screen-prototype .surface:has(.editor) > .surface-body { /* The drawn editor fills its pane and crops rather than letterboxing — a still of an editor with bars down the side reads as a broken image. */ -/* Both dimensions set, so the SVG fills the pane and crops. Given only a - width it reports an intrinsic height from its own aspect ratio and pushes - the pane taller than the window. */ +/* Taken out of flow, so the drawing fills the pane instead of deciding how + tall it is. An SVG in normal flow reports a height from its own aspect + ratio whenever its parent has no definite one — and `height: 100%` against + an indefinite parent resolves to exactly that — which pushed the pane past + the bottom of the window and clipped the palette and stage. */ +.editor-canvas { + position: relative; +} .editor-canvas-art { - flex: 1 1 auto; - width: 100%; - height: 100%; - min-height: 0; - border-radius: var(--radius-sm); - border: 1px solid var(--rpf-grey-150); + position: absolute; + top: var(--space-2); + left: var(--space-2); + /* Explicit, not `inset` with `auto` sizes: an SVG has an intrinsic aspect + ratio, so it takes its height from its width and ignores the bottom inset. */ + width: calc(100% - 2 * var(--space-2)); + height: calc(100% - 2 * var(--space-2)); } /* Generated project artwork. Cropped to its box, like a real thumbnail. */