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. */} -
- {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/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%; }