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/DESIGNS.md b/DESIGNS.md
index e680015..4d8e84b 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
@@ -25,5 +27,22 @@ 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
+ 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() {
/>
+
+
+
+
= [
+ ['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 (
+
+ )
+}
+
+function BlocksEditor() {
+ /* The Motion palette, in the order the real editor lists it. Real labels
+ rather than grey bars: at a glance the difference between "a picture of an
+ editor" and "a picture of THE editor" is mostly the words. */
+ 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],
+ ['show', LOOKS],
+ ]
+
+ return (
+ <>
+
+
+ {/* ---- Code / Costumes / Sounds. */}
+
+
+
+ Code
+
+
+ Costumes
+
+
+ Sounds
+
+
+ {/* ---- Category rail. */}
+
+ {CATEGORIES.map(([name, colour], i) => (
+
+
+
+ {name}
+
+
+ ))}
+
+ {/* ---- Palette. */}
+
+
+
+ Motion
+
+ {palette.map((label, i) => (
+
+
+
+ {label}
+
+
+ ))}
+
+ {/* ---- Workspace. The widest part of the real editor, and the part that
+ takes the extra room when the window is wide. */}
+
+
+
+
+ when ⚑ clicked
+
+ {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, moved to the right of the wider workspace.
+ Translated rather than recoordinated, so the panel stays one
+ readable block. */}
+
+ {/* ---- 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. */}
+
+
+
+ >
+ )
+}
+
+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, [34, 58]],
+ [0, [24, 44, 26]],
+ [0, []],
+ [0, [38, 62]],
+ [1, [28, 48, 24]],
+ [1, [44, 32]],
+ [0, []],
+ [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 and code. */}
+
+ {lines.map((_, i) => (
+
+ ))}
+ {lines.map(([indent, tokens], row) => {
+ let x = 28 + indent * 13
+ return (
+
+ {tokens.map((width, i) => {
+ const rect = (
+
+ )
+ x += width + 7
+ return rect
+ })}
+
+ )
+ })}
+
+
+ {/* Output beside the code, which is where it sits at this width. */}
+
+
+
+ {[96, 132, 72, 112, 84].map((width, i) => (
+
+ ))}
+ >
+ )
+}
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/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 (
+
+ )
+}
+
+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..a03e9a0 100644
--- a/src/kit/index.ts
+++ b/src/kit/index.ts
@@ -18,4 +18,7 @@ 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/README.md b/src/screens/README.md
index 758c023..b348b9b 100644
--- a/src/screens/README.md
+++ b/src/screens/README.md
@@ -32,6 +32,7 @@ chrome.
| `SchoolCodeEntry` | Pi Accounts, badged Code Classroom | Yes | Yes |
| `StudentSignIn` | Pi Accounts, badged Code Classroom | Yes | Yes |
| `MentorSignIn` | Pi Accounts | Yes | **No** — built from the pattern |
+| `EducatorSchoolHome` | Code Classroom | Yes | Yes |
| `EducatorClassPage` | Code Classroom | Yes | Yes |
| `EducatorProjectPage` | Code Classroom | Yes | Yes |
| `YoungPersonSchoolHome` | Code Classroom | Yes | Yes |
@@ -51,6 +52,18 @@ announced. The screens do not also mark the individual field — if a variant
wants that, pass a short message to the input's own `error` prop as well as
showing the alert.
+## Code Classroom pages are built as two bands
+
+A white block carrying the breadcrumb, the title, the description and the page
+actions, then the content below it on a warm off-white. `EducatorSchoolHome`
+and `EducatorClassPage` are built from the Code Editor Figma file and render
+`.cc-page-header` and `.cc-page-main` themselves rather than relying on the
+surface to pad them.
+
+If you write another Code Classroom screen, follow the same two blocks — a page
+that puts everything on one background will sit next to these in a flow and
+look like a different product.
+
## Code Classroom has no navigation
You move around Code Classroom entirely by breadcrumb — "Your school /
diff --git a/src/screens/ccp/ProjectEditor.tsx b/src/screens/ccp/ProjectEditor.tsx
index f5eedf0..a37d538 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, 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/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) {
)}
diff --git a/src/screens/classroom/ClassroomProjectEditor.tsx b/src/screens/classroom/ClassroomProjectEditor.tsx
index c531ed2..3b6203a 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, 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/screens/classroom/EducatorClassPage.tsx b/src/screens/classroom/EducatorClassPage.tsx
index 0290b74..4407dc3 100644
--- a/src/screens/classroom/EducatorClassPage.tsx
+++ b/src/screens/classroom/EducatorClassPage.tsx
@@ -13,6 +13,12 @@ interface Props {
classGroup: ClassGroup
projects: Project[]
memberCount: number
+ /**
+ * The blurb under the class title. Optional because a class need not have
+ * one — the designs show the space, and an empty class simply has no text
+ * there rather than a placeholder.
+ */
+ description?: string
onAddProject: () => 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. */}
+
+
-
- {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.
+
+
+ {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.
+
+ >
+ )
+}
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.
+
+ >
)
}
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) => (
-
- onOpenClass(group.id)}>
- {group.name}
-
-
- ))}
-
- )}
-
-
+ <>
+
+
+
{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. */}
+ onOpenClass(group.id)}>
+ {group.name}
+
+
+ {classesWithFeedback.includes(group.id) && (
+
+ )}
+
+ ))}
+
+ >
+ )}
+
+
+ >
)
}
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 075d918..bb8afcb 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,10 +822,30 @@ 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;
+}
+/* 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;
@@ -810,18 +891,63 @@ 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;
+}
+/* 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 {
+ 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;
+ /* 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 {
@@ -911,6 +1037,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 {
@@ -937,12 +1067,63 @@ hr.divider {
}
.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;
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. */
+/* 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 {
+ 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. */
+.project-thumb {
+ display: block;
+ border-radius: var(--radius-sm);
+ object-fit: cover;
+}
/* Project instructions markdown */
.md {
@@ -1331,3 +1512,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 (
- Raspberry Pi Foundation UK registered charity 1129409
- Help · Terms & Conditions · Safeguarding · Privacy
+ {surface.safeguardingFooter ? (
+ <>
+
+
+
+ flag
+
+ 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