From bbf7f48cb36db9bb2ddeaecdf77e459ae7ecbb1d Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 18:10:50 +0100 Subject: [PATCH 1/2] Narrow every modal by default The design system's modal is as wide as the viewport allows (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. Set as a repo default rather than copied per prototype. Two prototypes had already reached for the same --rpf-modal-max-inline-size override and the kit gallery wanted it too, so the alternative was everybody rediscovering the lever -- and it means import-yp gets a sane dialog without this reaching into another prototype's folder. out-to-projects-and-back drops its local copy. 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. Checked at 375px: unchanged there. Also excludes dialogs from the kit gallery's `.demo-stage > *` overflow guard. That guard was flattening the modal demo to the stage width, so with everything else narrowed the gallery would have advertised modals at twice the width they ever render. The guard still applies to form inputs and the rest. Co-Authored-By: Claude Opus 5 --- .../AddProjectChoice.tsx | 194 +++++++++--------- src/styles/global.css | 26 ++- 2 files changed, 119 insertions(+), 101 deletions(-) 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. */} -
- {onCreateStep ? ( - <> - { - setTyped(event.target.value) - setNameError(undefined) - }} - /> - +
+ {onCreateStep ? ( + <> + { + setTyped(event.target.value) + setNameError(undefined) + }} + /> + +

+ What kind of project do you want to make for your students? +

+ +
+ {PROJECT_TYPES.map((option) => ( + setType(option.id)} + /> + ))} +
+ + ) : ( + <> +

+ What kind of project do you want to add to {className}? +

+ + {nudge && (

- What kind of project do you want to make for your students? + Pick one of these to carry on.

- -
- {PROJECT_TYPES.map((option) => ( - setType(option.id)} - /> - ))} -
- - ) : ( - <> -

- What kind of project do you want to add to {className}? -

- - {nudge && ( -

- Pick one of these to carry on. -

- )} - -
- {ROUTES.map((option) => ( - { - setChoice(option.id) - setNudge(false) - }} - /> - ))} -
- - )} -
- -
+ )} + +
+ {ROUTES.map((option) => ( + { + setChoice(option.id) + setNudge(false) + }} + /> + ))} +
+ + )} +
+ ) } 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%; } From a3828ef88012d695f2fbb19c369ff9efe9ef670f Mon Sep 17 00:00:00 2001 From: Divya Mahadevan Date: Mon, 14 Sep 2026 18:11:02 +0100 Subject: [PATCH 2/2] Remove the two sample prototypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `onboarding-mentor/reference-manage-club-route` and `onboarding-yp/reference-school-code-join` were scaffolding — sample ideas to show the shape of a prototype until designers started building real ones. That has happened, so they go. They turned out to be load-bearing in the docs, which is the part worth knowing. Five references repointed: - README.md listed both as the worked examples to copy from. Now points at import-mentor/start-on-projects and import-yp/browse-projects-in-classroom, one per audience. - SETUP.md and src/prototypes/README.md used the school-code one in the ?full=1&autofill=0 example URL. Repointed to import-yp/browse-projects-in-classroom, which still has a school-code sign-in — so the surrounding text about watching someone type a six-digit code still means what it says. - The /new-prototype skill and _template/prototype.tsx both cited it as *the* worked example for deriving from the cast. Anyone running the skill would have been sent to a folder that no longer exists. Repointed to the same replacement, which uses that exact pattern. Left alone deliberately: dashboard-primary-action/notes.md references reference-manage-club-route twice, once as a testing instruction ("run it against ... with different mentors"). That is design reasoning about a comparison that no longer exists, and deciding what the prototype is for now is its owner's call, not a find-and-replace. onboarding-yp is now empty and shows "No prototypes yet", which lanes.ts says doubles as a visible to-do list. Co-Authored-By: Claude Opus 5 --- .claude/skills/new-prototype/SKILL.md | 2 +- README.md | 8 +- SETUP.md | 2 +- src/prototypes/README.md | 2 +- src/prototypes/_template/prototype.tsx | 2 +- .../reference-manage-club-route/meta.ts | 12 -- .../reference-manage-club-route/notes.md | 36 ----- .../reference-manage-club-route/prototype.tsx | 85 ---------- .../reference-school-code-join/meta.ts | 17 -- .../reference-school-code-join/notes.md | 38 ----- .../reference-school-code-join/prototype.tsx | 153 ------------------ 11 files changed, 9 insertions(+), 348 deletions(-) delete mode 100644 src/prototypes/onboarding-mentor/reference-manage-club-route/meta.ts delete mode 100644 src/prototypes/onboarding-mentor/reference-manage-club-route/notes.md delete mode 100644 src/prototypes/onboarding-mentor/reference-manage-club-route/prototype.tsx delete mode 100644 src/prototypes/onboarding-yp/reference-school-code-join/meta.ts delete mode 100644 src/prototypes/onboarding-yp/reference-school-code-join/notes.md delete mode 100644 src/prototypes/onboarding-yp/reference-school-code-join/prototype.tsx 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/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. -

-
-
-
- ) -}