diff --git a/.claude/skills/new-prototype/SKILL.md b/.claude/skills/new-prototype/SKILL.md
index 0f4397c..dc574e4 100644
--- a/.claude/skills/new-prototype/SKILL.md
+++ b/.claude/skills/new-prototype/SKILL.md
@@ -60,7 +60,7 @@ const CLUB = school(STUDENT.schoolId)!
Get this wrong and changing the cast produces a flow that fills in one club's
code and then rejects that club's usernames. See
-`onboarding-yp/reference-school-code-join` for the pattern.
+`import-yp/browse-projects-in-classroom` for the pattern.
See `#/debug` for everyone available.
diff --git a/README.md b/README.md
index 19ca8e2..4d707b4 100644
--- a/README.md
+++ b/README.md
@@ -39,10 +39,12 @@ Either way the prototype is discovered automatically and appears on the landing
page in its lane — **no shared file needs to change**, which is what lets
several people build at once without conflicting.
-Two worked examples to copy from, one per audience:
+Worked examples to copy from, one per audience:
-- `onboarding-mentor/reference-manage-club-route`
-- `onboarding-yp/reference-school-code-join`
+- `import-mentor/start-on-projects` — a mentor, signed out, browsing and
+ importing
+- `import-yp/browse-projects-in-classroom` — a young person, signing in with a
+ school code
Both are real click-throughs. See [`src/prototypes/README.md`](src/prototypes/README.md).
diff --git a/SETUP.md b/SETUP.md
index d289aaa..d2212ea 100644
--- a/SETUP.md
+++ b/SETUP.md
@@ -63,7 +63,7 @@ it is short, and the one rule in it matters.
Send them a link with `?full=1&autofill=0` on the end:
```
-#/p/onboarding-yp/reference-school-code-join?full=1&autofill=0
+#/p/import-yp/browse-projects-in-classroom?full=1&autofill=0
```
That hides everything belonging to the workbench and leaves the sign-in fields
diff --git a/src/prototypes/README.md b/src/prototypes/README.md
index ecc0b0c..46a7aed 100644
--- a/src/prototypes/README.md
+++ b/src/prototypes/README.md
@@ -57,7 +57,7 @@ you need to send:
For a testing session, both:
```
-#/p/onboarding-yp/reference-school-code-join?full=1&autofill=0
+#/p/import-yp/browse-projects-in-classroom?full=1&autofill=0
```
That opens clean, with nothing pre-typed — so you can watch someone read a
diff --git a/src/prototypes/_template/prototype.tsx b/src/prototypes/_template/prototype.tsx
index 559a166..6a31366 100644
--- a/src/prototypes/_template/prototype.tsx
+++ b/src/prototypes/_template/prototype.tsx
@@ -19,7 +19,7 @@ import { Surface } from '../../surfaces'
// and belongs in notes.md.
//
// Derive everything else from the cast — do not hardcode a club. See
-// onboarding-yp/reference-school-code-join for how.
+// import-yp/browse-projects-in-classroom for how.
//
// See #/debug for everyone available.
diff --git a/src/prototypes/import-mentor/out-to-projects-and-back/AddProjectChoice.tsx b/src/prototypes/import-mentor/out-to-projects-and-back/AddProjectChoice.tsx
index cfab9e2..4dcedda 100644
--- a/src/prototypes/import-mentor/out-to-projects-and-back/AddProjectChoice.tsx
+++ b/src/prototypes/import-mentor/out-to-projects-and-back/AddProjectChoice.tsx
@@ -1,4 +1,4 @@
-import { useState, type CSSProperties } from 'react'
+import { useState } from 'react'
import { Modal, TextInput } from '../../../kit'
import { PROJECT_TYPES, type ProjectTypeId } from './projectTypes'
@@ -143,107 +143,103 @@ export function AddProjectChoice({ isOpen, setIsOpen, className, onFind, onCreat
}
return (
- // The custom property inherits down to the dialog element, which is how a
- // narrower modal is possible without touching the shared stylesheet.
-
- (open ? setIsOpen(true) : close())}
- heading={onCreateStep ? 'Create a new project' : 'Add a project'}
- showCloseButton
- primaryButtonText={onCreateStep ? 'Create project' : 'Continue'}
- onClickPrimaryButton={onCreateStep ? create : forward}
- // "Back" rather than "Cancel" on step 2: it came from a question, so
- // returning to it is more use than dismissing. The header's X still
- // cancels, which is how the live dialog's two buttons stay two.
- secondaryButtonText={onCreateStep ? 'Back' : 'Cancel'}
- onClickSecondaryButton={onCreateStep ? () => setOnCreateStep(false) : close}
- >
- {/* The modal's content box centres its children, so this has to claim
+ (open ? setIsOpen(true) : close())}
+ heading={onCreateStep ? 'Create a new project' : 'Add a project'}
+ showCloseButton
+ primaryButtonText={onCreateStep ? 'Create project' : 'Continue'}
+ onClickPrimaryButton={onCreateStep ? create : forward}
+ // "Back" rather than "Cancel" on step 2: it came from a question, so
+ // returning to it is more use than dismissing. The header's X still
+ // cancels, which is how the live dialog's two buttons stay two.
+ secondaryButtonText={onCreateStep ? 'Back' : 'Cancel'}
+ onClickSecondaryButton={onCreateStep ? () => setOnCreateStep(false) : close}
+ >
+ {/* The modal's content box centres its children, so this has to claim
the full width or every row sits in the middle. */}
-
+
)
}
diff --git a/src/prototypes/onboarding-mentor/reference-manage-club-route/meta.ts b/src/prototypes/onboarding-mentor/reference-manage-club-route/meta.ts
deleted file mode 100644
index 60a823c..0000000
--- a/src/prototypes/onboarding-mentor/reference-manage-club-route/meta.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { PrototypeMeta } from '../../types'
-
-export const meta: PrototypeMeta = {
- title: 'Code Classroom via Manage Club',
- owner: 'Divya',
- hypothesis:
- 'A mentor will find Code Classroom if it sits in the Manage Club menu they already use — but it will read as club admin rather than something for a session.',
- status: 'explored',
-
- // Jo mentors both community clubs, so "which club?" is a real question here.
- cast: { piAccount: 'pi-mentor-jo' },
-}
diff --git a/src/prototypes/onboarding-mentor/reference-manage-club-route/notes.md b/src/prototypes/onboarding-mentor/reference-manage-club-route/notes.md
deleted file mode 100644
index 0bd0ed7..0000000
--- a/src/prototypes/onboarding-mentor/reference-manage-club-route/notes.md
+++ /dev/null
@@ -1,36 +0,0 @@
-A reference prototype: built to show the pattern, not because this is the
-answer. Copy it, argue with it, delete it.
-
-## What question is this answering?
-
-*How do we support mentors to set up Code Classroom for their clubs?* —
-specifically, the cheapest possible version: add a row to a menu that already
-exists.
-
-## Why this approach
-
-It is the lowest-effort route to build and the easiest to get wrong, so it is
-worth seeing early. If a seventh row in Manage Club is enough, we should know
-that before designing anything more elaborate.
-
-What I am betting on: mentors already go to Manage Club, so discovery is free.
-
-What I am betting against: Manage Club is club administration — events,
-volunteer lists, subscriber lists, profile editing. Nothing in it is about
-running a session. A mentor arriving with "what shall we do on Thursday?" is
-not in this menu, and a row here may be found only by people already looking
-for it.
-
-## What I'd want to watch in testing
-
-Ask a mentor to set up Code Classroom without telling them where it is. Watch
-whether they go to Manage Club at all, or look on the dashboard, or go to Code
-Club Projects, or search. Where they look first is the finding — not whether
-they can complete it once pointed at the row.
-
-Also worth watching: whether "Code Classroom" means anything to them as a
-label. It is a product name, not a description of what it does.
-
-## What we learned
-
-Not yet tested.
diff --git a/src/prototypes/onboarding-mentor/reference-manage-club-route/prototype.tsx b/src/prototypes/onboarding-mentor/reference-manage-club-route/prototype.tsx
deleted file mode 100644
index e0b1f38..0000000
--- a/src/prototypes/onboarding-mentor/reference-manage-club-route/prototype.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { useState } from 'react'
-import { Alert, Button, Card } from '../../../kit'
-import { schoolsForMentor } from '../../../fixtures'
-import { ManageClub, MentorDashboard, liveManageClubItems } from '../../../screens'
-import { meta } from './meta'
-import { Surface } from '../../../surfaces'
-
-// A mentor, signed in on codeclub.org. Unambiguous — unlike the young person
-// lanes, there is only one identity a mentor can hold.
-
-type Step = 'dashboard' | 'manage' | 'setup'
-
-export default function ManageClubRoute() {
- const [step, setStep] = useState('dashboard')
-
- // Derived from the cast in meta.ts, not hardcoded — change the cast and
- // this follows. Jo runs two clubs, so this takes the first of hers.
- const club = schoolsForMentor(meta.cast?.piAccount ?? '')[0]
-
- if (step === 'setup') {
- return (
-
-
-
-
- )
- }
-
- if (step === 'manage') {
- return (
-
- {}),
- // The proposal: one more row. `proposed` marks it as not real.
- {
- label: 'Code Classroom',
- tone: 'blue',
- proposed: true,
- onSelect: () => setStep('setup'),
- },
- ]}
- onBack={() => setStep('dashboard')}
- />
-
- )
- }
-
- return (
-
- {}}
- onCreateEvent={() => {}}
- onManageClub={() => setStep('manage')}
- onViewPublicProfile={() => {}}
- onStartAClub={() => {}}
- onFindAClub={() => {}}
- />
-
- )
-}
diff --git a/src/prototypes/onboarding-yp/reference-school-code-join/meta.ts b/src/prototypes/onboarding-yp/reference-school-code-join/meta.ts
deleted file mode 100644
index adb3014..0000000
--- a/src/prototypes/onboarding-yp/reference-school-code-join/meta.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { PrototypeMeta } from '../../types'
-
-export const meta: PrototypeMeta = {
- title: 'Joining with a school code',
- owner: 'Divya',
- hypothesis:
- 'A young person can get into Code Classroom from a six-digit code on a board — but the language and the code format will cost more time than the sign-in itself.',
- status: 'explored',
-
- // This story is about Siobhán, at the Westlands club. Change this one line to
- // tell it about someone else — the flow reads her club and code from here,
- // so nothing else needs touching.
- cast: { classroomStudent: 'cs-siobhan' },
-
- // The flow signs her in itself, so it starts signed out on purpose.
- ownsSignIn: true,
-}
diff --git a/src/prototypes/onboarding-yp/reference-school-code-join/notes.md b/src/prototypes/onboarding-yp/reference-school-code-join/notes.md
deleted file mode 100644
index 1615927..0000000
--- a/src/prototypes/onboarding-yp/reference-school-code-join/notes.md
+++ /dev/null
@@ -1,38 +0,0 @@
-A reference prototype: built to show the pattern, not because this is the
-answer. Copy it, argue with it, delete it.
-
-## What question is this answering?
-
-*How do we support young people accessing Code Classroom for the first time in
-a Code Club?* — walked through exactly as it works today, with nothing
-improved, so we can see what the baseline actually costs.
-
-## Why this approach
-
-Before designing anything, it is worth watching the real route end to end. Every
-screen here exists today. If it turns out to be fine, several proposed variants
-become unnecessary.
-
-What I expect to go wrong, in order:
-
-1. **"Are you a teacher or a student?"** — a club member is neither. In a
- library on a Saturday, "student" may not obviously mean them.
-2. **The school code is six digits.** `48-21-06` read off a board and typed by a
- nine-year-old, with hyphens they may or may not include. Not memorable, not
- guessable, and a single transposition sends them to an error.
-3. **The sign-in screen shows the code, not the club name.** "School code:
- 48-21-06" does not tell a young person they are in the right place.
-4. **Google sign-in sits on the same screen.** A club member has no school
- Google account, so half the screen is a question they cannot answer.
-
-## What I'd want to watch in testing
-
-Hand a young person a card with the code on it and say nothing else. Time it.
-Count how many times they look up for help. The mentor is the real interface
-here, and how often they are needed is the measure.
-
-Also: what happens with a typo. That path matters more than the happy one.
-
-## What we learned
-
-Not yet tested.
diff --git a/src/prototypes/onboarding-yp/reference-school-code-join/prototype.tsx b/src/prototypes/onboarding-yp/reference-school-code-join/prototype.tsx
deleted file mode 100644
index 7c808be..0000000
--- a/src/prototypes/onboarding-yp/reference-school-code-join/prototype.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-import { useEffect, useState } from 'react'
-import { Alert, Card } from '../../../kit'
-import { CLASSROOM_STUDENTS, classesInSchool, school, schoolBySchoolCode } from '../../../fixtures'
-import {
- RoleChooser,
- SchoolCodeEntry,
- StudentSignIn,
- YoungPersonSchoolHome,
-} from '../../../screens'
-import { Surface } from '../../../surfaces'
-import { useSession } from '../../../session'
-import { meta } from './meta'
-
-// Everything about this flow comes from the one young person named in meta.ts.
-// Nothing is hardcoded to a particular club, so changing the cast changes the
-// whole story — the code, the username, the club they land in.
-const STUDENT = CLASSROOM_STUDENTS.find((s) => s.id === meta.cast?.classroomStudent)!
-const CLUB = school(STUDENT.schoolId)!
-
-type Step = 'role' | 'code' | 'signin' | 'home'
-
-export default function SchoolCodeJoin() {
- const { autofill, classroomStudent, signInClassroomStudent } = useSession()
- const [step, setStep] = useState('role')
- const [code, setCode] = useState('')
- const [username, setUsername] = useState('')
- const [password, setPassword] = useState('')
- const [error, setError] = useState()
-
- // Fill in this young person's code and username, so a demo does not begin
- // with retyping six digits. Turn autofill off in the bar to watch someone
- // actually type it — which in a testing session is the point.
- useEffect(() => {
- setCode(autofill.schoolCode)
- setUsername(autofill.username)
- setPassword(autofill.password)
- }, [autofill.schoolCode, autofill.username, autofill.password])
-
- if (step === 'role') {
- return (
-
- setError('This walkthrough follows the young person route.')}
- onStudent={() => {
- setStep('code')
- setError(undefined)
- }}
- error={error}
- />
-
- )
- }
-
- if (step === 'code') {
- return (
-
- {
- setCode(value)
- setError(undefined)
- }}
- onContinue={() => {
- // Any real club's code gets you in — a young person who typed a
- // neighbouring club's code by mistake would get through to a
- // sign-in they cannot complete, which is worth seeing.
- if (schoolBySchoolCode(code)) {
- setStep('signin')
- setError(undefined)
- } else {
- setError(
- `Check the code and try again. It is six numbers, like ${CLUB.schoolCode}, and your mentor can tell you what it is.`,
- )
- }
- }}
- error={error}
- />
-
- )
- }
-
- if (step === 'signin') {
- const enteredClub = schoolBySchoolCode(code)
- return (
-
- {
- setStep('code')
- setError(undefined)
- }}
- onGoogleLogIn={() =>
- setError(
- 'A club member will not have a school Google account. This route is a dead end for clubs.',
- )
- }
- onLogIn={() => {
- // Matches on username within the club whose code was entered. The
- // password is never checked, because nothing here is real.
- const match = CLASSROOM_STUDENTS.find(
- (s) => s.username === username.trim() && s.schoolId === enteredClub?.id,
- )
- if (match) {
- signInClassroomStudent(match.id)
- setStep('home')
- setError(undefined)
- } else if (enteredClub && enteredClub.id !== CLUB.id) {
- setError(
- `That username is not at ${enteredClub.name}. The code you typed belongs to a different club.`,
- )
- } else {
- setError(
- `We do not recognise that username. Your mentor would have given you one — ${CLUB.name} uses names like ${STUDENT.username}.`,
- )
- }
- }}
- error={error}
- />
-
- )
- }
-
- const landedClub = classroomStudent ? school(classroomStudent.schoolId) ?? CLUB : CLUB
-
- return (
-
-
-
-
- Four screens, a six-digit code and a username. Count how much of that a young person
- could do without a mentor standing next to them.
-
-
- {}}
- />
-
-
- Signed in as {classroomStudent?.name ?? 'nobody'}. This account works
- only in Code Classroom — the same young person on Code Club Projects would be asked to
- log in or sign up all over again.
-
-
-
-
- )
-}
diff --git a/src/styles/global.css b/src/styles/global.css
index de7d917..6c08dde 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -40,6 +40,25 @@ button {
}
}
+/* ---------------- Design system defaults we override ---------------- */
+/* The modal is as wide as the viewport allows by default
+ (calc(100vw - 2 * --space-10)), which suits almost nothing here: every
+ dialog in this repo is a short question or a small form, and a question
+ stretched across a large monitor reads as a page that has gone wrong.
+ Narrowed to a measure a dialog can actually be read at.
+
+ Set here rather than per prototype. Two prototypes had already reached for
+ the same override and the kit gallery wanted it too; the alternative is
+ everybody rediscovering that --rpf-modal-max-inline-size is the lever.
+
+ The design system only applies this above 30rem — below that a separate
+ mobile variable keeps a dialog near full width, which is right on a phone —
+ so this changes nothing on small screens. A dialog that genuinely needs to
+ be wider can still set the property on any ancestor. */
+:root {
+ --rpf-modal-max-inline-size: 30rem;
+}
+
/* ---------------- App shell ---------------- */
/* Deliberately neutral chrome. This site is a hub for prototypes, not an
imitation of any one product, so it does not fake a Code Club or Code
@@ -366,8 +385,11 @@ hr.divider {
align-items: flex-start;
gap: var(--space-2);
}
-/* Form inputs and other block-level examples get their own line. */
-.demo-stage > * {
+/* Form inputs and other block-level examples get their own line.
+ Dialogs are excluded: they are overlaid rather than laid out in the stage,
+ and they carry their own measure — flattening them to the stage's width
+ made the gallery show a modal far wider than any modal in a prototype. */
+.demo-stage > *:not(dialog) {
max-width: 100%;
}