docs(prd): learner-first mobile app PRD and interactive prototypes (#916)

* docs(prd): add learner-first mobile app PRD and interactive prototypes

Draft the ClassroomIO iOS/Android companion as a learner-first product
(attention home, compliance wallet, offline content, push on top of
prd/notification-system) with clickable HTML prototypes as the UX source
of truth.

Co-authored-by: Rotimi Best <[email protected]>

* style(prd): quiet mobile app prototype colours to match @cio/ui

Replace filled red/orange/green surfaces with the dashboard Badge and Alert recipes: 10% tint chips, card-background alerts, and default rows. Keep destructive fills only for actual destroy actions.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-08-18 12:02:35 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 368a7fe5ae
commit 967e81d33f
20 changed files with 6327 additions and 0 deletions
+1
View File
@@ -8,6 +8,7 @@ Use this file to keep a single prioritized view of all PRDs.
| --- | --- | --- | --- |
| - | - | `prd/super-admin` | draft |
| - | - | `prd/embed-link-platforms` | draft |
| - | - | `prd/mobile-app` | draft (blocked on `prd/notification-system` for push) |
| 1 | - | `prd/notification-system` | todo |
| 2 | 14 | `prd/course-templates` | todo |
| 3 | 5 | `prd/course-import` | todo |
+397
View File
@@ -0,0 +1,397 @@
# ClassroomIO Mobile App PRD
## Status
- Draft (prototyped, ready for product review)
## Prototypes — the UX source of truth
**The UX of this feature must be taken from the prototype folder.** Interactive HTML prototypes for every screen live in [`prototypes/mobile-app/`](../../prototypes/mobile-app/) and are the approved design reference for layout, states (locked / overdue / offline / graded / empty), copy, and navigation flow. When this document and a prototype disagree on a UI detail, the prototype wins.
**Start here:**
```
prototypes/mobile-app/index.html
```
Open it in a browser (`open prototypes/mobile-app/index.html`) — it maps both journeys (learner → tutor) and every screen cross-links so you can click through each flow end to end. Each page has a light/dark toggle, renders inside a 390 × 844 device frame, and shows the alternate states of that screen side by side.
The prototypes are built on the real design tokens from `packages/ui/src/index.css` (OKLCH palette, Geist, `--radius: 0.625rem`) via `app-theme.css`, so the app reads as the same product as the dashboard.
| Surface | Files |
| --- | --- |
| Entry | `index.html` (screen map), `auth.html` |
| Learner — core | `home.html`, `learn.html`, `course.html`, `lesson.html`, `exercise.html` |
| Learner — compliance & proof | `compliance.html`, `certificates.html` |
| Learner — engagement | `community.html`, `notifications.html` |
| Learner — platform | `downloads.html`, `profile.html` |
| Tutor companion | `tutor-grading.html`, `tutor-attendance.html`, `tutor-course.html` |
## Purpose
Ship a native iOS and Android app that makes ClassroomIO usable by the people companies actually train — frontline staff, field technicians, distributed employees, partners, and customers — who often have no work laptop and unreliable connectivity. The app is a **learner-first companion**, not a port of the dashboard: it optimizes for "open the app → see what needs my attention → finish it," with a small tutor mode for grading and attendance on the go. Reference experiences: TalentLMS (offline company training), Canvas Student (learner productivity), Teachable (polished consumption).
## Problem Statement
- ClassroomIO is browser-only. A warehouse worker, retail associate, or field technician with no company laptop has no practical way to complete assigned compliance training.
- There is **no push channel**. Deadline, renewal, and assignment nudges exist only as email (`packages/email/src/emails/`), which is the wrong medium for staff who do not check work email daily. Overdue compliance is discovered by the admin, not prevented by the learner.
- There is **no offline path**. Lessons, video (HLS), and exercises all require a live connection, so training cannot happen on a plane, in a basement, on a shop floor, or in a low-coverage region.
- Mobile web is a poor substitute for the parts that matter: background audio, lock-screen video controls, camera/file submissions, biometric re-entry, and a home-screen icon that pulls a learner back in.
- Compliance status — the product's core value — is buried in a dashboard the learner rarely opens. There is no "am I compliant, what expires next, where is my certificate" surface in the learner's pocket.
- Competitors treat mobile as table stakes: Moodle, Canvas, Blackboard, TalentLMS, Thinkific, and Teachable all ship apps, and every one of them offers push notifications; all but Thinkific offer offline content.
## Confirmed Decisions
These were locked from the mobile brainstorm that preceded this PRD. Items marked **(product call)** were decided without an explicit answer and are the ones to veto first.
1. **One role-aware app, not two.** A single binary serves learners and tutors; the tab bar and course screens change by role. Canvas ships separate Student/Teacher apps, but ClassroomIO's tutor surface is far smaller (grade, attend, announce, moderate) and its orgs are smaller, so two store listings would be overhead without benefit. **(product call)**
2. **Learner scope ships first.** Tutor mode is a later release behind the same binary, not a v1 requirement.
3. **Authoring stays on the web.** No course/lesson/exercise creation, media manager, AI course builder, analytics configuration, billing, SSO setup, domains, or API keys on mobile. Admin-only surfaces are intentionally absent.
4. **Expo + React Native**, as a new `apps/mobile` workspace package. It shares `@cio/utils` validation and the Hono RPC types from `@cio/api/rpc-types`, so request/response shapes stay type-checked against the real API.
5. **No new BFF.** Mobile calls the existing API. Where mobile needs something the web does not (a home digest, a device token, a token-authenticated HLS URL), we add narrow endpoints to the existing routers rather than forking business logic.
6. **Auth is Better Auth via `@better-auth/expo`.** The repo already runs `better-auth@^1.4.18` (`packages/db/package.json`). The Expo client plugin stores the session cookie in `expo-secure-store` and handles OAuth deep links; the server adds the matching `expo()` plugin and the `classroomio://` scheme to `trustedOrigins` (`packages/db/src/auth.ts`). Email/password, Google, and enterprise SSO all work through it.
7. **Biometric lock is a re-entry gate, not an auth method.** Face ID / fingerprint unlocks an existing stored session; it never replaces the initial sign-in.
8. **Org context is explicit.** There are no subdomains on mobile, so the user picks an organization after sign-in and the app sends `cio-org-id` on every request, matching how the API authorizes today (`apps/api/src/middlewares/org-team-member.ts`).
9. **No purchasing in the app, in any storefront.** No course checkout, no plan upgrade, no AI token packs. Selling digital content in-app would trigger Apple's Guideline 3.1.1 in-app-purchase requirement outside the US storefront; ClassroomIO's B2B model (training is assigned, not bought by the learner) means we can sidestep IAP entirely. Paid self-serve enrollment stays on the web.
10. **Push notifications are a hard dependency on `prd/notification-system`.** That PRD builds the `notification` table and the `notify()` service; mobile adds a **push transport** (device token registry + Expo Push delivery) and a per-category preference screen on top of it. Mobile must not build a parallel notification system.
11. **Offline v1 is read-only content plus a queued write for completion.** Download lessons, video, audio, PDFs, and transcripts; mark-complete events queue locally and sync on reconnect. **Offline exercises and submissions are v2** — quiz integrity, grading, and conflict resolution need their own design (this is where Canvas and Blackboard also draw the line).
12. **Compliance is the differentiating home surface.** The app opens on what is due, overdue, expiring, or awaiting a retake, and the certificate wallet is a first-class tab-reachable screen — not a page buried under settings.
13. **Branding is dynamic, white-label is later.** v1 themes the app with the org's name, logo, and primary color at runtime (multi-org accounts re-theme on switch). Dedicated per-customer App Store listings are an enterprise follow-up, as Thinkific and TalentLMS both sell them.
14. **The app is cloud-first but self-host compatible.** The sign-in screen accepts a custom server URL so self-hosted deployments can point the same binary at their own API; single-org self-hosted installs skip the org picker.
## Current-State Audit
| Capability | Current state | Notes for mobile |
| --- | --- | --- |
| Native app | **None.** `apps/course-app` is a CLI generator for standalone SvelteKit course sites, not a client | Greenfield; no code to migrate |
| API surface | Hono app with ~24 mounted routers (`apps/api/src/app.ts`) | Reuse as-is; mobile is another client |
| Typed client | `hcWithType` + `InferRequestType`/`InferResponseType` (`apps/api/src/rpc-types.ts`) | Mobile imports the same types; no hand-written DTOs |
| Auth | Better Auth 1.4.18, cookie sessions, 30-day expiry, 1-hour cookie cache, `customSession` injecting `orgRoles` (`packages/db/src/auth.ts`) | Add `expo()` server plugin + scheme in `trustedOrigins`; `orgRoles` already rides the session so RBAC needs no extra call |
| Session endpoint | `GET /session` returns `{ session, user }` (`apps/api/src/app.ts`) | Cold-start hydration |
| Org RBAC | `cio-org-id` header → `orgRoles[orgId]`; ADMIN=1, TUTOR=2, STUDENT=3 (`packages/utils/src/constants/roles.ts`) | Drives the role-aware tab bar |
| Course delivery | `/course/:courseId/{content,section,lesson,exercise,submission,mark,attendance,newsfeed,members,compliance,ai-tutor,presign}` | Covers nearly all of the learner and tutor scope |
| Question types | 16 keys incl. `VIDEO_RECORDING`, `FILE_UPLOAD`, `HOTSPOT`, `MATCHING` (`packages/question-types/src/question-type-keys.ts`) | Each needs a touch renderer; camera/mic permissions required |
| Video | HLS proxied from R2, authorized by an **HMAC cookie** `classroomio_hls_<assetId>` **or** an org-role session (`apps/api/src/routes/hls/hls.ts`) | ⚠️ Native players can't carry per-asset cookies — needs a token-in-URL variant (see Technical Design) |
| Transcripts / media | `/transcripts`, `/media`, presigned uploads (`apps/api/src/routes/course/presign.ts`) | Presign is already the right shape for mobile uploads |
| Compliance | Deadlines, grace, renewals, waivers (`apps/api/src/routes/course/compliance.ts`; `prd/compliance-training-platform [DONE]`) | Data for the home surface exists; needs a learner-scoped aggregate |
| Certificates | `packages/certificates`, issuance + LMS certificates page | Wallet reads the same records |
| Community / newsfeed | `/community`, `/course/:id/newsfeed`, cohort newsfeed | Maps to the Community tab |
| Cohorts | `/cohort` with goals and deadlines | Surfaces as cohort cards + announcements |
| AI lesson tutor | `/course/:courseId/ai-tutor` with fair-use caps (`prd/ai-tutor-fair-use`) | Renders as a bottom sheet in the player |
| **Notifications** | ⚠️ **Not shipped.** `prd/notification-system` is todo (`PRD-TRACKER.md` rank 1); bell icon is a placeholder; only email exists | Hard dependency — see Decision 10 |
| **Offline** | ⚠️ None. No service worker, no sync queue | Built from scratch in the app |
| **Device tokens** | ⚠️ No table | New `device_token` table |
| Deployment modes | `PUBLIC_IS_SELFHOSTED` switches multi-org, licensing, billing (`FEATURE_AUDIT.md` §5) | App must handle both; see Decision 14 |
| Background jobs | BullMQ workers incl. a `notifications` worker (`apps/jobs`) | Push fan-out belongs here, not in the request path |
## Product Goals
1. A learner signs in (password, Google, or enterprise SSO), picks an org, and lands on a home screen that answers "what needs my attention today" in one glance.
2. A learner completes a full course on a phone: read lessons, watch video, take exercises, and get a certificate — without touching a browser.
3. A learner receives a push notification for an assignment, a deadline, a renewal, a grade, or a reply, and taps straight into the relevant screen.
4. A learner downloads a course, completes the lesson content on a plane, and sees their progress sync automatically on reconnect.
5. A learner can always answer "am I compliant, what expires next, where is my certificate" in two taps.
6. A tutor clears a grading queue, takes attendance, posts an announcement, and moderates a thread from a phone.
7. The web dashboard, self-hosted deployments, and existing API behavior are unchanged (zero regression).
## Non-Goals (v1)
- Course, lesson, or exercise **authoring**; media manager; AI course builder.
- Org administration: settings, team/roles, billing, SSO config, custom domains, API keys, widgets.
- **Any purchase flow** (see Decision 9) — no course checkout, plan upgrade, or AI token packs.
- **Offline exercises, quizzes, and submissions** (v2) — offline is read-only content plus queued completion.
- Live-class video conferencing inside the app (join links open the existing external provider).
- Direct messaging between users (ClassroomIO has no DM system today; community Q&A covers discussion).
- SCORM playback (`prd/scorm-support` is todo; not shipped on web either).
- Per-customer white-label App Store listings (enterprise follow-up, Decision 13).
- Tablet-optimized layouts, watch apps, widgets, offline video for public/unenrolled catalog courses.
---
## Functional Requirements
**Information architecture.** Five tabs, role-aware: learners get **Home · Learn · Compliance · Community · Profile**; ADMIN and TUTOR get **Teach** in place of Compliance. Notifications live behind the bell in the nav bar, and Certificates and Downloads are reached from Compliance and Profile.
### 1. Onboarding and authentication — `auth.html`
- **Sign-in**: email + password, "Continue with Google", and "Sign in with SSO" (email-domain discovery via `/sso`). Google and SSO open the system browser and return through the `classroomio://` deep link.
- **Self-hosted**: a "Use a custom server" affordance accepts an API base URL, validated before the credential step.
- **Org picker**: shown after sign-in when the account belongs to more than one org; each row shows the org logo, name, and role. Single-org accounts (including all self-hosted installs) skip it.
- **Biometric**: after the first successful sign-in the app offers Face ID / fingerprint for subsequent launches; declining falls back to the stored session, and a remote sign-out invalidates it.
- **States to build**: idle, invalid credentials, SSO-required-for-this-domain, offline (no network at launch → "you're offline, showing downloaded content" with a path to Downloads), and session-expired re-auth.
- Invitation and password-reset deep links open the app when installed and the web flow otherwise.
### 2. Home — `home.html`
The attention surface. Ordered by urgency, not by recency:
1. **Compliance alert band** — only when something is overdue or inside its grace period; red for overdue, amber for due soon; tapping opens Compliance.
2. **Continue learning** — the single most recent in-progress lesson with a thumbnail, course name, position ("Lesson 4 of 12"), and a progress bar.
3. **Due soon** — deadline-ordered list of exercises and required courses with relative dates ("Due in 2 days").
4. **Newly assigned** — courses assigned since the last open, with an enroll/start affordance.
5. **Cohort announcements** — latest newsfeed items from the learner's cohorts and courses.
6. **Recent feedback** — graded submissions with score and tutor comment preview.
States: fully-caught-up empty state ("Nothing due — you're all caught up"), first-run empty state (no enrollments), and offline state (cached digest with an "Offline — last updated 2h ago" chip).
### 3. Learn — `learn.html`, `course.html`
- **My Learning**: segmented control for In progress / Not started / Completed; rows carry a progress ring, course type chip (Compliance / Cohort / Self-paced), and a download indicator.
- **Explore**: the org's public catalog, enroll-in-place for free/assigned courses; no paid checkout (Decision 9).
- **Cohorts**: cohort cards with goal progress and deadline.
- **Course detail** (`course.html`): header with progress, then the section/lesson outline. Every lesson row shows its state — complete, current, locked (with the reason: "Complete Lesson 3 first"), or downloaded. Compliance courses show a deadline banner. Tabs: Content, Newsfeed, Community, Certificate, and — for compliance courses — Compliance.
- Locked content is enforced by the API, not just hidden (`apps/dashboard/src/lib/features/course/utils/content-lock-utils.ts` is the web equivalent).
### 4. Lesson player — `lesson.html`
- **Video**: HLS playback with captions, playback speed, AirPlay/Chromecast, picture-in-picture, background audio, and lock-screen controls. Position is remembered per lesson and synced across devices.
- **Text, slides, and documents** rendered natively; multilingual lessons respect the learner's chosen language (`lesson-language.ts`).
- **Transcript** in a bottom sheet, tap-to-seek where timestamps exist.
- **AI Lesson Tutor** in a bottom sheet, scoped to the current lesson, showing the fair-use cap state when exhausted (`prd/ai-tutor-fair-use`).
- **Mark complete** advances to the next item; when offline the action is queued and the row shows a "will sync" indicator.
- States: online, downloaded/offline, locked, video-unavailable-offline, and tutor-cap-reached.
### 5. Exercises — `exercise.html`
- Touch renderers for all 16 question types in `packages/question-types`, including `MATCHING` (drag), `ORDERING` (drag), `HOTSPOT` (tap target), `FILE_UPLOAD` (files/photo library/document scan), and `VIDEO_RECORDING` (in-app camera).
- Answers autosave locally as the learner works so a call or app switch never loses progress.
- Submission uses the existing presigned upload path for media (`apps/api/src/routes/course/presign.ts`) with a visible upload progress state and retry.
- Result view: score, per-question correctness where allowed, rubric, tutor feedback, and resubmit when the exercise permits it.
- States: unattempted, in progress, submitted/awaiting grade, graded/passed, graded/failed with retake, and **connection-required** (an explicit blocking state, since exercises are online-only in v1).
### 6. Compliance — `compliance.html`
- Status header: Compliant / Due soon / Overdue / In grace, with the count of requirements behind it.
- Per-requirement rows: course, status, deadline or renewal date, grace-period note, waiver note where one applies, and last completion date.
- Renewal countdown for certifications approaching expiry.
- A "Proof" action jumping to the matching certificate.
- States: all-clear, overdue, in-grace, waived, and no-requirements.
### 7. Certificates — `certificates.html`
- Wallet list of issued certificates with course, issue date, certificate ID, and expiry when applicable.
- Detail view: rendered certificate, share sheet (PDF), save to device, and a verification QR/link.
- Downloaded certificates remain viewable offline.
- States: populated, empty ("Finish a course to earn your first certificate"), and expiring-soon.
### 8. Community — `community.html`
- Org and course Q&A: ask, answer, upvote, and follow threads; course and cohort newsfeed posts with comments.
- A "For you" filter for mentions, replies, and followed threads.
- Compose supports text and image attachments.
- Author-or-team edit/delete rules mirror the API guards (`apps/api/src/routes/community/middlewares/`).
- States: populated, empty, offline (read cached, compose disabled with an explanatory chip).
### 9. Notifications — `notifications.html`
- **Center**: grouped Today / Earlier, unread dots, deep-link on tap, mark-all-read.
- **Categories** (each independently toggleable, push and in-app): new assignment, deadline reminder, overdue, renewal due, new lesson/newsfeed post, exercise graded, community mention or reply, certificate issued, cohort goal approaching.
- **Preferences**: per-category switches plus quiet hours; changes write through to the same preference store the web uses (`prd/email-notification-preferences`).
- The first launch asks for the OS notification permission with an explanatory pre-prompt, not a cold system dialog.
### 10. Downloads and offline — `downloads.html`
- Download a whole course or an individual lesson; per-item progress and cancel.
- Storage manager: total used, per-course breakdown, delete individual or all.
- Settings: Wi-Fi-only downloads (default on), auto-download newly assigned required training (default off).
- **Sync queue** panel: pending completion events with their state (queued / syncing / synced / failed-retry), and a manual "Sync now".
- A persistent offline banner appears app-wide when the device is offline.
- Content that cannot work offline (exercises, live classes, AI tutor, community compose) is visibly marked rather than silently broken — the failure mode TalentLMS solved with an authoring-side compatibility check.
### 11. Profile and settings — `profile.html`
- Profile summary, learning stats, and organization switcher (re-themes the app).
- Links to Certificates, Downloads, Notification preferences.
- App settings: language, theme (system/light/dark), video quality, biometric lock toggle.
- Account: change password (web link), sign out, delete account (web link).
### 12. Tutor mode — `tutor-grading.html`, `tutor-attendance.html`, `tutor-course.html`
Visible only when `orgRoles[activeOrg]` is ADMIN or TUTOR. Staff get **Teach** in place of the learner Compliance tab; their own compliance stays reachable from the Home alert band and Profile.
- **Grading queue**: submissions awaiting grading across courses, oldest first, with course/exercise/learner and waiting time. Review shows the learner's answers (including video/file submissions), the rubric, score entry, written or recorded feedback, and approve / request-resubmission.
- **Attendance**: roster for a live-class session with present/absent/late toggles, bulk actions, and a code or QR check-in.
- **Course snapshot**: enrollment, completion rate, overdue count; post an announcement; moderate community/newsfeed posts (hide, delete, mark answered).
- Explicitly absent: authoring, settings, people management beyond invite, analytics configuration.
### 13. Cross-cutting requirements
- **Accessibility**: full screen-reader labels, Dynamic Type, minimum 44 pt touch targets, and no color-only status encoding (every status carries an icon and text). Canvas advertises VPAT 2.2 conformance; we should not ship below that bar.
- **Localization**: the app reads the same translation catalog the dashboard uses (`apps/dashboard/src/lib/utils/translations/`), covering the 10+ supported languages; no hardcoded strings.
- **Frontline mode**: large-target layout, low-bandwidth media defaults, and QR-code course launch, aimed at shared or shop-floor devices.
- **Security**: session in `expo-secure-store`, biometric re-lock, no analytics of lesson content, and remote sign-out honored on next foreground.
---
## Technical Design
### Workspace
```
apps/mobile/ # @cio/mobile — Expo (React Native)
app/ # expo-router routes, grouped by tab
src/features/<domain>/ # mirrors the dashboard's feature folders
api/ # typed calls via @cio/api/rpc-types
components/
utils/types.ts # request/response types inferred from the API
src/lib/{auth,offline,push,i18n}/
```
`apps/mobile` joins the existing `apps/*` workspace glob (`pnpm-workspace.yaml`). It depends on `@cio/utils` (validation, constants, roles) and `@cio/api` (types only). It must **not** depend on `@cio/ui` (Svelte) or `@cio/db`.
### Data model additions (`packages/db/src/schema.ts`)
```
device_token
id uuid PK · profileId FK(profile, cascade) · token varchar NOT NULL
platform DEVICE_PLATFORM ('ios' | 'android')
provider PUSH_PROVIDER ('expo') -- room for direct FCM/APNs later
appVersion varchar · deviceName varchar · locale varchar
lastSeenAt timestamptz · disabledAt timestamptz nullable
createdAt / updatedAt
unique(token) · index(profileId) · index(profileId, disabledAt)
notification_preference -- extends prd/notification-system
id uuid PK · profileId FK(profile, cascade)
category NOTIFICATION_CATEGORY -- reuses the notification type enum
push boolean default true · inApp boolean default true · email boolean default true
unique(profileId, category)
profile_quiet_hours
profileId FK(profile) PK · startMinute smallint · endMinute smallint · timezone varchar
```
Notes, per repo rules: no relational IDs inside `jsonb`; nothing computed is stored. Download state, sync queue, and playback position live **on the device** (SQLite), not in Postgres — the server keeps only the authoritative completion records it already has. Last-read playback position reuses existing lesson progress rather than a new column.
### Video: token-authenticated HLS
Today `/hls/:assetId/*` authorizes on an HMAC cookie `classroomio_hls_<assetId>` **or** an org-role session (`apps/api/src/routes/hls/hls.ts`). A native player (AVPlayer / ExoPlayer) fetches manifests and segments outside the app's HTTP client, so neither mechanism is reliable on mobile.
```
GET /hls/:assetId/playback-token [authMiddleware + asset org membership]
→ { url: "/hls/<assetId>/master.m3u8?t=<hmac>", expiresIn: 3600 }
hlsRouter authorization order becomes:
1. query param `t` → verifyHlsToken (NEW — same HMAC, same claims)
2. cookie → verifyHlsToken (unchanged, web)
3. session orgRoles → membership check (unchanged)
```
The token is the existing HMAC payload; only the transport is new, so web behavior is untouched. Offline downloads fetch the segment set once and store it locally; expiry is enforced at download time, not playback time, so a downloaded lesson keeps working on a plane.
### Push delivery
Push rides on `prd/notification-system` — mobile adds a transport, not a second notification pipeline.
```
notify(...) # existing service (notification-system PRD)
├─ INSERT notification # in-app center
├─ enqueue email # existing
└─ enqueue push (NEW)
└─ apps/jobs notifications worker:
tokens = active device_token rows for profileId
skip if preference.push = false for category
skip / defer if inside quiet hours
chunk ≤100 → Expo Push API (expo-server-sdk)
on DeviceNotRegistered → set device_token.disabledAt
```
Payload carries `{ type, entityId, deepLink }` so the app can route without a fetch. Expo Push abstracts APNs and FCM behind one endpoint; the `provider` column leaves room to move to direct FCM/APNs if enterprise requirements demand it.
### Offline architecture
| Concern | Approach |
| --- | --- |
| Content cache | SQLite (`expo-sqlite`) for lesson/section/course metadata; file system for video segments, audio, PDFs, images |
| Download unit | Course or lesson; queued, resumable, cancellable; Wi-Fi-only by default |
| Write queue | Append-only local table of intents (`lesson.complete`, `playback.position`), each with an idempotency key |
| Sync | On reconnect and on foreground: replay queue oldest-first; `409`/duplicate responses are treated as success |
| Conflict rule | Completion is monotonic — once complete, always complete; the server value wins for everything else |
| Eviction | Manual delete only in v1 (no silent eviction of content a learner deliberately downloaded) |
Because v1 queues only completion and playback position — never grades or submissions — there is no merge conflict class that can corrupt an assessment record. That is exactly why offline exercises are deferred.
### API additions
All additions follow the repo layering (validation → queries → services → routes) and mount under existing root segments.
| Method | Path | Auth | Purpose |
| --- | --- | --- | --- |
| GET | `/account/home-digest` | auth + org | One call for the Home screen: continue-learning, due-soon, newly-assigned, announcements, feedback, compliance summary |
| GET | `/account/compliance` | auth + org | Learner-scoped compliance across courses (today's data is per-course) |
| POST | `/account/device-tokens` | auth | Register/refresh a push token |
| DELETE | `/account/device-tokens/:token` | auth | Deregister on sign-out |
| GET | `/account/notification-preferences` | auth | Category matrix + quiet hours |
| PUT | `/account/notification-preferences` | auth | Update |
| GET | `/course/:courseId/offline-manifest` | course member | Everything needed to download a course in one call: lessons, media URLs, sizes, checksums |
| POST | `/course/:courseId/sync` | course member | Batch-replay queued completion intents idempotently |
| GET | `/hls/:assetId/playback-token` | auth + asset org | Token-authenticated playback (above) |
| GET | `/account/grading-queue` | org team member | Tutor: submissions awaiting grading across courses |
`/account/home-digest` exists to avoid the 6–8 round trips a cold mobile launch would otherwise need on a slow connection; it is a read-only composition of existing services, not new business logic.
### Client plan
- **Auth**: `@better-auth/expo` client with `scheme: 'classroomio'` and `expo-secure-store`; server adds the `expo()` plugin and the scheme to `trustedOrigins` in `packages/db/src/auth.ts`. `orgRoles` already arrives on the session via `customSession`, so the role-aware tab bar needs no extra request.
- **Types**: every request/response type is inferred from `@cio/api/rpc-types` into `features/<domain>/utils/types.ts` — the same rule the dashboard follows. No hand-maintained mobile DTOs, and no importing from `@cio/db`.
- **Validation**: reuse the Zod schemas in `packages/utils/src/validation/`.
- **State**: TanStack Query with a SQLite-backed persister so cached screens render instantly offline.
- **i18n**: load the dashboard translation catalog; no literal user-facing strings in components.
### Build verification
```bash
export PATH="$HOME/.nvm/versions/node/v20.19.3/bin:$PATH"
pnpm --filter @cio/utils build && pnpm --filter @cio/db build && pnpm --filter @cio/api build
pnpm --filter @cio/mobile typecheck && pnpm --filter @cio/mobile test
pnpm --filter @cio/dashboard^... build && pnpm --filter @cio/dashboard build # regression
pnpm format:check
```
---
## Implementation Order
1. **Unblock notifications.** Ship `prd/notification-system` (table, `notify()`, in-app center). Mobile push has no foundation without it.
2. **Backend for mobile.** `device_token` + preference tables, push transport in the `apps/jobs` notifications worker, `/account/home-digest`, `/account/compliance`, HLS playback token, offline manifest, sync endpoint.
3. **App skeleton.** `apps/mobile` Expo project, auth (password → Google → SSO), org picker, role-aware tab bar, theming from org branding, i18n.
4. **Consumption path.** My Learning → course detail → lesson player (video, text, transcript) → mark complete → certificate. This is the first genuinely useful build.
5. **Assessment path.** All 16 question types, autosave, presigned media upload, results and resubmission.
6. **Compliance and certificates.** Home compliance band, compliance screen, certificate wallet with share/verify.
7. **Push end to end.** Token registration, categories, quiet hours, deep links from every notification type.
8. **Offline.** Download manager, storage settings, write queue, sync-on-reconnect, offline banners and blocked-state messaging.
9. **Engagement.** Community, newsfeed, cohorts, AI lesson tutor sheet.
10. **Tutor mode.** Grading queue, attendance, announcements, moderation.
11. **Store readiness.** Accessibility audit, App Store / Play listings, privacy nutrition labels, data-safety form, beta via TestFlight and Play internal testing.
Releases: 3–6 = Release 1 (learner core), 7–9 = Release 2 (push, offline, engagement), 10 = Release 3 (tutor), then white-label and microlearning.
## Acceptance Criteria
1. A learner can sign in with email/password, Google, and enterprise SSO; multi-org accounts choose an org and every request carries `cio-org-id`.
2. A self-hosted user can point the app at a custom API base URL and sign in; the single-org install skips the org picker.
3. Home shows compliance alerts, continue-learning, due-soon, newly-assigned, announcements, and feedback in one request, and renders a cached version offline with an explicit staleness chip.
4. A learner can complete an entire course on a phone — lessons, video, all 16 exercise question types, and certificate issuance — with progress matching the web dashboard exactly.
5. Video plays via HLS with captions, background audio, lock-screen controls, and PiP; the playback token authorizes a native player, and the existing cookie and session paths still work on web (regression check).
6. Push notifications arrive for every category in §9, respect per-category preferences and quiet hours, and deep-link to the right screen; disabling a category stops both push and in-app for that category.
7. A learner downloads a course, airplane-mode completes its lesson content, and every completion appears on the web dashboard after reconnect, with no duplicates on retry.
8. Exercises, AI tutor, and community compose show explicit "connection required" states offline rather than failing silently.
9. Compliance status, renewal dates, grace periods, and waivers match the web compliance view for the same learner.
10. A tutor sees the Teach tab only with ADMIN or TUTOR in the active org, and can grade a submission, take attendance, post an announcement, and moderate a post.
11. A student-role user has no path to authoring, org settings, billing, or admin data anywhere in the app; the API rejects such calls independently of the UI.
12. No purchase, upgrade, or checkout affordance exists anywhere in the app on any storefront.
13. Accessibility: screen-reader labels on all interactive elements, Dynamic Type honored, 44 pt minimum targets, no color-only status.
14. All user-facing copy comes from translation keys; the app runs in every language the dashboard supports.
15. Zero regression: dashboard, API, and self-hosted deployments behave exactly as before; all builds and `pnpm format:check` pass.
## Risks and Mitigations
- **Notification-system dependency slips.** Mobile's headline retention feature has no foundation. *Mitigation*: sequence it first (Implementation Order 1); if it slips, Release 1 ships without push and the app remains useful for consumption, but do not build a parallel notification path.
- **HLS auth on native players.** Cookie-based gating silently 403s in AVPlayer/ExoPlayer, which is easy to misdiagnose as a media bug. *Mitigation*: the playback-token endpoint is scheduled in Implementation Order 2, before any player work, and the web cookie path is left untouched.
- **Offline scope creep into assessments.** Offline quizzes bring conflict resolution, integrity, and grading-race problems. *Mitigation*: v1 queues only monotonic completion and playback position (Decision 11); revisit with its own PRD.
- **App Store rejection under Guideline 3.1.1.** Any in-app path to buying a course or plan triggers IAP obligations outside the US storefront. *Mitigation*: no purchase affordance at all (Decision 9), enforced by acceptance criterion 12; revisit only with a deliberate IAP design.
- **16 question types is a large surface.** Drag, hotspot, camera, and file types each carry native complexity. *Mitigation*: ship the legacy trio (`RADIO`, `CHECKBOX`, `TEXTAREA`) plus the common objective types first, gate uncommon types behind a "open in browser" fallback until their renderer lands, and never let an unsupported type silently render blank.
- **Storage exhaustion on low-end devices.** Downloaded video can fill a cheap Android phone. *Mitigation*: pre-download size estimate, a visible storage manager, Wi-Fi-only default, and a device-space check before each download.
- **Two clients drifting.** Mobile and web can diverge into different business rules. *Mitigation*: shared Zod validation, types inferred from the live API, and no mobile-only business logic — new capability lands in the API and both clients consume it.
- **Push cost and deliverability at scale.** Expo Push is a third-party hop in the delivery chain. *Mitigation*: the `provider` column and a transport-shaped worker make a move to direct FCM/APNs a swap rather than a rewrite.
- **Self-hosted push.** Self-hosters cannot use our Expo credentials. *Mitigation*: push degrades gracefully to in-app plus email when unconfigured — the same fallback pattern `prd/notification-system` uses for Trigger.dev.
File diff suppressed because it is too large Load Diff
+203
View File
@@ -0,0 +1,203 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sign in — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Entry</span>
<h1>Sign in &amp; organization</h1>
<p>
Email/password, Google, and enterprise SSO all run through Better Auth's Expo client — the session cookie lives in
<code>expo-secure-store</code> and OAuth returns through the <code>classroomio://</code> deep link. Multi-org accounts
pick an organization, which sets the <code>cio-org-id</code> header for every later request and re-themes the app.
Self-hosted installs point the same binary at their own API and skip the picker.
</p>
<div class="proto-links">
<a href="home.html">Next: Home →</a>
<a href="profile.html">Org switcher (Profile)</a>
<a href="downloads.html">Offline entry</a>
</div>
</header>
<div class="stage">
<!-- 1. SIGN IN -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="screen centered">
<div style="text-align: center; margin-bottom: 26px">
<div class="avatar brand lg" style="margin: 0 auto 14px; width: 60px; height: 60px; border-radius: 16px; font-size: 24px">C</div>
<div style="font-size: 21px; font-weight: 600; letter-spacing: -0.02em">Welcome back</div>
<div class="text-muted" style="font-size: 13.5px; margin-top: 4px">Sign in to continue your training</div>
</div>
<div class="field">
<label class="label">Work email</label>
<input class="input" type="email" value="[email protected]" readonly />
</div>
<div class="field">
<label class="label">Password</label>
<input class="input" type="password" value="············" readonly />
</div>
<button class="btn btn-primary btn-lg btn-block" style="margin-top: 4px">Sign in</button>
<div class="row" style="margin: 18px 0 16px">
<div class="divider" style="flex: 1; margin: 0"></div>
<span class="text-muted" style="font-size: 11.5px">OR</span>
<div class="divider" style="flex: 1; margin: 0"></div>
</div>
<div class="stack">
<button class="btn btn-outline btn-lg btn-block">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>
Continue with Google
</button>
<button class="btn btn-outline btn-lg btn-block">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Sign in with SSO
</button>
</div>
<div style="text-align: center; margin-top: 22px">
<a class="text-primary" style="font-size: 13px; font-weight: 500">Forgot password?</a>
</div>
<div style="text-align: center; margin-top: 14px">
<a class="text-muted" style="font-size: 12px">Use a custom server</a>
<div class="hint" style="margin-top: 3px">Self-hosted ClassroomIO</div>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Sign in</b>Three auth paths in one screen. "Use a custom server" is how a self-hosted deployment points the same app at its own API.</figcaption>
</figure>
<!-- 2. ORG PICKER -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:42"></div>
<div class="navbar">
<div>
<div class="title">Choose organization</div>
<div class="sub">You're a member of 3 academies</div>
</div>
</div>
<div class="screen">
<div class="stack">
<a class="lrow current" href="home.html">
<div class="avatar brand" style="background: oklch(0.488 0.243 264.376)">NL</div>
<div class="grow">
<div class="ltitle">Northwind Logistics</div>
<div class="lmeta"><span class="badge badge-primary-soft">Learner</span><span>4 courses in progress</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="tutor-grading.html">
<div class="avatar brand" style="background: oklch(0.424 0.199 265.638)">AH</div>
<div class="grow">
<div class="ltitle">Acme Health Academy</div>
<div class="lmeta"><span class="badge badge-success-soft">Tutor</span><span>12 submissions to grade</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="home.html">
<div class="avatar brand" style="background: oklch(0.546 0.245 262.881)">MP</div>
<div class="grow">
<div class="ltitle">Meridian Partner Network</div>
<div class="lmeta"><span class="badge badge-primary-soft">Learner</span><span>Certification expires in 21 days</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<div class="banner banner-muted" style="margin-top: 18px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
<div>
<div class="bd" style="color: var(--muted-foreground)">
Each organization keeps its own courses, branding, and records. You can switch any time from your profile.
</div>
</div>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Organization picker</b>Only for multi-org accounts. The choice sets <code>cio-org-id</code> and re-themes the app; single-org and self-hosted users never see it.</figcaption>
</figure>
<!-- 3. BIOMETRIC + OFFLINE LAUNCH -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="7:02" data-offline></div>
<div class="offline-strip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M2 2 22 22M8.5 16.5a5 5 0 0 1 7 0M5 13a10 10 0 0 1 4-2.4M12 20h.01"/></svg>
You're offline — showing downloaded content
</div>
<div class="screen centered">
<div style="text-align: center">
<div
style="width: 88px; height: 88px; border-radius: 999px; margin: 0 auto 20px; display: grid; place-items: center; background: color-mix(in oklab, var(--primary) 12%, transparent); color: var(--primary)"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="40" height="40" stroke-width="1.6">
<path d="M12 11c0 3-1 5.5-2 7"/><path d="M5 11a7 7 0 0 1 12-5"/><path d="M5 15v-4"/>
<path d="M8.5 20a12 12 0 0 0 1.5-6 2 2 0 1 1 4 0 12 12 0 0 0 .5 3.5"/><path d="M19 11a7 7 0 0 0-.5-2.6"/>
<path d="M17.5 19a20 20 0 0 0 .5-5"/>
</svg>
</div>
<div style="font-size: 20px; font-weight: 600; letter-spacing: -0.02em">Unlock ClassroomIO</div>
<div class="text-muted" style="font-size: 13.5px; margin-top: 6px; line-height: 1.5; padding: 0 12px">
Use Face ID to open your saved session. Your training records stay on this device until you reconnect.
</div>
<button class="btn btn-primary btn-lg btn-block" style="margin-top: 26px">Unlock with Face ID</button>
<button class="btn btn-ghost btn-block" style="margin-top: 8px">Use password instead</button>
</div>
<a class="card card-pad row" href="downloads.html" style="margin-top: 30px; text-align: left">
<div class="media is-current">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg>
</div>
<div class="grow">
<div class="ltitle">3 courses available offline</div>
<div class="lsub">Forklift Safety, Fire Safety, GDPR Essentials</div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Biometric re-entry, offline launch</b>Face ID unlocks an existing stored session — it never replaces sign-in. Launching with no network goes straight to downloaded content instead of an error.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+199
View File
@@ -0,0 +1,199 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Certificates — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Proof</span>
<h1>Certificate wallet</h1>
<p>
Proof of training in the learner's pocket — the thing a supervisor, an auditor, or a client site actually asks for.
Reads the existing <code>packages/certificates</code> records, stays viewable offline once downloaded, and shares as
a PDF with a verification link and QR.
</p>
<div class="proto-links">
<a href="compliance.html">← Compliance</a>
<a href="course.html">Earn another →</a>
<a href="profile.html">Profile →</a>
</div>
</header>
<div class="stage">
<!-- 1. WALLET -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<a class="icon-btn plain" href="compliance.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title">Certificates</div><div class="sub">6 issued · 1 expiring soon</div></div>
</div>
<div class="screen">
<div class="chips" data-select style="margin: 10px 0 14px">
<button class="chip on" data-option>All</button>
<button class="chip" data-option>Valid</button>
<button class="chip" data-option>Expiring</button>
<button class="chip" data-option>Expired</button>
</div>
<div class="stack">
<div class="card" style="overflow: hidden">
<div class="card-pad">
<div class="row row-top">
<div class="media lg is-warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></div>
<div class="grow">
<div class="ltitle" style="font-size: 15px">Hazardous Materials Handling</div>
<div class="lsub">Issued 8 Sep 2025 · Northwind Logistics</div>
<div class="lmeta"><span class="badge badge-warning-soft">Expires in 21 days</span></div>
</div>
</div>
</div>
<div class="card-pad row" style="gap: 8px; padding-top: 0">
<button class="btn btn-outline btn-sm" style="flex: 1">View</button>
<button class="btn btn-outline btn-sm" style="flex: 1">Share</button>
<button class="btn btn-primary btn-sm" style="flex: 1.2">Renew</button>
</div>
</div>
<a class="lrow" href="certificates.html">
<div class="media lg is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></div>
<div class="grow">
<div class="ltitle">Data Protection &amp; GDPR Essentials</div>
<div class="lmeta"><span>Issued 16 Aug 2026</span><span class="sep">·</span><span class="tnum">CIO-GDPR-4F91</span></div>
<div class="lmeta"><span class="badge badge-success-soft">Valid until 16 Aug 2027</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="certificates.html">
<div class="media lg is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></div>
<div class="grow">
<div class="ltitle">Manual Handling &amp; Ergonomics</div>
<div class="lmeta"><span>Issued 2 Jun 2026</span><span class="sep">·</span><span class="tnum">CIO-MHE-8C22</span></div>
<div class="lmeta"><span class="badge badge-success-soft">No expiry</span><span class="badge badge-outline">Saved offline</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="certificates.html">
<div class="media lg is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></div>
<div class="grow">
<div class="ltitle">Site Induction — Meridian Depot</div>
<div class="lmeta"><span>Issued 4 Mar 2026</span><span class="sep">·</span><span class="tnum">CIO-SI-1A70</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<div class="lrow" style="opacity: 0.65">
<div class="media lg"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></div>
<div class="grow">
<div class="ltitle">Forklift Safety Recertification 2025</div>
<div class="lmeta"><span class="badge badge-danger-soft">Expired 17 Aug 2026</span></div>
</div>
</div>
</div>
</div>
<nav class="tabbar" data-tabbar="compliance"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Wallet</b>Sorted by what needs attention. Expiring certificates get a Renew action that jumps straight into the refresher course.</figcaption>
</figure>
<!-- 2. DETAIL -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:43"></div>
<div class="navbar">
<a class="icon-btn plain" href="certificates.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Certificate</div></div>
<button class="icon-btn" aria-label="Share"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="m16 6-4-4-4 4M12 2v13"/></svg></button>
</div>
<div class="screen">
<!-- rendered certificate -->
<div
class="card"
style="margin: 12px 0 14px; padding: 22px 18px; text-align: center; border-width: 2px; border-color: color-mix(in oklab, var(--primary) 30%, transparent)"
>
<div class="avatar brand" style="margin: 0 auto 12px; width: 38px; height: 38px">NL</div>
<div class="eyebrow">Northwind Logistics Academy</div>
<div style="font-size: 12.5px; color: var(--muted-foreground); margin: 12px 0 4px">This certifies that</div>
<div style="font-size: 19px; font-weight: 600; letter-spacing: -0.02em">Amara Okafor</div>
<div style="font-size: 12.5px; color: var(--muted-foreground); margin: 10px 0 4px">has successfully completed</div>
<div style="font-size: 15.5px; font-weight: 600; letter-spacing: -0.01em; line-height: 1.35">
Data Protection &amp; GDPR Essentials
</div>
<div class="lsub" style="margin-top: 8px">16 August 2026 · Final score 92%</div>
<div class="divider" style="margin: 16px 0 14px"></div>
<div class="row" style="justify-content: center; gap: 14px">
<div style="width: 62px; height: 62px; border-radius: 8px; border: 1px solid var(--border); display: grid; place-items: center; background: var(--muted)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="30" height="30" stroke-width="1.7">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
<path d="M14 14h3v3h-3zM18 18h3v3h-3z"/>
</svg>
</div>
<div style="text-align: left">
<div class="eyebrow">Verify</div>
<div class="tnum" style="font-size: 12px; font-weight: 600; margin-top: 3px">CIO-GDPR-4F91-2026</div>
<div class="lsub" style="margin-top: 2px">verify.classroomio.com</div>
</div>
</div>
</div>
<div class="row" style="gap: 8px; margin-bottom: 14px">
<button class="btn btn-primary btn-lg" style="flex: 1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="m16 6-4-4-4 4M12 2v13"/></svg>
Share PDF
</button>
<button class="btn btn-outline btn-lg" style="flex: 1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg>
Save
</button>
</div>
<div class="card card-pad">
<div class="kv"><span class="k">Issued to</span><span class="v">Amara Okafor</span></div>
<div class="kv"><span class="k">Issued by</span><span class="v">Northwind Logistics</span></div>
<div class="kv"><span class="k">Issue date</span><span class="v">16 Aug 2026</span></div>
<div class="kv"><span class="k">Valid until</span><span class="v">16 Aug 2027</span></div>
<div class="kv"><span class="k">Certificate ID</span><span class="v tnum" style="font-size: 12.5px">CIO-GDPR-4F91-2026</span></div>
<div class="kv"><span class="k">Available offline</span><span class="v"><span class="badge badge-success-soft">Yes</span></span></div>
</div>
<div class="hint" style="text-align: center; margin-top: 12px">
Anyone with the ID can confirm this certificate is genuine without you sending a file.
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Detail &amp; verification</b>The rendered certificate, share sheet, and a verification ID/QR — so a site supervisor can check it on the spot.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+321
View File
@@ -0,0 +1,321 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Community — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Tab 4</span>
<h1>Community &amp; newsfeed</h1>
<p>
Org and course Q&amp;A plus cohort announcements, backed by the existing <code>/community</code> and
<code>/course/:id/newsfeed</code> routes. There is no direct messaging in ClassroomIO, so threaded Q&amp;A is the
discussion surface — and offline it degrades to read-only with the compose box explicitly disabled.
</p>
<div class="proto-links">
<a href="notifications.html">Notifications →</a>
<a href="tutor-course.html">Tutor moderation →</a>
<a href="home.html">← Home</a>
</div>
</header>
<div class="stage">
<!-- 1. FEED -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="12:20"></div>
<div class="navbar">
<div class="grow"><div class="title">Community</div></div>
<button class="icon-btn" aria-label="Search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></button>
<button class="icon-btn" aria-label="New post"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
<div class="tabs" data-select>
<button class="on" data-option>For you</button>
<button data-option>Q&amp;A</button>
<button data-option>Announcements</button>
<button data-option>My threads</button>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 12px 0 10px; border-color: color-mix(in oklab, var(--primary) 30%, transparent)">
<div class="row row-top">
<div class="avatar">DM</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13.5px">Daniel Mensah <span class="badge badge-primary-soft" style="margin-left: 4px">Tutor</span></div>
<span class="text-muted" style="font-size: 11px">2h</span>
</div>
<div class="lmeta" style="margin-top: 1px"><span>Announcement · Q3 Frontline Onboarding</span></div>
<div style="font-size: 13.5px; line-height: 1.55; margin-top: 7px">
The practical forklift assessment moves to <strong>Bay 4</strong> this Thursday. Bring your completion code
from Lesson 12 — we can't sign anyone off without it.
</div>
<div class="row" style="margin-top: 10px; gap: 14px">
<span class="lmeta" style="margin: 0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7 10v12M15 5.9 14 10h5.8a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7V10l4-8a2.7 2.7 0 0 1 4 2z"/></svg> 12</span>
<span class="lmeta" style="margin: 0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg> 4 replies</span>
<span class="badge badge-primary-soft" style="margin-left: auto">Mentions you</span>
</div>
</div>
</div>
</div>
<div class="sec-head" style="margin-top: 14px"><h2>Questions you follow</h2></div>
<div class="stack">
<div class="card card-pad">
<div class="row row-top">
<div class="avatar">TA</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13.5px">Tomás Alvarez</div>
<span class="text-muted" style="font-size: 11px">5h</span>
</div>
<div class="lmeta" style="margin-top: 1px"><span>Forklift Safety Recertification · Lesson 4</span></div>
<div style="font-size: 14px; font-weight: 500; margin-top: 6px; line-height: 1.4">
Does a hairline crack at the fork heel always mean tag-out?
</div>
<div class="lrow" style="margin-top: 10px; background: var(--muted); border: none">
<div class="avatar sm">DM</div>
<div class="grow">
<div style="font-size: 12.5px; line-height: 1.45">
Yes — any crack at the heel is a tag-out. Photograph it, log it, and tell your supervisor before the
next shift.
</div>
<div class="lmeta"><span class="badge badge-success-soft">Answered by tutor</span></div>
</div>
</div>
<div class="row" style="margin-top: 10px; gap: 14px">
<span class="lmeta" style="margin: 0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7 10v12M15 5.9 14 10h5.8a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7V10l4-8a2.7 2.7 0 0 1 4 2z"/></svg> 8</span>
<span class="lmeta" style="margin: 0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg> 3</span>
<span class="lmeta" style="margin: 0 0 0 auto"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg> Following</span>
</div>
</div>
</div>
</div>
<div class="card card-pad">
<div class="row row-top">
<div class="avatar">PK</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13.5px">Priya Kaur</div>
<span class="text-muted" style="font-size: 11px">1d</span>
</div>
<div class="lmeta" style="margin-top: 1px"><span>GDPR Essentials · General</span></div>
<div style="font-size: 14px; font-weight: 500; margin-top: 6px; line-height: 1.4">
If a customer asks for their data verbally at the counter, does the 30-day clock start?
</div>
<div class="row" style="margin-top: 10px; gap: 14px">
<span class="lmeta" style="margin: 0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7 10v12M15 5.9 14 10h5.8a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7V10l4-8a2.7 2.7 0 0 1 4 2z"/></svg> 5</span>
<span class="lmeta" style="margin: 0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg> 2</span>
<span class="badge badge-warning-soft" style="margin-left: auto">Unanswered</span>
</div>
</div>
</div>
</div>
</div>
<div class="sec-head"><h2>Ask your own</h2></div>
<div class="card card-pad">
<div class="row">
<div class="avatar">AO</div>
<div class="grow"><div class="text-muted" style="font-size: 13.5px">Ask a question about a lesson…</div></div>
<button class="icon-btn" aria-label="Attach"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m21.4 11.1-9.2 9.2a5 5 0 0 1-7-7l9.2-9.2a3.3 3.3 0 1 1 4.7 4.7l-9.2 9.2a1.7 1.7 0 0 1-2.4-2.4l8.5-8.5"/></svg></button>
</div>
</div>
</div>
<nav class="tabbar" data-tabbar="community" data-badges="community:3"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>For you</b>Mentions, followed threads, and announcements in one stream. Tutor answers are marked so learners can trust them at a glance.</figcaption>
</figure>
<!-- 2. THREAD -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="12:24"></div>
<div class="navbar bordered">
<a class="icon-btn plain" href="community.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Question</div><div class="sub">Forklift Safety · Lesson 4</div></div>
<button class="icon-btn" aria-label="More"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg></button>
</div>
<div class="screen">
<div style="padding: 14px 0 10px">
<div class="row row-top">
<div class="avatar">TA</div>
<div class="grow">
<div class="ltitle" style="font-size: 13.5px">Tomás Alvarez</div>
<div class="lmeta" style="margin-top: 0"><span>5h ago</span></div>
</div>
<button class="btn btn-outline btn-sm">Following</button>
</div>
<div style="font-size: 16.5px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.35; margin: 12px 0 8px">
Does a hairline crack at the fork heel always mean tag-out?
</div>
<div class="prose" style="font-size: 14px">
<p>
The lesson says cracks are a fault but doesn't distinguish severity. On our older trucks there are surface
marks that have been there for years. Do I tag out every time?
</p>
</div>
<div class="row" style="gap: 8px">
<button class="btn btn-outline btn-sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7 10v12M15 5.9 14 10h5.8a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7V10l4-8a2.7 2.7 0 0 1 4 2z"/></svg> 8</button>
<button class="btn btn-ghost btn-sm">Reply</button>
<button class="btn btn-ghost btn-sm" style="margin-left: auto">Share</button>
</div>
</div>
<div class="divider"></div>
<div class="card card-pad" style="margin-bottom: 12px">
<div class="row row-top">
<div class="avatar">DM</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13.5px">Daniel Mensah <span class="badge badge-success-soft" style="margin-left: 4px">Tutor answer</span></div>
<span class="text-muted" style="font-size: 11px">4h</span>
</div>
<div style="font-size: 13.5px; line-height: 1.55; margin-top: 7px">
Yes — any crack at the heel is a tag-out, no severity judgement. Surface scoring from wear is different:
it's smooth and doesn't catch a fingernail. If you can feel an edge, tag it, photograph it, and log it.
</div>
<div class="row" style="margin-top: 10px; gap: 14px">
<span class="lmeta" style="margin: 0"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7 10v12M15 5.9 14 10h5.8a2 2 0 0 1 2 2.3l-1.4 8a2 2 0 0 1-2 1.7H7V10l4-8a2.7 2.7 0 0 1 4 2z"/></svg> 14</span>
<span class="lmeta" style="margin: 0">Reply</span>
</div>
</div>
</div>
</div>
<div class="stack">
<div class="lrow" style="align-items: flex-start">
<div class="avatar sm">PK</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13px">Priya Kaur</div>
<span class="text-muted" style="font-size: 11px">3h</span>
</div>
<div style="font-size: 13px; line-height: 1.5; margin-top: 4px">
The fingernail test is a good rule of thumb — that's what our depot uses too.
</div>
</div>
</div>
<div class="lrow" style="align-items: flex-start">
<div class="avatar sm">AO</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13px">Amara Okafor</div>
<span class="text-muted" style="font-size: 11px">2h</span>
</div>
<div style="font-size: 13px; line-height: 1.5; margin-top: 4px">
Thanks — I tagged out FLT-07 this morning on exactly this.
</div>
</div>
</div>
</div>
<div class="row" style="gap: 8px; margin-top: 16px">
<input class="input" placeholder="Write a reply…" />
<button class="icon-btn" style="width: 40px; height: 40px" aria-label="Send"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m22 2-7 20-4-9-9-4z"/></svg></button>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Thread</b>The tutor answer is visually promoted above learner replies — the same author/team distinction the API already enforces.</figcaption>
</figure>
<!-- 3. OFFLINE READ-ONLY -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="19:05" data-offline></div>
<div class="offline-strip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M2 2 22 22M8.5 16.5a5 5 0 0 1 7 0M5 13a10 10 0 0 1 4-2.4M12 20h.01"/></svg>
Offline — showing cached posts
</div>
<div class="navbar">
<div class="grow"><div class="title">Community</div></div>
<button class="icon-btn" disabled style="opacity: 0.45" aria-label="New post"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 5v14M5 12h14"/></svg></button>
</div>
<div class="screen">
<div class="banner banner-muted" style="margin: 12px 0 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M2 2 22 22M8.5 16.5a5 5 0 0 1 7 0M5 13a10 10 0 0 1 4-2.4M12 20h.01"/></svg>
<div class="grow">
<div class="bt">Posting is off while you're offline</div>
<div class="bd">You can read the last 50 posts. Replies need a connection so they don't post out of order.</div>
</div>
</div>
<div class="stack" style="opacity: 0.85">
<div class="card card-pad">
<div class="row row-top">
<div class="avatar">DM</div>
<div class="grow">
<div class="ltitle" style="font-size: 13.5px">Daniel Mensah</div>
<div class="lmeta" style="margin-top: 1px"><span>Cached 2h ago</span></div>
<div style="font-size: 13.5px; line-height: 1.55; margin-top: 7px">
The practical forklift assessment moves to Bay 4 this Thursday.
</div>
</div>
</div>
</div>
<div class="card card-pad">
<div class="row row-top">
<div class="avatar">TA</div>
<div class="grow">
<div class="ltitle" style="font-size: 13.5px">Tomás Alvarez</div>
<div class="lmeta" style="margin-top: 1px"><span>Cached 5h ago</span></div>
<div style="font-size: 13.5px; line-height: 1.55; margin-top: 7px">
Does a hairline crack at the fork heel always mean tag-out?
</div>
</div>
</div>
</div>
</div>
<div class="lrow locked" style="margin-top: 14px">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="grow">
<div class="ltitle">Reply box disabled</div>
<div class="lsub">Reconnect to post — nothing you type would be saved</div>
</div>
</div>
</div>
<nav class="tabbar" data-tabbar="community"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Offline read-only</b>The compose affordance is disabled and explained rather than accepting text that would be lost — no fake optimistic posting.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+206
View File
@@ -0,0 +1,206 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Compliance — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Tab 3</span>
<h1>Compliance — the differentiator</h1>
<p>
ClassroomIO's core value is knowing where you stand, so it gets a tab rather than a settings sub-page. Deadlines,
grace periods, renewals, and waivers all already exist server-side (<code>/course/:courseId/compliance</code>);
mobile needs one learner-scoped aggregate, <code>GET /account/compliance</code>, to render them together.
</p>
<div class="proto-links">
<a href="certificates.html">Certificate wallet →</a>
<a href="course.html">Open a required course →</a>
<a href="home.html">← Home</a>
</div>
</header>
<div class="stage">
<!-- 1. AT RISK -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<div class="grow"><div class="title">Compliance</div><div class="sub">Northwind Logistics · updated just now</div></div>
<a class="icon-btn" href="certificates.html" aria-label="Certificates"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></a>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 8px 0 14px">
<div class="row">
<div class="media lg is-danger">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0"/></svg>
</div>
<div class="grow">
<div class="ltitle">Action required</div>
<div class="lmeta">
<span class="badge badge-danger-soft">1 overdue</span>
<span class="badge badge-warning-soft">1 expiring</span>
<span class="badge badge-outline">4 compliant</span>
</div>
</div>
</div>
</div>
<div class="sec-head" style="margin-top: 0"><h2>Overdue</h2></div>
<a class="card card-pad" href="course.html" style="display: block">
<div class="row row-top">
<div class="media is-danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0"/></svg></div>
<div class="grow">
<div class="ltitle">Forklift Safety Recertification 2026</div>
<div class="lmeta"><span class="badge badge-danger-soft">Overdue 3 days</span><span class="badge badge-warning-soft">Grace ends 24 Aug</span></div>
</div>
</div>
<div class="divider" style="margin: 12px 0"></div>
<div class="kv"><span class="k">Deadline</span><span class="v">17 Aug 2026</span></div>
<div class="kv"><span class="k">Progress</span><span class="v tnum">4 of 12 lessons</span></div>
<div class="kv"><span class="k">If missed</span><span class="v">Authorization suspended</span></div>
<button class="btn btn-primary btn-block" style="margin-top: 12px">Resume now · 28 min left</button>
</a>
<div class="sec-head"><h2>Expiring soon</h2></div>
<div class="card card-pad">
<div class="row row-top">
<div class="media is-warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div>
<div class="grow">
<div class="ltitle">Hazardous Materials Handling</div>
<div class="lmeta"><span class="badge badge-warning-soft">Expires in 21 days</span></div>
</div>
</div>
<div class="divider" style="margin: 12px 0"></div>
<div class="row" style="margin-bottom: 6px">
<div class="progress"><span style="width: 82%"></span></div>
<span class="text-muted tnum" style="font-size: 11.5px">21d</span>
</div>
<div class="kv"><span class="k">Certified</span><span class="v">8 Sep 2025</span></div>
<div class="kv"><span class="k">Renewal opens</span><span class="v">8 Sep 2026</span></div>
<a class="btn btn-outline btn-block" href="certificates.html" style="margin-top: 12px">View current certificate</a>
</div>
<div class="sec-head"><h2>Compliant</h2><span class="count">4</span></div>
<div class="stack stack-sm">
<a class="lrow flat" href="certificates.html">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Data Protection &amp; GDPR</div><div class="lmeta"><span>Completed 16 Aug 2026</span><span class="sep">·</span><span>Renews annually</span></div></div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow flat" href="certificates.html">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Manual Handling</div><div class="lmeta"><span>Completed 2 Jun 2026</span></div></div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow flat" href="certificates.html">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Site Induction</div><div class="lmeta"><span>Completed 4 Mar 2026</span></div></div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<div class="lrow flat">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M9 15l2 2 4-4"/></svg></div>
<div class="grow">
<div class="ltitle">Working at Height</div>
<div class="lmeta"><span class="badge badge-muted">Waived</span><span>Not required for your role</span></div>
</div>
</div>
</div>
</div>
<nav class="tabbar" data-tabbar="compliance" data-badges="compliance:dot"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>At risk</b>Overdue first, with the consequence spelled out ("Authorization suspended") and grace-period dates visible. Waivers are shown, not hidden.</figcaption>
</figure>
<!-- 2. ALL CLEAR -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<div class="grow"><div class="title">Compliance</div><div class="sub">Northwind Logistics · updated just now</div></div>
<a class="icon-btn" href="certificates.html" aria-label="Certificates"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></a>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 8px 0 14px">
<div class="row">
<div class="media lg is-done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
</div>
<div class="grow">
<div class="ltitle">Fully compliant</div>
<div class="lmeta"><span class="badge badge-success-soft">All 6 requirements up to date</span></div>
</div>
</div>
</div>
<div class="sec-head" style="margin-top: 0"><h2>Renewal timeline</h2></div>
<div class="card card-pad">
<div class="lrow flat">
<div class="media is-warning">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<div class="grow"><div class="ltitle">Hazardous Materials Handling</div><div class="lmeta"><span>Renews 15 Feb 2027</span></div></div>
<span class="badge badge-warning-soft tnum">183d</span>
</div>
<div class="lrow flat">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg></div>
<div class="grow"><div class="ltitle">Forklift Safety Recertification</div><div class="lmeta"><span>Renews 17 Aug 2027</span></div></div>
<span class="badge badge-outline tnum">366d</span>
</div>
<div class="lrow flat">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg></div>
<div class="grow"><div class="ltitle">Data Protection &amp; GDPR</div><div class="lmeta"><span>Renews 16 Aug 2027</span></div></div>
<span class="badge badge-outline tnum">365d</span>
</div>
</div>
<div class="sec-head"><h2>Your record</h2></div>
<div class="card card-pad">
<div class="kv"><span class="k">Requirements met</span><span class="v tnum">6 of 6</span></div>
<div class="kv"><span class="k">On-time completion rate</span><span class="v tnum">83%</span></div>
<div class="kv"><span class="k">Certificates held</span><span class="v tnum">6</span></div>
<div class="kv"><span class="k">Last audit export</span><span class="v">1 Aug 2026</span></div>
</div>
<a class="btn btn-outline btn-block" href="certificates.html" style="margin-top: 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg>
Open certificate wallet
</a>
<div class="hint" style="text-align: center">
Your organization can verify any certificate from its ID without you sending a file.
</div>
</div>
<nav class="tabbar" data-tabbar="compliance"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>All clear</b>The same screen when nothing is wrong: status, what renews next, and the record an auditor would ask for.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+284
View File
@@ -0,0 +1,284 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Course detail — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Course</span>
<h1>Course detail &amp; outline</h1>
<p>
Sections and lessons with an explicit state on every row: complete, current, locked (with the reason spelled out),
or downloaded. Compliance courses carry a deadline banner. Download is a whole-course action from the header,
sized before it starts and Wi-Fi-only by default.
</p>
<div class="proto-links">
<a href="lesson.html">Open lesson →</a>
<a href="exercise.html">Knowledge check →</a>
<a href="certificates.html">Certificate →</a>
<a href="learn.html">← Learn</a>
</div>
</header>
<div class="stage">
<!-- 1. OUTLINE -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:44"></div>
<div class="navbar">
<a class="icon-btn plain" href="learn.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Forklift Safety Recertification</div></div>
<button class="icon-btn" data-sheet-open="dl" aria-label="Download course"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></button>
<button class="icon-btn" aria-label="More"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg></button>
</div>
<div class="tabs" data-select>
<button class="on" data-option>Content</button>
<button data-option>Compliance</button>
<button data-option>Newsfeed</button>
<button data-option>Community</button>
<button data-option>Certificate</button>
</div>
<div class="screen">
<div class="banner banner-danger" style="margin: 12px 0 12px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0"/></svg>
<div class="grow">
<div class="bt">Overdue by 3 days</div>
<div class="bd">Grace period ends 24 Aug. After that your forklift authorization is suspended.</div>
</div>
</div>
<div class="card card-pad" style="margin-bottom: 6px">
<div class="row">
<div class="ring lg">
<svg viewBox="0 0 36 36"><circle class="track" cx="18" cy="18" r="15.5" fill="none" stroke-width="3"/><circle class="arc primary" cx="18" cy="18" r="15.5" fill="none" stroke-width="3" stroke-linecap="round" stroke-dasharray="97.4" stroke-dashoffset="65.3"/></svg>
<span class="lbl tnum">33%</span>
</div>
<div class="grow">
<div class="lmeta" style="margin: 0"><span>4 of 12 lessons</span></div>
<div class="lmeta"><span>~28 min remaining</span></div>
<div class="lmeta"><span class="badge badge-success-soft">Downloaded · 84 MB</span></div>
</div>
</div>
<a class="btn btn-primary btn-block" href="lesson.html" style="margin-top: 12px">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M8 5v14l11-7z"/></svg>
Continue lesson 4
</a>
</div>
<div class="sec-head"><h2>Section 1 · Before you drive</h2><span class="count">3/3</span></div>
<div class="stack stack-sm">
<a class="lrow flat done" href="lesson.html">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Why recertification matters</div><div class="lmeta"><span>Video · 4 min</span></div></div>
</a>
<a class="lrow flat done" href="lesson.html">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Site rules and exclusion zones</div><div class="lmeta"><span>Reading · 5 min</span></div></div>
</a>
<a class="lrow flat done" href="exercise.html">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Section 1 check</div><div class="lmeta"><span class="badge badge-success-soft">9/10</span></div></div>
</a>
</div>
<div class="sec-head"><h2>Section 2 · Inspection</h2><span class="count">1/4</span></div>
<div class="stack stack-sm">
<a class="lrow current" href="lesson.html" style="padding: 12px 14px">
<div class="media is-current"><svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M8 5v14l11-7z"/></svg></div>
<div class="grow">
<div class="ltitle">Pre-operation inspection checklist</div>
<div class="lmeta"><span>Video · 9 min</span><span class="sep">·</span><span class="text-primary" style="font-weight: 600">6 min left</span></div>
</div>
<span class="dl-dot"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
</a>
<a class="lrow flat" href="lesson.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 5v14l11-7z"/></svg></div>
<div class="grow"><div class="ltitle">Load stability and centre of gravity</div><div class="lmeta"><span>Video · 7 min</span></div></div>
<span class="dl-dot"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
</a>
<div class="lrow flat locked">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="grow">
<div class="ltitle">Hydraulics walkthrough</div>
<div class="lmeta"><span>Complete "Load stability" to unlock</span></div>
</div>
</div>
<div class="lrow flat locked">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="grow">
<div class="ltitle">Section 2 check</div>
<div class="lmeta"><span>Complete all lessons in this section</span></div>
</div>
</div>
</div>
<div class="sec-head"><h2>Section 3 · Assessment</h2><span class="count">0/5</span></div>
<div class="lrow flat locked">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="grow">
<div class="ltitle">5 items locked</div>
<div class="lmeta"><span>Finish Section 2 to unlock the assessment</span></div>
</div>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Outline</b>Every row states its own condition. Locked items name the prerequisite instead of just greying out — and the API enforces the same rule.</figcaption>
</figure>
<!-- 2. DOWNLOAD SHEET -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:45"></div>
<div class="navbar">
<a class="icon-btn plain" href="learn.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Warehouse Fire Safety</div></div>
<button class="icon-btn" aria-label="Download course"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></button>
</div>
<div class="tabs">
<button class="on">Content</button>
<button>Compliance</button>
<button>Newsfeed</button>
<button>Community</button>
</div>
<div class="screen">
<div class="banner banner-warning" style="margin: 12px 0">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
<div class="grow"><div class="bt">Due in 6 days</div><div class="bd">Required for all warehouse staff</div></div>
</div>
<div class="stack stack-sm" style="opacity: 0.5">
<div class="lrow flat done"><div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div><div class="grow"><div class="ltitle">Fire triangle basics</div></div></div>
<div class="lrow flat done"><div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div><div class="grow"><div class="ltitle">Extinguisher classes</div></div></div>
<div class="lrow flat"><div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 5v14l11-7z"/></svg></div><div class="grow"><div class="ltitle">Evacuation routes</div></div></div>
</div>
</div>
<div class="scrim"></div>
<div class="sheet" style="display: flex">
<div class="grabber"></div>
<div class="sheet-head">
<div class="st">Download this course</div>
<button class="icon-btn plain" style="margin-left: auto" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
<div class="sheet-body">
<div class="card card-pad" style="margin-bottom: 12px">
<div class="kv"><span class="k">8 lessons · video, PDFs, transcripts</span><span class="v tnum">112 MB</span></div>
<div class="kv"><span class="k">Free space on this device</span><span class="v tnum">3.4 GB</span></div>
<div class="kv"><span class="k">Connection</span><span class="v">Wi-Fi</span></div>
</div>
<div class="lrow" style="margin-bottom: 8px">
<div class="grow">
<div class="ltitle">Download over Wi-Fi only</div>
<div class="lsub">Avoids using your mobile data</div>
</div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="banner banner-muted" style="margin-bottom: 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
<div class="bd" style="color: var(--muted-foreground)">
Lessons, video, and transcripts work offline. The knowledge check still needs a connection to submit.
</div>
</div>
<button class="btn btn-primary btn-lg btn-block">Download 112 MB</button>
<button class="btn btn-ghost btn-block" style="margin-top: 6px">Not now</button>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Download sheet</b>Size, free space, and connection shown before committing, with the offline limits stated up front rather than discovered on a plane.</figcaption>
</figure>
<!-- 3. COMPLETED -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="10:02"></div>
<div class="navbar">
<a class="icon-btn plain" href="learn.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Data Protection &amp; GDPR Essentials</div></div>
<button class="icon-btn" aria-label="Share"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="m16 6-4-4-4 4M12 2v13"/></svg></button>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 14px 0 12px">
<div class="row row-top">
<div class="media lg is-done">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg>
</div>
<div class="grow">
<div class="ltitle">Course complete</div>
<div class="lmeta"><span class="badge badge-success-soft">Finished 16 Aug 2026</span><span>Final score 92%</span></div>
</div>
</div>
<a class="btn btn-primary btn-block" href="certificates.html" style="margin-top: 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg>
View your certificate
</a>
</div>
<div class="card card-pad" style="margin-bottom: 12px">
<div class="kv"><span class="k">Lessons completed</span><span class="v tnum">10 / 10</span></div>
<div class="kv"><span class="k">Exercises passed</span><span class="v tnum">3 / 3</span></div>
<div class="kv"><span class="k">Time spent</span><span class="v">1h 12m</span></div>
<div class="kv"><span class="k">Certificate ID</span><span class="v tnum" style="font-size: 12.5px">CIO-GDPR-4F91-2026</span></div>
</div>
<div class="sec-head"><h2>Keep the content</h2></div>
<div class="lrow">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></div>
<div class="grow">
<div class="ltitle">Downloaded · 96 MB</div>
<div class="lsub">Review offline any time, or free the space</div>
</div>
<button class="btn btn-outline btn-sm">Remove</button>
</div>
<div class="sec-head"><h2>Recommended next</h2></div>
<a class="lrow" href="course.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg></div>
<div class="grow">
<div class="ltitle">Handling a Data Subject Request</div>
<div class="lmeta"><span>4 lessons</span><span class="sep">·</span><span>~18 min</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Completed</b>Certificate issuance is the payoff, and the download stays until the learner removes it.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+212
View File
@@ -0,0 +1,212 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Downloads &amp; sync — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Platform</span>
<h1>Downloads, storage &amp; sync queue</h1>
<p>
Content is cached in SQLite plus the file system from one <code>GET /course/:courseId/offline-manifest</code> call.
Writes go the other way: completion and playback position queue locally with an idempotency key and replay through
<code>POST /course/:courseId/sync</code>. Completion is monotonic, so a retry can never corrupt a record — which is
exactly why offline exercises are deferred to v2.
</p>
<div class="proto-links">
<a href="lesson.html">← Offline lesson</a>
<a href="course.html">Download a course →</a>
<a href="profile.html">Profile →</a>
</div>
</header>
<div class="stage">
<!-- 1. DOWNLOADS -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:48"></div>
<div class="navbar">
<a class="icon-btn plain" href="profile.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title">Downloads</div><div class="sub">3 courses · 284 MB used</div></div>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 10px 0 14px">
<div class="row between" style="margin-bottom: 8px">
<span class="eyebrow">Storage on this device</span>
<span class="tnum" style="font-size: 12px; font-weight: 600">284 MB of 3.7 GB free</span>
</div>
<div style="display: flex; height: 8px; border-radius: 999px; overflow: hidden; background: var(--muted)">
<span style="width: 30%; background: var(--primary)"></span>
<span style="width: 22%; background: color-mix(in oklab, var(--primary) 55%, var(--muted))"></span>
<span style="width: 12%; background: color-mix(in oklab, var(--primary) 30%, var(--muted))"></span>
</div>
<div class="row" style="gap: 12px; margin-top: 10px; flex-wrap: wrap">
<span class="lmeta" style="margin: 0"><span style="width: 8px; height: 8px; border-radius: 2px; background: var(--primary); display: inline-block"></span> Forklift 84 MB</span>
<span class="lmeta" style="margin: 0"><span style="width: 8px; height: 8px; border-radius: 2px; background: color-mix(in oklab, var(--primary) 55%, var(--muted)); display: inline-block"></span> Fire Safety 112 MB</span>
<span class="lmeta" style="margin: 0"><span style="width: 8px; height: 8px; border-radius: 2px; background: color-mix(in oklab, var(--primary) 30%, var(--muted)); display: inline-block"></span> GDPR 88 MB</span>
</div>
</div>
<div class="sec-head" style="margin-top: 0"><h2>Downloading</h2></div>
<div class="card card-pad" style="margin-bottom: 4px">
<div class="row row-top">
<div class="media lg is-current"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></div>
<div class="grow">
<div class="ltitle">Hazardous Materials Handling</div>
<div class="lmeta"><span>Lesson 5 of 8</span><span class="sep">·</span><span class="tnum">48 MB of 96 MB</span></div>
<div class="row" style="margin-top: 8px">
<div class="progress"><span style="width: 50%"></span></div>
<span class="text-muted tnum" style="font-size: 11px">50%</span>
</div>
</div>
<button class="icon-btn plain" aria-label="Pause"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg></button>
</div>
</div>
<div class="sec-head"><h2>On this device</h2><span class="count">3</span></div>
<div class="stack">
<div class="lrow">
<div class="media lg is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow">
<div class="ltitle">Forklift Safety Recertification</div>
<div class="lmeta"><span>12 lessons</span><span class="sep">·</span><span class="tnum">84 MB</span><span class="sep">·</span><span>Synced 2h ago</span></div>
</div>
<button class="icon-btn" aria-label="Remove download"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
<div class="lrow">
<div class="media lg is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow">
<div class="ltitle">Warehouse Fire Safety</div>
<div class="lmeta"><span>8 lessons</span><span class="sep">·</span><span class="tnum">112 MB</span><span class="sep">·</span><span>Synced 2h ago</span></div>
</div>
<button class="icon-btn" aria-label="Remove download"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
<div class="lrow">
<div class="media lg is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow">
<div class="ltitle">Data Protection &amp; GDPR Essentials</div>
<div class="lmeta"><span>Completed</span><span class="sep">·</span><span class="tnum">88 MB</span></div>
</div>
<button class="icon-btn" aria-label="Remove download"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
</div>
</div>
<div class="sec-head"><h2>Settings</h2></div>
<div class="card card-pad">
<div class="lrow flat">
<div class="grow"><div class="ltitle">Download over Wi-Fi only</div><div class="lsub">Protects your mobile data</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Auto-download required training</div><div class="lsub">When new compliance courses are assigned</div></div>
<label class="switch"><input type="checkbox" /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="kv"><span class="k">Video quality offline</span><span class="v">Standard (480p)</span></div>
</div>
<button class="btn btn-outline btn-block" style="margin-top: 12px; color: var(--destructive); border-color: color-mix(in oklab, var(--destructive) 35%, transparent)">
Remove all downloads · 284 MB
</button>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Downloads &amp; storage</b>Per-course breakdown, resumable progress, and Wi-Fi-only by default. Nothing is silently evicted — the learner chose to download it.</figcaption>
</figure>
<!-- 2. SYNC QUEUE -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="19:22"></div>
<div class="navbar">
<a class="icon-btn plain" href="downloads.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Sync queue</div><div class="sub">Back online · syncing</div></div>
<button class="icon-btn" aria-label="Sync now"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 12a9 9 0 1 1-6.2-8.6"/><path d="M21 3v6h-6"/></svg></button>
</div>
<div class="screen">
<div class="banner banner-info" style="margin: 12px 0 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 12a9 9 0 1 1-6.2-8.6"/><path d="M21 3v6h-6"/></svg>
<div class="grow">
<div class="bt">Syncing 4 changes</div>
<div class="bd">Recorded while you were offline. Your dashboard will match in a few seconds.</div>
</div>
</div>
<div class="stack">
<div class="lrow">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow">
<div class="ltitle">Lesson 4 completed</div>
<div class="lmeta"><span>Forklift Safety</span><span class="sep">·</span><span>Queued 18:31</span></div>
</div>
<span class="badge badge-success-soft">Synced</span>
</div>
<div class="lrow">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow">
<div class="ltitle">Lesson 5 completed</div>
<div class="lmeta"><span>Forklift Safety</span><span class="sep">·</span><span>Queued 18:40</span></div>
</div>
<span class="badge badge-success-soft">Synced</span>
</div>
<div class="lrow current">
<div class="media is-current"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 12a9 9 0 1 1-6.2-8.6"/><path d="M21 3v6h-6"/></svg></div>
<div class="grow">
<div class="ltitle">Playback position 7:02</div>
<div class="lmeta"><span>Lesson 5</span><span class="sep">·</span><span>Queued 18:47</span></div>
</div>
<span class="badge badge-primary-soft">Syncing</span>
</div>
<div class="lrow">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0"/></svg></div>
<div class="grow">
<div class="ltitle">Lesson 6 completed</div>
<div class="lmeta"><span>Retrying in 8s · attempt 2 of 5</span></div>
</div>
<button class="btn btn-outline btn-sm">Retry</button>
</div>
</div>
<div class="sec-head"><h2>What syncs offline</h2></div>
<div class="card card-pad">
<div class="kv"><span class="k">Lesson completion</span><span class="v"><span class="badge badge-success-soft">Queued offline</span></span></div>
<div class="kv"><span class="k">Playback position</span><span class="v"><span class="badge badge-success-soft">Queued offline</span></span></div>
<div class="kv"><span class="k">Exercise answers</span><span class="v"><span class="badge badge-muted">Online only</span></span></div>
<div class="kv"><span class="k">Community posts</span><span class="v"><span class="badge badge-muted">Online only</span></span></div>
</div>
<div class="hint" style="margin-top: 10px">
Completion only ever moves forward, so replaying a queued change twice is harmless. Grading and submissions
stay online to keep assessment records unambiguous.
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Sync queue</b>Every queued intent is visible with its state and retry count — the learner can see their work is safe rather than hoping.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+254
View File
@@ -0,0 +1,254 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Exercise — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Assessment</span>
<h1>Exercises</h1>
<p>
Touch renderers for all 16 question types in <code>packages/question-types</code> — including the ones the web
takes for granted: <code>MATCHING</code> and <code>ORDERING</code> as drag, <code>HOTSPOT</code> as tap,
<code>FILE_UPLOAD</code> through the camera, and <code>VIDEO_RECORDING</code> in-app. Answers autosave locally so a
phone call never costs an attempt; media goes up through the existing presigned-upload route.
</p>
<div class="proto-links">
<a href="lesson.html">← Lesson</a>
<a href="tutor-grading.html">How it's graded →</a>
<a href="certificates.html">Certificate →</a>
</div>
</header>
<div class="stage">
<!-- 1. IN PROGRESS -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:52"></div>
<div class="navbar bordered">
<button class="icon-btn plain" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
<div class="grow">
<div class="title-sm">Section 2 check</div>
<div class="sub">Question 3 of 8 · no time limit</div>
</div>
<span class="badge badge-muted">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg>
Saved
</span>
</div>
<div class="screen">
<div class="row" style="margin: 12px 0 18px">
<div class="segs">
<span class="seg done"></span><span class="seg done"></span><span class="seg now"></span><span class="seg"></span>
<span class="seg"></span><span class="seg"></span><span class="seg"></span><span class="seg"></span>
</div>
</div>
<div class="eyebrow">Single choice · 2 points</div>
<div style="font-size: 17px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.4; margin: 8px 0 16px">
You find a wet, shiny film on a hydraulic ram that reappears after wiping. What do you do?
</div>
<div class="stack" data-select style="margin-bottom: 18px">
<label class="opt" data-option>
<span class="mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span>Top up the hydraulic fluid and continue the shift</span>
</label>
<label class="opt on" data-option>
<span class="mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span>Tag the truck out of service and report the fault</span>
</label>
<label class="opt" data-option>
<span class="mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span>Note it on the log and check again at the end of the shift</span>
</label>
<label class="opt" data-option>
<span class="mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span>Wipe it down and only escalate if the load feels unstable</span>
</label>
</div>
<div class="banner banner-muted">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
<div class="bd" style="color: var(--muted-foreground)">
Your answers save on this device as you go. If you get interrupted, you'll pick up exactly here.
</div>
</div>
<div class="row" style="gap: 8px; margin-top: 18px">
<button class="btn btn-outline btn-lg" style="flex: 1">Back</button>
<button class="btn btn-primary btn-lg" style="flex: 2">Next question</button>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>In progress</b>One question per screen with a segment bar and a persistent "Saved" chip — the autosave promise made visible.</figcaption>
</figure>
<!-- 2. RICH TYPES -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:58"></div>
<div class="navbar bordered">
<button class="icon-btn plain" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
<div class="grow">
<div class="title-sm">Section 2 check</div>
<div class="sub">Question 7 of 8</div>
</div>
<span class="badge badge-muted"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg> Saved</span>
</div>
<div class="screen">
<div class="row" style="margin: 12px 0 18px">
<div class="segs">
<span class="seg done"></span><span class="seg done"></span><span class="seg done"></span><span class="seg done"></span>
<span class="seg done"></span><span class="seg done"></span><span class="seg now"></span><span class="seg"></span>
</div>
</div>
<div class="eyebrow">Photo evidence · 5 points</div>
<div style="font-size: 17px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.4; margin: 8px 0 6px">
Photograph the data plate on the truck you're authorized to drive.
</div>
<div class="lsub" style="margin-bottom: 14px">Your tutor checks that the rated capacity matches your authorization.</div>
<div class="card card-pad" style="margin-bottom: 10px">
<div class="row">
<div class="thumb" style="background: linear-gradient(135deg, oklch(0.556 0 0), oklch(0.3 0 0)); width: 64px; height: 48px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="6" width="18" height="14" rx="2"/><circle cx="12" cy="13" r="3.5"/><path d="M8 6l1.5-2h5L16 6"/></svg>
</div>
<div class="grow">
<div class="ltitle">IMG_4192.jpg</div>
<div class="lmeta"><span>2.4 MB</span><span class="sep">·</span><span>Uploading…</span></div>
<div class="row" style="margin-top: 6px"><div class="progress"><span style="width: 62%"></span></div><span class="text-muted tnum" style="font-size: 11px">62%</span></div>
</div>
<button class="icon-btn plain" aria-label="Cancel upload"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
</div>
<div class="row" style="gap: 8px; margin-bottom: 18px">
<button class="btn btn-outline" style="flex: 1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="6" width="18" height="14" rx="2"/><circle cx="12" cy="13" r="3.5"/><path d="M8 6l1.5-2h5L16 6"/></svg>
Camera
</button>
<button class="btn btn-outline" style="flex: 1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
Files
</button>
</div>
<div class="divider"></div>
<div class="eyebrow">Also in this exercise</div>
<div class="stack stack-sm" style="margin-top: 8px">
<div class="lrow flat">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Match the hazard to its control</div><div class="lmeta"><span>Drag to match · answered</span></div></div>
</div>
<div class="lrow flat">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Tap the blind spot on the diagram</div><div class="lmeta"><span>Hotspot · answered</span></div></div>
</div>
<div class="lrow flat">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m22 8-6 4 6 4V8z"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg></div>
<div class="grow"><div class="ltitle">Record your 60-second handover</div><div class="lmeta"><span>Video · not started</span></div></div>
</div>
</div>
<button class="btn btn-primary btn-lg btn-block" style="margin-top: 18px">Next question</button>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Media &amp; rich question types</b>Camera capture with visible upload progress and retry, plus drag, hotspot, and in-app video recording — the types a phone does better than a laptop.</figcaption>
</figure>
<!-- 3. GRADED -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="14:07"></div>
<div class="navbar bordered">
<a class="icon-btn plain" href="course.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Section 2 check · Result</div></div>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 14px 0 12px; text-align: center">
<div class="ring lg" style="margin: 0 auto 12px; width: 92px; height: 92px">
<svg viewBox="0 0 36 36"><circle class="track" cx="18" cy="18" r="15.5" fill="none" stroke-width="3"/><circle class="arc" cx="18" cy="18" r="15.5" fill="none" stroke-width="3" stroke-linecap="round" stroke-dasharray="97.4" stroke-dashoffset="12.7"/></svg>
<span class="lbl tnum" style="font-size: 20px">87%</span>
</div>
<div style="font-size: 17px; font-weight: 600; letter-spacing: -0.02em">Passed</div>
<div class="lsub" style="margin-top: 3px">26 of 30 points · pass mark 70%</div>
<div class="row" style="justify-content: center; gap: 6px; margin-top: 10px">
<span class="badge badge-success-soft">Graded by Daniel Mensah</span>
<span class="badge badge-outline">2h ago</span>
</div>
</div>
<div class="card card-pad" style="margin-bottom: 12px">
<div class="eyebrow" style="margin-bottom: 8px">Tutor feedback</div>
<div style="font-size: 13.5px; line-height: 1.55">
Solid inspection sequence and the tag-out call was right. Your handover recording skipped the battery state —
mention it next time, it's the one item the incoming driver can't check quickly.
</div>
</div>
<div class="sec-head"><h2>Question breakdown</h2></div>
<div class="stack stack-sm">
<div class="lrow flat">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Q3 · Hydraulic fault response</div><div class="lmeta"><span>2 / 2</span></div></div>
</div>
<div class="lrow flat">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Q5 · Match hazard to control</div><div class="lmeta"><span>4 / 4</span></div></div>
</div>
<div class="lrow flat">
<div class="media is-warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0"/></svg></div>
<div class="grow">
<div class="ltitle">Q8 · Handover recording</div>
<div class="lmeta"><span>4 / 8</span><span class="sep">·</span><span>Missing battery state</span></div>
</div>
</div>
</div>
<div class="divider"></div>
<a class="btn btn-primary btn-lg btn-block" href="course.html">Continue to Section 3</a>
<button class="btn btn-outline btn-block" style="margin-top: 8px">Retake for a higher score</button>
<div class="hint" style="text-align: center">1 retake remaining · your highest score counts</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Graded</b>Score, tutor comment, and a per-question breakdown that names what was missing — feedback the learner can act on without opening a laptop.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+353
View File
@@ -0,0 +1,353 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Home — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Tab 1</span>
<h1>Home — the attention surface</h1>
<p>
Ordered by urgency, not recency: compliance alerts, then continue-learning, due-soon, newly assigned, announcements,
and feedback. The whole screen is one <code>GET /account/home-digest</code> call so a cold launch on a weak
connection costs a single round trip — and the cached copy still renders offline with an explicit staleness chip.
</p>
<div class="proto-links">
<a href="compliance.html">Compliance →</a>
<a href="lesson.html">Continue lesson →</a>
<a href="exercise.html">Due exercise →</a>
<a href="notifications.html">Notifications →</a>
<a href="learn.html">Learn tab →</a>
</div>
</header>
<div class="stage">
<!-- 1. DEFAULT — WORK TO DO -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<div class="avatar brand">NL</div>
<div class="grow">
<div class="sub">Northwind Logistics</div>
<div class="title-sm">Good morning, Amara</div>
</div>
<a class="icon-btn plain" href="notifications.html" aria-label="Notifications">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/></svg>
<span class="ping"></span>
</a>
</div>
<div class="screen">
<a class="banner banner-danger" href="compliance.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0"/></svg>
<div class="grow">
<div class="bt">1 training overdue</div>
<div class="bd">Forklift Safety Recertification was due 3 days ago · 4 days of grace left</div>
</div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" style="width: 15px; height: 15px; margin-top: 3px"><path d="m9 18 6-6-6-6"/></svg>
</a>
<!-- continue -->
<div class="sec-head"><h2>Continue learning</h2></div>
<a class="card" href="lesson.html" style="display: block; overflow: hidden">
<div style="display: flex; gap: 12px; padding: 12px">
<div class="thumb" style="background: linear-gradient(135deg, oklch(0.488 0.243 264.376), oklch(0.546 0.245 262.881))">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M8 5v14l11-7z"/></svg>
</div>
<div class="grow">
<div class="lsub" style="margin: 0">Forklift Safety Recertification 2026</div>
<div class="ltitle" style="margin-top: 2px">Pre-operation inspection checklist</div>
<div class="lmeta"><span>Lesson 4 of 12</span><span class="sep">·</span><span>6 min left</span></div>
</div>
</div>
<div style="padding: 0 12px 12px">
<div class="row">
<div class="progress"><span style="width: 33%"></span></div>
<span class="text-muted tnum" style="font-size: 11.5px">33%</span>
</div>
</div>
</a>
<!-- due soon -->
<div class="sec-head"><h2>Due soon</h2><span class="count">4</span><a class="more" href="learn.html">See all</a></div>
<div class="stack">
<a class="lrow" href="exercise.html">
<div class="media is-danger">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0"/></svg>
</div>
<div class="grow">
<div class="ltitle">Forklift Safety Recertification</div>
<div class="lmeta"><span class="badge badge-danger-soft">Overdue 3 days</span><span class="badge badge-outline">Required</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="certificates.html">
<div class="media is-warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div>
<div class="grow">
<div class="ltitle">Hazardous Materials Handling</div>
<div class="lmeta"><span class="badge badge-warning-soft">Expires in 21 days</span><span>Renewal opens 8 Sep</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="exercise.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></div>
<div class="grow">
<div class="ltitle">Module 3 knowledge check</div>
<div class="lmeta"><span>Data Protection &amp; GDPR Essentials</span><span class="sep">·</span><span>Due in 2 days</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="course.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div>
<div class="grow">
<div class="ltitle">Warehouse Fire Safety</div>
<div class="lmeta"><span>Due in 6 days</span><span class="sep">·</span><span>2 of 8 lessons done</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<!-- newly assigned -->
<div class="sec-head"><h2>Newly assigned</h2></div>
<div class="card card-pad">
<div class="row row-top">
<div class="media lg is-current"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg></div>
<div class="grow">
<div class="ltitle">Hazardous Materials Handling</div>
<div class="lsub">Assigned by Daniel Mensah · 8 lessons · ~45 min</div>
<div class="lmeta"><span class="badge badge-outline">Required</span><span class="badge badge-outline">Deadline 30 Sep</span></div>
</div>
</div>
<button class="btn btn-primary btn-block btn-sm" style="margin-top: 12px; height: 36px">Start course</button>
</div>
<!-- announcements -->
<div class="sec-head"><h2>Cohort announcements</h2><a class="more" href="community.html">See all</a></div>
<a class="card card-pad" href="community.html" style="display: block">
<div class="row row-top">
<div class="avatar">DM</div>
<div class="grow">
<div class="ltitle" style="font-size: 13.5px">Daniel Mensah</div>
<div class="lsub" style="margin-top: 3px">
Reminder: the practical forklift assessment moves to Bay 4 this Thursday. Bring your completion code from
Lesson 12.
</div>
<div class="lmeta"><span>Q3 Frontline Onboarding</span><span class="sep">·</span><span>2h ago</span></div>
</div>
</div>
</a>
<!-- feedback -->
<div class="sec-head"><h2>Recent feedback</h2></div>
<a class="lrow" href="exercise.html">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow">
<div class="ltitle">Incident reporting scenario</div>
<div class="lsub">"Clear escalation path — nice work on step 3."</div>
<div class="lmeta"><span class="badge badge-success-soft">18 / 20 · Passed</span><span>Graded yesterday</span></div>
</div>
</a>
</div>
<nav class="tabbar" data-tabbar="home" data-badges="compliance:dot"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Default — work to do</b>Overdue compliance outranks everything. Every card is a deep-link target for a push notification.</figcaption>
</figure>
<!-- 2. CAUGHT UP -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<div class="avatar brand">NL</div>
<div class="grow">
<div class="sub">Northwind Logistics</div>
<div class="title-sm">Good morning, Amara</div>
</div>
<a class="icon-btn plain" href="notifications.html" aria-label="Notifications">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/></svg>
</a>
</div>
<div class="screen">
<a class="banner banner-muted" href="compliance.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
<div class="grow">
<div class="bt">All 6 requirements compliant</div>
<div class="bd">Next renewal: Hazmat Handling on 15 Feb 2027</div>
</div>
</a>
<div class="empty" style="padding: 34px 22px 26px">
<div class="ei">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg>
</div>
<div class="et">Nothing due — you're all caught up</div>
<div class="ed">No overdue training, no deadlines this week. We'll notify you when something new is assigned.</div>
</div>
<div class="card card-pad" style="margin-bottom: 12px">
<div class="row" style="align-items: stretch">
<div class="stat"><div class="v tnum">9</div><div class="k">Completed</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">6</div><div class="k">Certificates</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">14</div><div class="k">Day streak</div></div>
</div>
</div>
<div class="sec-head"><h2>Keep going</h2><a class="more" href="learn.html">Explore</a></div>
<div class="stack">
<a class="lrow" href="course.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg></div>
<div class="grow">
<div class="ltitle">Customer Service Excellence</div>
<div class="lmeta"><span class="badge badge-outline">Optional</span><span>6 lessons · ~30 min</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="certificates.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></div>
<div class="grow">
<div class="ltitle">Your certificate wallet</div>
<div class="lmeta"><span>6 certificates · 1 expiring soon</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
</div>
<nav class="tabbar" data-tabbar="home"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Caught up</b>The reward state. A muted status line replaces the overdue alert, the urgency stack collapses to an empty state, and the screen shifts to optional learning.</figcaption>
</figure>
<!-- 3. OFFLINE + PUSH -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="18:12" data-offline></div>
<!-- push notification preview -->
<div class="push">
<div class="pi">C</div>
<div class="grow">
<div class="row between">
<div class="pt">Deadline reminder</div>
<div class="pw">now</div>
</div>
<div class="pb">Forklift Safety Recertification is due tomorrow. 8 lessons left — about 20 minutes.</div>
</div>
</div>
<div class="offline-strip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M2 2 22 22M8.5 16.5a5 5 0 0 1 7 0M5 13a10 10 0 0 1 4-2.4M12 20h.01"/></svg>
Offline — showing your last sync
</div>
<div class="navbar" style="margin-top: 54px">
<div class="avatar brand">NL</div>
<div class="grow">
<div class="sub">Northwind Logistics</div>
<div class="title-sm">Good evening, Amara</div>
</div>
<a class="icon-btn plain" href="notifications.html" aria-label="Notifications">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/></svg>
<span class="ping"></span>
</a>
</div>
<div class="screen">
<div class="row" style="margin-bottom: 10px">
<span class="badge badge-warning-soft">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
Last updated 2h ago
</span>
<a class="badge badge-outline" href="downloads.html">2 changes waiting to sync</a>
</div>
<div class="sec-head" style="margin-top: 6px"><h2>Available offline</h2></div>
<div class="stack">
<a class="card" href="lesson.html" style="display: block; overflow: hidden">
<div style="display: flex; gap: 12px; padding: 12px">
<div class="thumb" style="background: linear-gradient(135deg, oklch(0.488 0.243 264.376), oklch(0.546 0.245 262.881))">
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M8 5v14l11-7z"/></svg>
</div>
<div class="grow">
<div class="lsub" style="margin: 0">Forklift Safety Recertification 2026</div>
<div class="ltitle" style="margin-top: 2px">Load stability and centre of gravity</div>
<div class="lmeta">
<span class="dl-dot"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span>Downloaded</span><span class="sep">·</span><span>Lesson 5 of 12</span>
</div>
</div>
</div>
<div style="padding: 0 12px 12px">
<div class="row"><div class="progress"><span style="width: 42%"></span></div><span class="text-muted tnum" style="font-size: 11.5px">42%</span></div>
</div>
</a>
<a class="lrow" href="course.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></div>
<div class="grow">
<div class="ltitle">Warehouse Fire Safety</div>
<div class="lmeta"><span>8 lessons downloaded</span><span class="sep">·</span><span>112 MB</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<div class="sec-head"><h2>Needs a connection</h2></div>
<div class="stack">
<div class="lrow locked">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="grow">
<div class="ltitle">Module 3 knowledge check</div>
<div class="lsub">Exercises submit online only — we'll remind you when you reconnect</div>
</div>
</div>
<div class="lrow locked">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg></div>
<div class="grow">
<div class="ltitle">Community &amp; announcements</div>
<div class="lsub">Reading cached posts · posting disabled offline</div>
</div>
</div>
</div>
</div>
<nav class="tabbar" data-tabbar="home"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Offline + incoming push</b>The cached digest with an explicit staleness chip. Anything that can't work offline is listed and explained rather than silently failing.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+345
View File
@@ -0,0 +1,345 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ClassroomIO Mobile App — Prototype Map</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
<style>
.page {
max-width: 1040px;
margin: 0 auto;
padding: 56px 24px 80px;
}
h1 {
font-size: 32px;
font-weight: 600;
letter-spacing: -0.03em;
margin: 8px 0 10px;
}
.lead {
color: var(--muted-foreground);
font-size: 15px;
max-width: 74ch;
margin: 0 0 12px;
line-height: 1.65;
}
.lead b {
color: var(--foreground);
font-weight: 500;
}
.lead code {
background: var(--muted);
color: var(--foreground);
padding: 1px 6px;
border-radius: 4px;
font-size: 12.5px;
}
.sec {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted-foreground);
margin: 38px 0 14px;
display: flex;
align-items: center;
gap: 10px;
}
.sec::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.shot {
overflow: hidden;
display: flex;
flex-direction: column;
transition: border-color 0.15s, box-shadow 0.15s;
}
.shot:hover {
border-color: var(--ring);
box-shadow: var(--shadow-md);
}
.shot .cover {
height: 72px;
display: grid;
place-items: center;
color: #fff;
}
.shot .cover svg {
width: 26px;
height: 26px;
stroke-width: 1.8;
}
.shot .b {
padding: 13px 15px 15px;
}
.shot .n {
font-size: 11px;
font-weight: 600;
color: var(--muted-foreground);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.shot .t {
font-size: 14.5px;
font-weight: 600;
letter-spacing: -0.01em;
margin: 3px 0 4px;
}
.shot .d {
font-size: 12.5px;
color: var(--muted-foreground);
margin: 0;
line-height: 1.5;
}
.foot {
margin-top: 46px;
padding-top: 20px;
border-top: 1px solid var(--border);
color: var(--muted-foreground);
font-size: 13.5px;
line-height: 1.75;
}
.foot b {
color: var(--foreground);
font-weight: 500;
}
.foot code {
background: var(--muted);
padding: 1px 6px;
border-radius: 4px;
font-size: 12.5px;
}
@media (max-width: 900px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="page">
<span class="eyebrow" style="font-size: 12px; letter-spacing: 0.1em">ClassroomIO · Prototype + PRD handoff</span>
<h1>Mobile App</h1>
<p class="lead">
A <b>learner-first</b> iOS and Android app for the people companies actually train — frontline staff, field
technicians, and distributed employees who often have no work laptop and unreliable connectivity. It opens on
<b>what needs your attention</b>, works <b>offline</b>, and finally gives ClassroomIO a <b>push channel</b> for
deadlines and renewals. A small tutor mode handles grading and attendance on the go; authoring and org
administration stay on the web.
</p>
<p class="lead" style="font-size: 13.5px">
Built on the real <code>packages/ui</code> tokens, rendered in a 390 × 844 device frame. Every page has a theme
toggle and cross-links so you can click through each journey end to end. The engineering PRD lives at
<code>prd/mobile-app/README.md</code>.
</p>
<!-- ENTRY -->
<div class="sec">1 · Entry — sign in &amp; organization</div>
<div class="grid">
<a class="card shot" href="auth.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.488 0.243 264.376), oklch(0.623 0.214 259.815))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</div>
<div class="b">
<div class="n">Auth · 3 states</div>
<div class="t">Sign in, org picker, biometric</div>
<p class="d">Password, Google, and enterprise SSO through Better Auth's Expo client; custom server URL for self-hosted; Face ID re-entry and an offline launch path.</p>
</div>
</a>
<a class="card shot" href="home.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.546 0.245 262.881), oklch(0.488 0.243 264.376))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m3 10 9-7 9 7v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></svg>
</div>
<div class="b">
<div class="n">Tab 1 · 3 states</div>
<div class="t">Home — attention surface</div>
<p class="d">Compliance alerts, continue-learning, due-soon, assignments, announcements, feedback — one <code>/account/home-digest</code> call. Plus caught-up and offline states.</p>
</div>
</a>
<a class="card shot" href="notifications.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.623 0.214 259.815), oklch(0.546 0.245 262.881))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/></svg>
</div>
<div class="b">
<div class="n">Retention · 3 states</div>
<div class="t">Notifications</div>
<p class="d">Permission pre-prompt, the notification center with deep links, and per-category preferences with quiet hours.</p>
</div>
</a>
</div>
<!-- LEARNING -->
<div class="sec">2 · Learner — the consumption path</div>
<div class="grid">
<a class="card shot" href="learn.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.424 0.199 265.638), oklch(0.488 0.243 264.376))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
</div>
<div class="b">
<div class="n">Tab 2 · 3 states</div>
<div class="t">Learn</div>
<p class="d">My learning with progress rings and download markers, the org catalog with enroll-in-place, and cohorts with goal progress.</p>
</div>
</a>
<a class="card shot" href="course.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.685 0.169 237.323), oklch(0.488 0.243 264.376))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
</div>
<div class="b">
<div class="n">Detail · 3 states</div>
<div class="t">Course outline</div>
<p class="d">Complete / current / locked / downloaded on every row, a deadline banner for compliance courses, the download sheet, and the completed state.</p>
</div>
</a>
<a class="card shot" href="lesson.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.424 0.199 265.638), oklch(0.546 0.245 262.881))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m22 8-6 4 6 4V8z"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg>
</div>
<div class="b">
<div class="n">Player · 3 states</div>
<div class="t">Lesson</div>
<p class="d">HLS video with a playback token, transcript and AI-tutor sheets, the fair-use cap state, and offline playback with a queued completion.</p>
</div>
</a>
<a class="card shot" href="exercise.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.488 0.243 264.376), oklch(0.623 0.214 259.815))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
</div>
<div class="b">
<div class="n">Assessment · 3 states</div>
<div class="t">Exercises</div>
<p class="d">Autosaving single-choice, camera capture with upload progress, drag/hotspot/video types, and the graded result with a per-question breakdown.</p>
</div>
</a>
<a class="card shot" href="compliance.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.37 0.044 257.287), oklch(0.488 0.243 264.376))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>
</div>
<div class="b">
<div class="n">Tab 3 · 2 states</div>
<div class="t">Compliance</div>
<p class="d">The differentiator as a tab: overdue with grace dates and consequences, expiring certifications, waivers, and the all-clear renewal timeline.</p>
</div>
</a>
<a class="card shot" href="certificates.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.685 0.169 237.323), oklch(0.424 0.199 265.638))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg>
</div>
<div class="b">
<div class="n">Proof · 2 states</div>
<div class="t">Certificate wallet</div>
<p class="d">Issued certificates sorted by what needs attention, plus the rendered certificate with share, save, and a verification ID/QR.</p>
</div>
</a>
</div>
<!-- PLATFORM -->
<div class="sec">3 · Learner — engagement &amp; platform</div>
<div class="grid">
<a class="card shot" href="community.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.488 0.243 264.376), oklch(0.685 0.169 237.323))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg>
</div>
<div class="b">
<div class="n">Tab 4 · 3 states</div>
<div class="t">Community &amp; newsfeed</div>
<p class="d">Mentions and followed threads, a Q&amp;A thread with the tutor answer promoted, and the offline read-only state with compose disabled.</p>
</div>
</a>
<a class="card shot" href="downloads.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.556 0 0), oklch(0.488 0.243 264.376))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg>
</div>
<div class="b">
<div class="n">Offline · 2 states</div>
<div class="t">Downloads &amp; sync queue</div>
<p class="d">Storage breakdown, resumable downloads, Wi-Fi-only defaults, and every queued write with its sync state and retry count.</p>
</div>
</a>
<a class="card shot" href="profile.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.546 0.245 262.881), oklch(0.685 0.169 237.323))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
</div>
<div class="b">
<div class="n">Tab 5 · 2 states</div>
<div class="t">Profile &amp; settings</div>
<p class="d">Learner record, org switcher sheet, app settings and biometric lock — plus frontline mode, QR launch, and the accessibility commitments.</p>
</div>
</a>
</div>
<!-- TUTOR -->
<div class="sec">4 · Tutor companion — role-aware, same binary</div>
<div class="grid">
<a class="card shot" href="tutor-grading.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.424 0.199 265.638), oklch(0.546 0.245 262.881))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M9 15l2 2 4-4"/></svg>
</div>
<div class="b">
<div class="n">Teach · 2 states</div>
<div class="t">Grading queue</div>
<p class="d">Submissions across all courses, oldest first, and a review screen with answer, video, rubric, score, and recorded audio feedback.</p>
</div>
</a>
<a class="card shot" href="tutor-attendance.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.488 0.243 264.376), oklch(0.546 0.245 262.881))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18M9 16l2 2 4-4"/></svg>
</div>
<div class="b">
<div class="n">Teach · 2 states</div>
<div class="t">Attendance</div>
<p class="d">One-tap Present / Late / Absent on the roster, plus a rotating QR check-in code so learners mark themselves.</p>
</div>
</a>
<a class="card shot" href="tutor-course.html">
<div class="cover" style="background: linear-gradient(135deg, oklch(0.685 0.169 237.323), oklch(0.488 0.243 264.376))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="m7 15 4-4 3 3 5-6"/></svg>
</div>
<div class="b">
<div class="n">Teach · 2 states</div>
<div class="t">Course snapshot &amp; moderation</div>
<p class="d">Completion, overdue learners, and where people get stuck; post an announcement; handle unanswered and reported posts.</p>
</div>
</a>
</div>
<div class="foot">
<p style="margin: 0 0 10px">
<b>Explicitly not in the app.</b> Course, lesson, and exercise authoring; the media manager and AI course builder;
org settings, team management, billing, SSO configuration, custom domains, and API keys; live-class video hosting;
SCORM playback; and <b>any purchase flow at all</b> — no checkout, no plan upgrade, no AI token packs, on any
storefront.
</p>
<p style="margin: 0 0 10px">
<b>Hard dependency.</b> Push notifications ride on <code>prd/notification-system</code>. Mobile contributes a
device-token registry and an Expo Push transport in the existing <code>apps/jobs</code> worker — never a second
notification pipeline.
</p>
<p style="margin: 0">
<b>Offline scope.</b> v1 caches content and queues two monotonic writes (lesson completion, playback position).
Exercises, submissions, and grading stay online so no assessment record can be corrupted by a replay — the same
line Canvas and Blackboard draw.
</p>
</div>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+329
View File
@@ -0,0 +1,329 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Learn — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Tab 2</span>
<h1>Learn — my learning, explore, cohorts</h1>
<p>
Everything the learner is enrolled in, everything the organization has published, and the cohorts they belong to.
Course rows carry a progress ring, a type chip (Compliance / Cohort / Self-paced), and a download marker.
Explore enrolls in place for assigned and free courses — there is deliberately no checkout anywhere in the app.
</p>
<div class="proto-links">
<a href="course.html">Course detail →</a>
<a href="downloads.html">Downloads →</a>
<a href="home.html">← Home</a>
</div>
</header>
<div class="stage">
<!-- 1. MY LEARNING -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<div class="title">Learn</div>
<div class="actions">
<button class="icon-btn" aria-label="Search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></button>
<a class="icon-btn" href="downloads.html" aria-label="Downloads"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></a>
</div>
</div>
<div class="tabs" data-select>
<button class="on" data-option>My learning</button>
<button data-option>Explore</button>
<button data-option>Cohorts</button>
</div>
<div class="screen">
<div class="segmented" data-select style="margin: 12px 0 14px">
<button class="on" data-option>In progress</button>
<button data-option>Not started</button>
<button data-option>Completed</button>
</div>
<div class="stack">
<a class="lrow current" href="course.html">
<div class="ring">
<svg viewBox="0 0 36 36"><circle class="track" cx="18" cy="18" r="15.5" fill="none" stroke-width="3.4"/><circle class="arc primary" cx="18" cy="18" r="15.5" fill="none" stroke-width="3.4" stroke-linecap="round" stroke-dasharray="97.4" stroke-dashoffset="65.3"/></svg>
<span class="lbl tnum">33%</span>
</div>
<div class="grow">
<div class="ltitle">Forklift Safety Recertification 2026</div>
<div class="lmeta">
<span class="badge badge-danger-soft">Overdue</span>
<span class="badge badge-outline">Compliance</span>
</div>
<div class="lmeta">
<span class="dl-dot"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span>Downloaded</span><span class="sep">·</span><span>Lesson 4 of 12</span>
</div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="course.html">
<div class="ring">
<svg viewBox="0 0 36 36"><circle class="track" cx="18" cy="18" r="15.5" fill="none" stroke-width="3.4"/><circle class="arc primary" cx="18" cy="18" r="15.5" fill="none" stroke-width="3.4" stroke-linecap="round" stroke-dasharray="97.4" stroke-dashoffset="73.1"/></svg>
<span class="lbl tnum">25%</span>
</div>
<div class="grow">
<div class="ltitle">Warehouse Fire Safety</div>
<div class="lmeta"><span class="badge badge-warning-soft">Due in 6 days</span><span class="badge badge-outline">Compliance</span></div>
<div class="lmeta">
<span class="dl-dot"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span>Downloaded</span><span class="sep">·</span><span>2 of 8 lessons</span>
</div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="course.html">
<div class="ring">
<svg viewBox="0 0 36 36"><circle class="track" cx="18" cy="18" r="15.5" fill="none" stroke-width="3.4"/><circle class="arc primary" cx="18" cy="18" r="15.5" fill="none" stroke-width="3.4" stroke-linecap="round" stroke-dasharray="97.4" stroke-dashoffset="29.2"/></svg>
<span class="lbl tnum">70%</span>
</div>
<div class="grow">
<div class="ltitle">Data Protection &amp; GDPR Essentials</div>
<div class="lmeta"><span class="badge badge-outline">Cohort</span><span>Q3 Frontline Onboarding</span></div>
<div class="lmeta"><span>Knowledge check due in 2 days</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="course.html">
<div class="ring">
<svg viewBox="0 0 36 36"><circle class="track" cx="18" cy="18" r="15.5" fill="none" stroke-width="3.4"/><circle class="arc primary" cx="18" cy="18" r="15.5" fill="none" stroke-width="3.4" stroke-linecap="round" stroke-dasharray="97.4" stroke-dashoffset="88.6"/></svg>
<span class="lbl tnum">9%</span>
</div>
<div class="grow">
<div class="ltitle">Customer Service Excellence</div>
<div class="lmeta"><span class="badge badge-outline">Self-paced</span><span>Optional</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
</div>
<nav class="tabbar" data-tabbar="learn"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>My learning</b>Progress ring, urgency badge, course type, and download state on every row — the four things a learner scans for.</figcaption>
</figure>
<!-- 2. EXPLORE -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<div class="title">Learn</div>
<div class="actions">
<button class="icon-btn" aria-label="Search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></button>
<a class="icon-btn" href="downloads.html" aria-label="Downloads"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></a>
</div>
</div>
<div class="tabs" data-select>
<button data-option>My learning</button>
<button class="on" data-option>Explore</button>
<button data-option>Cohorts</button>
</div>
<div class="screen">
<div class="search" style="margin: 12px 0 12px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
Search Northwind Academy
</div>
<div class="chips" data-select style="margin-bottom: 14px">
<button class="chip on" data-option>All</button>
<button class="chip" data-option>Safety</button>
<button class="chip" data-option>Compliance</button>
<button class="chip" data-option>Operations</button>
<button class="chip" data-option>Soft skills</button>
</div>
<div class="stack">
<div class="card" style="overflow: hidden">
<div style="height: 96px; background: linear-gradient(135deg, oklch(0.488 0.243 264.376), oklch(0.424 0.199 265.638)); position: relative">
<span class="badge badge-secondary" style="position: absolute; top: 10px; left: 10px">Safety</span>
</div>
<div class="card-pad">
<div class="ltitle" style="font-size: 15px">Hazardous Materials Handling</div>
<div class="lsub">Segregation, spill response, and documentation for warehouse teams.</div>
<div class="lmeta"><span>8 lessons</span><span class="sep">·</span><span>~45 min</span><span class="sep">·</span><span>Certificate</span></div>
<div class="row" style="margin-top: 12px">
<span class="badge badge-primary-soft">Assigned to you</span>
<button class="btn btn-primary btn-sm" style="margin-left: auto">Start</button>
</div>
</div>
</div>
<a class="lrow" href="course.html">
<div class="media sq" style="background: linear-gradient(135deg, oklch(0.488 0.243 264.376), oklch(0.424 0.199 265.638)); border: none; color: #fff">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
</div>
<div class="grow">
<div class="ltitle">Manual Handling &amp; Ergonomics</div>
<div class="lmeta"><span>6 lessons</span><span class="sep">·</span><span>~25 min</span></div>
</div>
<button class="btn btn-outline btn-sm">Enroll</button>
</a>
<a class="lrow" href="course.html">
<div class="media sq" style="background: linear-gradient(135deg, oklch(0.546 0.245 262.881), oklch(0.488 0.243 264.376)); border: none; color: #fff">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
</div>
<div class="grow">
<div class="ltitle">Working at Height</div>
<div class="lmeta"><span>5 lessons</span><span class="sep">·</span><span>~20 min</span></div>
</div>
<button class="btn btn-outline btn-sm">Enroll</button>
</a>
<a class="lrow" href="course.html">
<div class="media sq" style="background: linear-gradient(135deg, oklch(0.685 0.169 237.323), oklch(0.424 0.199 265.638)); border: none; color: #fff">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
</div>
<div class="grow">
<div class="ltitle">Difficult Conversations</div>
<div class="lmeta"><span>4 lessons</span><span class="sep">·</span><span>~18 min</span></div>
</div>
<button class="btn btn-outline btn-sm">Enroll</button>
</a>
</div>
<div class="banner banner-muted" style="margin-top: 16px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
<div class="bd" style="color: var(--muted-foreground)">
Paid enrollment is handled by your organization on the web — the app never asks for payment.
</div>
</div>
</div>
<nav class="tabbar" data-tabbar="learn"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Explore</b>The org catalog with enroll-in-place. No checkout, no upgrade prompt, no external purchase link — Decision 9 in the PRD.</figcaption>
</figure>
<!-- 3. COHORTS -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<div class="title">Learn</div>
<div class="actions">
<button class="icon-btn" aria-label="Search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></button>
<a class="icon-btn" href="downloads.html" aria-label="Downloads"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></a>
</div>
</div>
<div class="tabs" data-select>
<button data-option>My learning</button>
<button data-option>Explore</button>
<button class="on" data-option>Cohorts</button>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 12px 0 10px">
<div class="row between">
<div>
<div class="ltitle" style="font-size: 15px">Q3 Frontline Onboarding</div>
<div class="lsub">18 members · Daniel Mensah</div>
</div>
<span class="badge badge-success-soft">Active</span>
</div>
<div class="divider"></div>
<div class="eyebrow" style="margin-bottom: 8px">Cohort goal</div>
<div class="row">
<div class="progress tall success"><span style="width: 72%"></span></div>
<span class="tnum" style="font-size: 12px; font-weight: 600">13/18</span>
</div>
<div class="lmeta"><span>Everyone completes GDPR Essentials by 30 Aug · 9 days left</span></div>
<div class="divider"></div>
<div class="eyebrow" style="margin-bottom: 8px">Your progress</div>
<div class="stack-sm stack">
<div class="row">
<span class="dl-dot"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span style="font-size: 13px">Welcome &amp; expectations</span>
</div>
<div class="row">
<span class="dl-dot"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
<span style="font-size: 13px">Site induction</span>
</div>
<div class="row">
<span class="media" style="width: 16px; height: 16px; border-radius: 999px"></span>
<span style="font-size: 13px" class="text-muted">GDPR Essentials — knowledge check</span>
</div>
</div>
<a class="btn btn-outline btn-block btn-sm" href="course.html" style="margin-top: 14px; height: 36px">Open cohort course</a>
</div>
<div class="sec-head"><h2>Upcoming sessions</h2></div>
<div class="stack">
<div class="lrow">
<div class="media is-current"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg></div>
<div class="grow">
<div class="ltitle">Practical forklift assessment</div>
<div class="lmeta"><span>Thu 21 Aug · 14:00</span><span class="sep">·</span><span>Bay 4</span></div>
</div>
<span class="badge badge-outline">In person</span>
</div>
<div class="lrow">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m22 8-6 4 6 4V8z"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg></div>
<div class="grow">
<div class="ltitle">GDPR Q&amp;A with Daniel</div>
<div class="lmeta"><span>Fri 22 Aug · 10:00</span><span class="sep">·</span><span>Live class</span></div>
</div>
<button class="btn btn-outline btn-sm">Join</button>
</div>
</div>
<div class="hint" style="margin-top: 12px">
Live classes open in your organization's meeting provider — the app doesn't host video conferencing.
</div>
</div>
<nav class="tabbar" data-tabbar="learn"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Cohorts</b>Goal progress, the learner's own checklist, and upcoming sessions. Live classes hand off to the org's existing provider.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+324
View File
@@ -0,0 +1,324 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lesson player — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Player</span>
<h1>Lesson player</h1>
<p>
HLS video with captions, speed, PiP, background audio, and lock-screen controls, authorized by the new
<code>GET /hls/:assetId/playback-token</code> because a native player can't carry the web's per-asset cookie.
Transcript and the AI Lesson Tutor are bottom sheets over the same screen; mark-complete queues locally when offline.
</p>
<div class="proto-links">
<a href="course.html">← Course outline</a>
<a href="exercise.html">Next: knowledge check →</a>
<a href="downloads.html">Sync queue →</a>
</div>
</header>
<div class="stage">
<!-- 1. ONLINE PLAYER -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:46"></div>
<div class="navbar">
<a class="icon-btn plain" href="course.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow">
<div class="sub">Section 2 · Lesson 4 of 12</div>
<div class="title-sm">Pre-operation inspection</div>
</div>
<button class="icon-btn" aria-label="Lesson language"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20"/></svg></button>
</div>
<div class="screen">
<div class="player">
<span class="pill"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg> Downloaded</span>
<div class="play"><svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M8 5v14l11-7z"/></svg></div>
<div class="ctrl">
<div class="bar"><span style="width: 34%"></span></div>
<div class="meta">
<span>3:04</span>
<span style="opacity: 0.7">9:12</span>
<span class="right">
<span style="font-weight: 600">1.25x</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="5" width="20" height="14" rx="2"/><path d="M7 15h4M14 15h3"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v3"/><rect x="12" y="13" width="10" height="7" rx="2"/></svg>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg>
</span>
</div>
</div>
</div>
<div class="row" style="margin: 12px 0 6px">
<div class="segs">
<span class="seg done"></span><span class="seg done"></span><span class="seg done"></span><span class="seg now"></span>
<span class="seg"></span><span class="seg"></span><span class="seg"></span><span class="seg"></span>
<span class="seg"></span><span class="seg"></span><span class="seg"></span><span class="seg"></span>
</div>
<span class="text-muted tnum" style="font-size: 11.5px">4/12</span>
</div>
<h2 style="font-size: 18px; font-weight: 600; letter-spacing: -0.02em; margin: 10px 0 8px">
Pre-operation inspection checklist
</h2>
<div class="row" style="margin-bottom: 12px; gap: 6px; flex-wrap: wrap">
<span class="badge badge-outline">Video · 9 min</span>
<span class="badge badge-outline">English</span>
<span class="badge badge-success-soft">Available offline</span>
</div>
<div class="prose">
<p>
A pre-operation inspection takes about four minutes and catches most of the faults that cause workplace
incidents. Work through it in the same order every time so nothing gets skipped when you're in a hurry.
</p>
<h3>The seven checks</h3>
<ul>
<li><strong>Tyres and wheels</strong> — cuts, embedded debris, correct pressure.</li>
<li><strong>Forks</strong> — cracks at the heel, bent tips, locking pins seated.</li>
<li><strong>Hydraulic hoses</strong> — chafing, weeping seals, pooled fluid underneath.</li>
<li><strong>Mast and chains</strong> — even tension, no kinked links.</li>
<li><strong>Horn, lights, and alarm</strong> — audible and visible from 10 metres.</li>
<li><strong>Data plate</strong> — legible, and the rated capacity matches today's load.</li>
<li><strong>Seatbelt</strong> — retracts cleanly and latches.</li>
</ul>
<p>Log the result before you move the truck. An unlogged inspection counts as no inspection.</p>
</div>
<div class="stack" style="margin: 16px 0 12px">
<button class="lrow" data-sheet-open="transcript" style="width: 100%; text-align: left; cursor: pointer">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M8 13h8M8 17h5"/></svg></div>
<div class="grow"><div class="ltitle">Transcript</div><div class="lsub">Tap a line to jump to that moment</div></div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</button>
<button class="lrow" data-sheet-open="tutor" style="width: 100%; text-align: left; cursor: pointer">
<div class="media is-current"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m12 3 1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/></svg></div>
<div class="grow"><div class="ltitle">Ask the AI tutor</div><div class="lsub">Answers from this lesson only · 8 of 20 questions left today</div></div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</button>
</div>
<div class="divider"></div>
<a class="btn btn-primary btn-lg btn-block" href="lesson.html">
Mark complete &amp; continue
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg>
</a>
<div class="hint" style="text-align: center">Next: Load stability and centre of gravity · 7 min</div>
</div>
<div class="scrim" data-scrim-for="transcript" style="display: none"></div>
<div class="sheet" id="transcript" style="display: none">
<div class="grabber"></div>
<div class="sheet-head">
<div class="st">Transcript</div>
<button class="icon-btn plain" data-sheet-close="transcript" style="margin-left: auto" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
<div class="sheet-body">
<div class="lrow flat"><span class="text-muted tnum" style="font-size: 11.5px; width: 34px">0:12</span><div class="grow"><div style="font-size: 13.5px">Before you touch the controls, walk the truck. Start at the front-left tyre.</div></div></div>
<div class="lrow flat" style="background: color-mix(in oklab, var(--primary) 8%, transparent); border-radius: var(--radius-md); padding: 11px 8px">
<span class="text-primary tnum" style="font-size: 11.5px; width: 34px; font-weight: 600">3:04</span>
<div class="grow"><div style="font-size: 13.5px; font-weight: 500">Look for weeping seals — a wet film on the ram is a fault, not a maybe.</div></div>
</div>
<div class="lrow flat"><span class="text-muted tnum" style="font-size: 11.5px; width: 34px">3:41</span><div class="grow"><div style="font-size: 13.5px">Chains should have even tension. If one side sags, tag the truck out.</div></div></div>
<div class="lrow flat"><span class="text-muted tnum" style="font-size: 11.5px; width: 34px">4:18</span><div class="grow"><div style="font-size: 13.5px">The data plate tells you the rated capacity at a given load centre.</div></div></div>
<div class="lrow flat"><span class="text-muted tnum" style="font-size: 11.5px; width: 34px">5:02</span><div class="grow"><div style="font-size: 13.5px">Finally, the seatbelt. It has to retract cleanly and latch first time.</div></div></div>
</div>
</div>
<div class="scrim" data-scrim-for="tutor" style="display: none"></div>
<div class="sheet" id="tutor" style="display: none">
<div class="grabber"></div>
<div class="sheet-head">
<div class="media is-current" style="width: 30px; height: 30px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m12 3 1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/></svg></div>
<div><div class="st">AI lesson tutor</div><div class="lsub" style="margin: 0">Scoped to this lesson</div></div>
<button class="icon-btn plain" data-sheet-close="tutor" style="margin-left: auto" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
<div class="sheet-body">
<div style="display: flex; justify-content: flex-end; margin-bottom: 10px">
<div style="max-width: 78%; background: var(--primary); color: var(--primary-foreground); padding: 9px 12px; border-radius: 16px 16px 4px 16px; font-size: 13.5px">
What counts as a "weeping" seal versus normal residue?
</div>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 12px">
<div class="media is-current" style="width: 26px; height: 26px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m12 3 1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/></svg></div>
<div style="max-width: 82%; background: var(--muted); padding: 9px 12px; border-radius: 16px 16px 16px 4px; font-size: 13.5px; line-height: 1.5">
Residue is dry and dull — old dust bonded to grease. Weeping is a wet, shiny film that reappears after you
wipe it. This lesson's rule at 3:04: if it comes back, tag the truck out.
<div class="lmeta" style="margin-top: 7px"><span class="badge badge-outline">From 3:04</span></div>
</div>
</div>
<div class="row" style="gap: 8px">
<input class="input" placeholder="Ask about this lesson…" />
<button class="icon-btn" style="width: 40px; height: 40px" aria-label="Send"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m22 2-7 20-4-9-9-4z"/></svg></button>
</div>
<div class="hint">8 of 20 questions left today · resets at midnight</div>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Online player</b>Video, native prose, and two sheets (transcript, AI tutor) — tap either row in the live prototype to open it.</figcaption>
</figure>
<!-- 2. AI TUTOR CAP -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="21:18"></div>
<div class="navbar">
<a class="icon-btn plain" href="course.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow">
<div class="sub">Section 2 · Lesson 5 of 12</div>
<div class="title-sm">Load stability</div>
</div>
</div>
<div class="screen">
<div class="player">
<div class="play"><svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M8 5v14l11-7z"/></svg></div>
<div class="ctrl">
<div class="bar"><span style="width: 88%"></span></div>
<div class="meta"><span>6:11</span><span style="opacity: 0.7">7:02</span></div>
</div>
</div>
<div class="prose" style="margin-top: 14px">
<p>
The load centre is the horizontal distance from the fork face to the load's centre of gravity. Every truck's
data plate is rated at a specific load centre — usually 500 mm.
</p>
</div>
</div>
<div class="scrim"></div>
<div class="sheet" style="display: flex">
<div class="grabber"></div>
<div class="sheet-head">
<div class="media is-current" style="width: 30px; height: 30px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m12 3 1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z"/></svg></div>
<div><div class="st">AI lesson tutor</div><div class="lsub" style="margin: 0">Scoped to this lesson</div></div>
<button class="icon-btn plain" style="margin-left: auto" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
<div class="sheet-body">
<div class="empty" style="padding: 22px 12px 16px">
<div class="ei">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<div class="et">You've used today's 20 questions</div>
<div class="ed">Your allowance resets at midnight. The transcript and lesson notes are still available.</div>
</div>
<button class="btn btn-outline btn-block">Open transcript instead</button>
<button class="btn btn-ghost btn-block" style="margin-top: 6px">Ask in Community</button>
<div class="hint" style="text-align: center; margin-top: 10px">
Fair-use caps are set by your organization.
</div>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>AI tutor, cap reached</b>The fair-use limit is stated plainly with two real alternatives, not a paywall — the app never sells token packs.</figcaption>
</figure>
<!-- 3. OFFLINE / QUEUED -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="18:40" data-offline></div>
<div class="offline-strip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M2 2 22 22M8.5 16.5a5 5 0 0 1 7 0M5 13a10 10 0 0 1 4-2.4M12 20h.01"/></svg>
Offline — progress will sync automatically
</div>
<div class="navbar">
<a class="icon-btn plain" href="course.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow">
<div class="sub">Section 2 · Lesson 5 of 12</div>
<div class="title-sm">Load stability</div>
</div>
</div>
<div class="screen">
<div class="player">
<span class="pill"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg> Playing from device</span>
<div class="play"><svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M6 4h4v16H6zM14 4h4v16h-4z"/></svg></div>
<div class="ctrl">
<div class="bar"><span style="width: 100%"></span></div>
<div class="meta"><span>7:02</span><span style="opacity: 0.7">7:02</span></div>
</div>
</div>
<div class="banner banner-muted" style="margin: 14px 0 12px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg>
<div class="grow">
<div class="bt">Marked complete</div>
<div class="bd">Saved on this device · will sync when you're back online</div>
</div>
</div>
<div class="sec-head" style="margin-top: 6px"><h2>Waiting to sync</h2><span class="count">2</span></div>
<div class="stack">
<div class="lrow">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 12a9 9 0 1 1-6.2-8.6"/><path d="M21 3v6h-6"/></svg></div>
<div class="grow">
<div class="ltitle">Lesson 5 completed</div>
<div class="lmeta"><span>Queued 18:40</span></div>
</div>
<span class="badge badge-muted">Queued</span>
</div>
<div class="lrow">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M21 12a9 9 0 1 1-6.2-8.6"/><path d="M21 3v6h-6"/></svg></div>
<div class="grow">
<div class="ltitle">Lesson 4 position 9:12</div>
<div class="lmeta"><span>Queued 18:31</span></div>
</div>
<span class="badge badge-muted">Queued</span>
</div>
</div>
<div class="divider"></div>
<a class="btn btn-primary btn-lg btn-block" href="lesson.html">
Next lesson
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg>
</a>
<div class="lrow locked" style="margin-top: 10px">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
<div class="grow">
<div class="ltitle">Section 2 check</div>
<div class="lsub">Exercises need a connection — we'll nudge you when you're back online</div>
</div>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Offline &amp; queued</b>Completion is a monotonic, idempotent intent held on-device — the reason v1 offline covers content but not assessments.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+263
View File
@@ -0,0 +1,263 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Notifications — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Retention</span>
<h1>Notifications, permission &amp; preferences</h1>
<p>
The single biggest reason to ship an app: ClassroomIO has no push channel today, only email. This rides on
<code>prd/notification-system</code> — mobile adds a device-token registry and an Expo Push transport in the existing
<code>apps/jobs</code> worker, never a second notification pipeline. Every category is independently switchable and
quiet hours defer rather than drop.
</p>
<div class="proto-links">
<a href="home.html">Deep-link target: Home →</a>
<a href="exercise.html">Deep-link target: Exercise →</a>
<a href="profile.html">← Profile</a>
</div>
</header>
<div class="stage">
<!-- 1. PRE-PROMPT -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:43"></div>
<div class="screen centered">
<div style="text-align: center">
<div style="width: 76px; height: 76px; border-radius: 22px; background: color-mix(in oklab, var(--primary) 12%, transparent); color: var(--primary); display: grid; place-items: center; margin: 0 auto 18px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="34" height="34" stroke-width="1.7"><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/></svg>
</div>
<div style="font-size: 20px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.3">
Never miss a deadline
</div>
<div class="text-muted" style="font-size: 14px; margin-top: 8px; line-height: 1.55">
Northwind Logistics assigns required training with real deadlines. Turn on notifications and we'll remind you
before something goes overdue — not after.
</div>
</div>
<div class="card card-pad" style="margin: 22px 0 20px; text-align: left">
<div class="lrow flat">
<div class="media is-warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div>
<div class="grow"><div class="ltitle">Deadline &amp; renewal reminders</div></div>
</div>
<div class="lrow flat">
<div class="media is-current"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></div>
<div class="grow"><div class="ltitle">New assignments and grades</div></div>
</div>
<div class="lrow flat">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg></div>
<div class="grow"><div class="ltitle">Replies to your questions</div></div>
</div>
</div>
<button class="btn btn-primary btn-lg btn-block">Turn on notifications</button>
<button class="btn btn-ghost btn-block" style="margin-top: 6px">Not now</button>
<div class="hint" style="text-align: center; margin-top: 12px">
You choose exactly which categories reach you — nothing is marketing.
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Permission pre-prompt</b>Asks in context with the value stated, so a "no" from the cold OS dialog doesn't permanently kill the channel.</figcaption>
</figure>
<!-- 2. CENTER -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:41"></div>
<div class="navbar">
<a class="icon-btn plain" href="home.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title">Notifications</div></div>
<button class="btn btn-ghost btn-sm">Mark all read</button>
</div>
<div class="screen">
<div class="sec-head" style="margin-top: 10px"><h2>Today</h2></div>
<div class="stack">
<a class="lrow" href="compliance.html">
<div class="media is-danger"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 9v4M12 17h.01"/><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0"/></svg></div>
<div class="grow">
<div class="ltitle">Forklift Safety is now overdue</div>
<div class="lsub">Grace period ends 24 Aug · 8 lessons left</div>
<div class="lmeta"><span>08:00</span></div>
</div>
<span style="width: 8px; height: 8px; border-radius: 999px; background: var(--primary)"></span>
</a>
<a class="lrow" href="exercise.html">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></div>
<div class="grow">
<div class="ltitle">Your Section 2 check was graded</div>
<div class="lsub">87% · Passed · Feedback from Daniel Mensah</div>
<div class="lmeta"><span>07:12</span></div>
</div>
<span style="width: 8px; height: 8px; border-radius: 999px; background: var(--primary)"></span>
</a>
<a class="lrow" href="community.html">
<div class="media is-current"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/></svg></div>
<div class="grow">
<div class="ltitle">Daniel replied to your question</div>
<div class="lsub">"Yes — any crack at the heel is a tag-out…"</div>
<div class="lmeta"><span>06:40</span></div>
</div>
<span style="width: 8px; height: 8px; border-radius: 999px; background: var(--primary)"></span>
</a>
</div>
<div class="sec-head"><h2>Earlier</h2></div>
<div class="stack">
<a class="lrow" href="course.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg></div>
<div class="grow">
<div class="ltitle">New course assigned: Hazardous Materials Handling</div>
<div class="lsub">Assigned by Daniel Mensah · due 30 Sep</div>
<div class="lmeta"><span>Yesterday 16:20</span></div>
</div>
</a>
<a class="lrow" href="certificates.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></div>
<div class="grow">
<div class="ltitle">Certificate issued: GDPR Essentials</div>
<div class="lsub">Valid until 16 Aug 2027</div>
<div class="lmeta"><span>Sat 16 Aug</span></div>
</div>
</a>
<a class="lrow" href="compliance.html">
<div class="media is-warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></div>
<div class="grow">
<div class="ltitle">Hazmat certification expires in 30 days</div>
<div class="lsub">Renewal opens 8 Sep</div>
<div class="lmeta"><span>Fri 15 Aug</span></div>
</div>
</a>
<a class="lrow" href="learn.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg></div>
<div class="grow">
<div class="ltitle">Cohort goal: 9 days left</div>
<div class="lsub">Q3 Frontline Onboarding · 13 of 18 done</div>
<div class="lmeta"><span>Thu 14 Aug</span></div>
</div>
</a>
</div>
</div>
<nav class="tabbar" data-tabbar="home"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Center</b>Grouped Today / Earlier with unread dots. Every row deep-links to the exact screen its push notification would open.</figcaption>
</figure>
<!-- 3. PREFERENCES -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:47"></div>
<div class="navbar bordered">
<a class="icon-btn plain" href="profile.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Notification preferences</div></div>
</div>
<div class="screen">
<div class="sec-head" style="margin-top: 12px"><h2>Training &amp; deadlines</h2></div>
<div class="card card-pad">
<div class="lrow flat">
<div class="grow"><div class="ltitle">New assignment</div><div class="lsub">When a course is assigned to you</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Deadline reminder</div><div class="lsub">7 days, 1 day, and morning-of</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Overdue alert</div><div class="lsub">Always on for required training</div></div>
<label class="switch"><input type="checkbox" checked disabled /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Certification renewal</div><div class="lsub">30 and 7 days before expiry</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
</div>
<div class="sec-head"><h2>Progress &amp; feedback</h2></div>
<div class="card card-pad">
<div class="lrow flat">
<div class="grow"><div class="ltitle">Exercise graded</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Certificate issued</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Cohort goal approaching</div></div>
<label class="switch"><input type="checkbox" /><span class="track"></span><span class="knob"></span></label>
</div>
</div>
<div class="sec-head"><h2>Community</h2></div>
<div class="card card-pad">
<div class="lrow flat">
<div class="grow"><div class="ltitle">Mentions &amp; replies</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">New lesson or newsfeed post</div></div>
<label class="switch"><input type="checkbox" /><span class="track"></span><span class="knob"></span></label>
</div>
</div>
<div class="sec-head"><h2>Quiet hours</h2></div>
<div class="card card-pad">
<div class="lrow flat">
<div class="grow"><div class="ltitle">Pause notifications</div><div class="lsub">Deferred, not dropped — they arrive after</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="kv"><span class="k">From</span><span class="v tnum">21:00</span></div>
<div class="kv"><span class="k">Until</span><span class="v tnum">07:00</span></div>
<div class="kv"><span class="k">Time zone</span><span class="v">Europe/London</span></div>
</div>
<div class="banner banner-muted" style="margin-top: 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
<div class="bd" style="color: var(--muted-foreground)">
These settings apply to push and in-app alike, and stay in sync with your email preferences on the web.
</div>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Preferences</b>Per-category switches writing to the same preference store the web uses. Overdue compliance alerts are deliberately not switchable.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+242
View File
@@ -0,0 +1,242 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Profile — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Learner · Tab 5</span>
<h1>Profile &amp; settings</h1>
<p>
The learner's own record plus the app's controls. Switching organization re-themes the app and swaps every list
behind it. Anything that belongs to org administration — billing, team management, SSO, domains — is deliberately a
link to the web, not a screen here.
</p>
<div class="proto-links">
<a href="downloads.html">Downloads →</a>
<a href="notifications.html">Notification preferences →</a>
<a href="certificates.html">Certificates →</a>
</div>
</header>
<div class="stage">
<!-- 1. PROFILE -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="9:50"></div>
<div class="navbar">
<div class="grow"><div class="title">Profile</div></div>
<button class="icon-btn" data-sheet-open="orgs" aria-label="Switch organization"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m18 8 4 4-4 4M2 12h20M6 16l-4-4 4-4"/></svg></button>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 10px 0 12px">
<div class="row">
<div class="avatar lg">AO</div>
<div class="grow">
<div style="font-size: 16px; font-weight: 600; letter-spacing: -0.02em">Amara Okafor</div>
<div class="lsub">[email protected]</div>
<div class="lmeta"><span class="badge badge-primary-soft">Learner</span><span>Warehouse Operations</span></div>
</div>
</div>
<div class="divider"></div>
<div class="row" style="align-items: stretch">
<div class="stat"><div class="v tnum">9</div><div class="k">Completed</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">6</div><div class="k">Certificates</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">83%</div><div class="k">On time</div></div>
</div>
</div>
<button class="lrow" data-sheet-open="orgs" style="width: 100%; text-align: left; cursor: pointer; margin-bottom: 14px">
<div class="avatar brand">NL</div>
<div class="grow">
<div class="ltitle">Northwind Logistics</div>
<div class="lsub">Tap to switch organization · 3 available</div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</button>
<div class="sec-head" style="margin-top: 0"><h2>Your learning</h2></div>
<div class="card card-pad">
<a class="lrow flat" href="certificates.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="8" r="6"/><path d="M15.5 13.5 17 22l-5-3-5 3 1.5-8.5"/></svg></div>
<div class="grow"><div class="ltitle">Certificates</div></div>
<span class="badge badge-outline">6</span>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow flat" href="compliance.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg></div>
<div class="grow"><div class="ltitle">Compliance record</div></div>
<span class="badge badge-danger-soft">1 overdue</span>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow flat" href="downloads.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 15V3M7 10l5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg></div>
<div class="grow"><div class="ltitle">Downloads</div></div>
<span class="badge badge-outline tnum">284 MB</span>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<div class="sec-head"><h2>App</h2></div>
<div class="card card-pad">
<a class="lrow flat" href="notifications.html">
<div class="media"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/></svg></div>
<div class="grow"><div class="ltitle">Notifications</div></div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<div class="kv"><span class="k">Language</span><span class="v">English (UK)</span></div>
<div class="kv"><span class="k">Theme</span><span class="v">Match system</span></div>
<div class="kv"><span class="k">Video quality</span><span class="v">Auto</span></div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Unlock with Face ID</div><div class="lsub">Re-lock the app after 5 minutes</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
</div>
<div class="sec-head"><h2>Account</h2></div>
<div class="card card-pad">
<div class="lrow flat">
<div class="grow"><div class="ltitle">Change password</div><div class="lsub">Opens the web dashboard</div></div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="15" height="15" class="text-muted"><path d="M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Privacy &amp; data</div></div>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="15" height="15" class="text-muted"><path d="M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>
</div>
<a class="lrow flat" href="auth.html">
<div class="grow"><div class="ltitle" style="color: var(--destructive)">Sign out</div></div>
</a>
</div>
<div class="hint" style="text-align: center; margin-top: 14px">
ClassroomIO 1.0.0 (build 118) · Connected to app.classroomio.com
</div>
</div>
<div class="scrim" data-scrim-for="orgs" style="display: none"></div>
<div class="sheet" id="orgs" style="display: none">
<div class="grabber"></div>
<div class="sheet-head">
<div class="st">Switch organization</div>
<button class="icon-btn plain" data-sheet-close="orgs" style="margin-left: auto" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
<div class="sheet-body">
<div class="stack">
<div class="lrow current">
<div class="avatar brand">NL</div>
<div class="grow"><div class="ltitle">Northwind Logistics</div><div class="lmeta"><span class="badge badge-primary-soft">Learner</span><span>Current</span></div></div>
<span class="dl-dot"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></span>
</div>
<a class="lrow" href="tutor-grading.html">
<div class="avatar brand" style="background: oklch(0.424 0.199 265.638)">AH</div>
<div class="grow"><div class="ltitle">Acme Health Academy</div><div class="lmeta"><span class="badge badge-success-soft">Tutor</span><span>12 to grade</span></div></div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="home.html">
<div class="avatar brand" style="background: oklch(0.546 0.245 262.881)">MP</div>
<div class="grow"><div class="ltitle">Meridian Partner Network</div><div class="lmeta"><span class="badge badge-primary-soft">Learner</span></div></div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<div class="hint" style="margin-top: 12px">
Switching changes the theme, the courses, and your records. Downloads stay on the device per organization.
</div>
</div>
</div>
<nav class="tabbar" data-tabbar="profile"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Profile</b>Learner record, app settings, and the org switcher (tap the header row in the live prototype). Admin concerns link out to the web on purpose.</figcaption>
</figure>
<!-- 2. FRONTLINE MODE -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="6:05"></div>
<div class="navbar bordered">
<a class="icon-btn plain" href="profile.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Frontline mode</div></div>
</div>
<div class="screen">
<div class="banner banner-info" style="margin: 12px 0 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
<div class="grow">
<div class="bt">Built for shop floors and shared devices</div>
<div class="bd">Bigger targets, lighter media, and QR launch for staff without a work laptop.</div>
</div>
</div>
<div class="card card-pad" style="margin-bottom: 14px">
<div class="lrow flat">
<div class="grow"><div class="ltitle">Large touch targets</div><div class="lsub">Gloves-friendly rows and buttons</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Low-bandwidth media</div><div class="lsub">Audio-first playback on cellular</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Shared device</div><div class="lsub">Sign out automatically after each session</div></div>
<label class="switch"><input type="checkbox" /><span class="track"></span><span class="knob"></span></label>
</div>
</div>
<div class="card card-pad" style="text-align: center">
<div class="eyebrow">Scan to start a course</div>
<div
style="width: 150px; height: 150px; margin: 14px auto; border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--muted); display: grid; place-items: center; color: var(--muted-foreground)"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="66" height="66" stroke-width="1.5">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
<path d="M14 14h3v3h-3zM18 18h3v3h-3zM14 21h3M21 14v3"/>
</svg>
</div>
<div class="lsub">Point the camera at the poster in the break room, or a QR on the machine itself.</div>
<button class="btn btn-primary btn-lg btn-block" style="margin-top: 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="6" width="18" height="14" rx="2"/><circle cx="12" cy="13" r="3.5"/><path d="M8 6l1.5-2h5L16 6"/></svg>
Open scanner
</button>
</div>
<div class="sec-head"><h2>Accessibility</h2></div>
<div class="card card-pad">
<div class="kv"><span class="k">Text size</span><span class="v">Follows system (Dynamic Type)</span></div>
<div class="kv"><span class="k">Screen reader</span><span class="v">Full labels on every control</span></div>
<div class="kv"><span class="k">Status colours</span><span class="v">Always paired with an icon</span></div>
<div class="kv"><span class="k">Minimum target</span><span class="v tnum">44 pt</span></div>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Frontline mode &amp; accessibility</b>The settings that make the app usable in a warehouse: large targets, low-bandwidth media, shared-device sign-out, and QR launch.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+118
View File
@@ -0,0 +1,118 @@
/*
* proto.js — shared behaviour for the mobile-app prototypes.
*
* Deliberately tiny: a theme toggle, plus opt-in interactions (segmented
* controls, in-screen tabs, chips, bottom sheets, switches) that pages get
* for free by using the right data attribute. No framework, no build step.
*/
(function () {
// ---- device chrome ------------------------------------------------------
// <div class="statusbar" data-status="9:41" data-offline></div>
const SIGNAL =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M2 20h.01M7 20v-4M12 20v-8M17 20V8"/></svg>';
const WIFI =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M5 13a10 10 0 0 1 14 0M8.5 16.5a5 5 0 0 1 7 0M12 20h.01"/></svg>';
const WIFI_OFF =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M2 2 22 22M8.5 16.5a5 5 0 0 1 7 0M5 13a10 10 0 0 1 4-2.4M12 20h.01"/></svg>';
const BATTERY =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="2" y="7" width="18" height="10" rx="2"/><path d="M22 11v2"/></svg>';
document.querySelectorAll('[data-status]').forEach((bar) => {
const offline = bar.hasAttribute('data-offline');
bar.innerHTML = `<span>${bar.dataset.status}</span><span class="icons">${offline ? '' : SIGNAL}${
offline ? WIFI_OFF : WIFI
}${BATTERY}</span>`;
});
// <nav class="tabbar" data-tabbar="home" data-role="tutor"></nav>
const TAB_ICONS = {
home: '<path d="m3 10 9-7 9 7v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/>',
learn:
'<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
compliance:
'<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/>',
teach:
'<path d="M21.4 10.9a1 1 0 0 0 0-1.8L12.8 5.2a2 2 0 0 0-1.6 0L2.6 9.1a1 1 0 0 0 0 1.8l8.6 3.9a2 2 0 0 0 1.6 0z"/><path d="M22 10v6"/><path d="M6 12.5V16a6 3 0 0 0 12 0v-3.5"/>',
community: '<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22z"/>',
profile: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>'
};
const LEARNER_TABS = [
{ id: 'home', label: 'Home', href: 'home.html' },
{ id: 'learn', label: 'Learn', href: 'learn.html' },
{ id: 'compliance', label: 'Compliance', href: 'compliance.html' },
{ id: 'community', label: 'Community', href: 'community.html' },
{ id: 'profile', label: 'Profile', href: 'profile.html' }
];
const TUTOR_TABS = [
{ id: 'home', label: 'Home', href: 'home.html' },
{ id: 'learn', label: 'Learn', href: 'learn.html' },
{ id: 'teach', label: 'Teach', href: 'tutor-grading.html' },
{ id: 'community', label: 'Community', href: 'community.html' },
{ id: 'profile', label: 'Profile', href: 'profile.html' }
];
document.querySelectorAll('[data-tabbar]').forEach((bar) => {
const active = bar.dataset.tabbar;
const badges = (bar.dataset.badges || '').split(',').filter(Boolean);
const tabs = bar.dataset.role === 'tutor' ? TUTOR_TABS : LEARNER_TABS;
bar.innerHTML = tabs
.map((tab) => {
const badge = badges.find((entry) => entry.startsWith(`${tab.id}:`));
const value = badge ? badge.split(':')[1] : '';
const marker =
value === 'dot' ? '<span class="dot"></span>' : value ? `<span class="count">${value}</span>` : '';
return `<a class="tab${tab.id === active ? ' on' : ''}" href="${tab.href}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor">${
TAB_ICONS[tab.id]
}</svg>${tab.label}${marker}</a>`;
})
.join('');
});
// ---- theme toggle -------------------------------------------------------
const toggle = document.querySelector('.theme-toggle');
if (toggle) {
toggle.addEventListener('click', () => {
const dark = document.documentElement.classList.toggle('dark');
localStorage.cioTheme = dark ? 'dark' : 'light';
});
}
// ---- exclusive selection groups ----------------------------------------
// Any container with [data-select] makes its direct children mutually
// exclusive on click: .segmented, .tabs, .chips, option lists.
document.querySelectorAll('[data-select]').forEach((group) => {
const activeClass = group.dataset.select || 'on';
group.addEventListener('click', (event) => {
const target = event.target.closest('[data-option]');
if (!target || !group.contains(target)) return;
group.querySelectorAll('[data-option]').forEach((option) => option.classList.remove(activeClass));
target.classList.add(activeClass);
});
});
// ---- bottom sheets ------------------------------------------------------
// [data-sheet-open="id"] shows #id and its scrim; [data-sheet-close] hides.
function setSheet(id, open) {
const sheet = document.getElementById(id);
if (!sheet) return;
const scrim = sheet.parentElement.querySelector(`[data-scrim-for="${id}"]`);
sheet.style.display = open ? 'flex' : 'none';
if (scrim) scrim.style.display = open ? 'block' : 'none';
}
document.querySelectorAll('[data-sheet-open]').forEach((trigger) => {
trigger.addEventListener('click', () => setSheet(trigger.dataset.sheetOpen, true));
});
document.querySelectorAll('[data-sheet-close]').forEach((trigger) => {
trigger.addEventListener('click', () => setSheet(trigger.dataset.sheetClose, false));
});
document.querySelectorAll('[data-scrim-for]').forEach((scrim) => {
scrim.addEventListener('click', () => setSheet(scrim.dataset.scrimFor, false));
});
})();
+192
View File
@@ -0,0 +1,192 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tutor attendance — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Tutor · Release 3</span>
<h1>Attendance</h1>
<p>
The clearest case for a phone over a laptop: marking a roster while standing in the room. Backed by the existing
<code>/course/:courseId/attendance</code> route, with a QR/code check-in so learners mark themselves and the tutor
only handles exceptions.
</p>
<div class="proto-links">
<a href="tutor-grading.html">← Grading queue</a>
<a href="tutor-course.html">Course snapshot →</a>
</div>
</header>
<div class="stage">
<!-- 1. ROSTER -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="14:03"></div>
<div class="navbar bordered">
<a class="icon-btn plain" href="tutor-grading.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow">
<div class="title-sm">Practical assessment</div>
<div class="sub">Thu 21 Aug · 14:00 · Bay 4</div>
</div>
<button class="icon-btn" aria-label="Check-in code"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><path d="M14 14h3v3h-3zM18 18h3v3h-3z"/></svg></button>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 12px 0 12px">
<div class="row" style="align-items: stretch">
<div class="stat"><div class="v tnum">14</div><div class="k">Present</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">2</div><div class="k">Late</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">2</div><div class="k">Absent</div></div>
</div>
<div class="divider"></div>
<div class="row" style="gap: 8px">
<button class="btn btn-outline btn-sm" style="flex: 1">Mark all present</button>
<button class="btn btn-outline btn-sm" style="flex: 1">Reset</button>
</div>
</div>
<div class="search" style="margin-bottom: 12px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
Find a learner
</div>
<div class="stack stack-sm">
<div class="lrow">
<div class="avatar">AO</div>
<div class="grow"><div class="ltitle">Amara Okafor</div><div class="lmeta"><span>Checked in 13:52 via QR</span></div></div>
<div class="segmented" style="width: 128px; padding: 2px">
<button class="on" style="padding: 4px 6px; font-size: 11px">P</button>
<button style="padding: 4px 6px; font-size: 11px">L</button>
<button style="padding: 4px 6px; font-size: 11px">A</button>
</div>
</div>
<div class="lrow">
<div class="avatar">TA</div>
<div class="grow"><div class="ltitle">Tomás Alvarez</div><div class="lmeta"><span>Checked in 13:58 via QR</span></div></div>
<div class="segmented" style="width: 128px; padding: 2px">
<button class="on" style="padding: 4px 6px; font-size: 11px">P</button>
<button style="padding: 4px 6px; font-size: 11px">L</button>
<button style="padding: 4px 6px; font-size: 11px">A</button>
</div>
</div>
<div class="lrow">
<div class="avatar">PK</div>
<div class="grow"><div class="ltitle">Priya Kaur</div><div class="lmeta"><span>Arrived 14:09</span><span class="badge badge-warning-soft">Late</span></div></div>
<div class="segmented" style="width: 128px; padding: 2px">
<button style="padding: 4px 6px; font-size: 11px">P</button>
<button class="on" style="padding: 4px 6px; font-size: 11px">L</button>
<button style="padding: 4px 6px; font-size: 11px">A</button>
</div>
</div>
<div class="lrow">
<div class="avatar">MB</div>
<div class="grow"><div class="ltitle">Marcus Bell</div><div class="lmeta"><span>No check-in</span><span class="badge badge-danger-soft">Absent</span></div></div>
<div class="segmented" style="width: 128px; padding: 2px">
<button style="padding: 4px 6px; font-size: 11px">P</button>
<button style="padding: 4px 6px; font-size: 11px">L</button>
<button class="on" style="padding: 4px 6px; font-size: 11px">A</button>
</div>
</div>
<div class="lrow">
<div class="avatar">EN</div>
<div class="grow"><div class="ltitle">Esther Nwosu</div><div class="lmeta"><span>Checked in 13:47 via QR</span></div></div>
<div class="segmented" style="width: 128px; padding: 2px">
<button class="on" style="padding: 4px 6px; font-size: 11px">P</button>
<button style="padding: 4px 6px; font-size: 11px">L</button>
<button style="padding: 4px 6px; font-size: 11px">A</button>
</div>
</div>
<div class="lrow">
<div class="avatar">LC</div>
<div class="grow"><div class="ltitle">Liam Chen</div><div class="lmeta"><span>Checked in 13:55 via QR</span></div></div>
<div class="segmented" style="width: 128px; padding: 2px">
<button class="on" style="padding: 4px 6px; font-size: 11px">P</button>
<button style="padding: 4px 6px; font-size: 11px">L</button>
<button style="padding: 4px 6px; font-size: 11px">A</button>
</div>
</div>
</div>
<button class="btn btn-primary btn-lg btn-block" style="margin-top: 16px">Save attendance · 18 learners</button>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Roster</b>Present / Late / Absent as one-tap segments, with the QR check-in time shown so the tutor only corrects exceptions.</figcaption>
</figure>
<!-- 2. CHECK-IN CODE -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="13:45"></div>
<div class="navbar bordered">
<a class="icon-btn plain" href="tutor-attendance.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Check-in code</div><div class="sub">Practical assessment · Bay 4</div></div>
</div>
<div class="screen centered">
<div class="card card-pad" style="text-align: center; padding: 24px 18px">
<div class="eyebrow">Learners scan or type this</div>
<div
style="width: 190px; height: 190px; margin: 16px auto 14px; border-radius: var(--radius-lg); border: 1px solid var(--border); background: var(--muted); display: grid; place-items: center; color: var(--foreground)"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="92" height="92" stroke-width="1.4">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>
<path d="M14 14h3v3h-3zM18 18h3v3h-3zM14 21h3M21 14v3"/>
</svg>
</div>
<div class="tnum" style="font-size: 30px; font-weight: 600; letter-spacing: 0.14em">4Q7 812</div>
<div class="lsub" style="margin-top: 6px">Expires in 14:52 · rotates automatically</div>
</div>
<div class="card card-pad" style="margin-top: 14px">
<div class="row between" style="margin-bottom: 10px">
<span class="eyebrow">Checked in so far</span>
<span class="tnum" style="font-size: 12px; font-weight: 600">14 of 18</span>
</div>
<div class="progress tall success" style="margin-bottom: 12px"><span style="width: 78%"></span></div>
<div class="row" style="gap: -8px">
<div class="avatar sm">AO</div>
<div class="avatar sm" style="margin-left: -8px">TA</div>
<div class="avatar sm" style="margin-left: -8px">EN</div>
<div class="avatar sm" style="margin-left: -8px">LC</div>
<div class="avatar sm" style="margin-left: -8px">RS</div>
<div class="avatar sm" style="margin-left: -8px; background: var(--muted); color: var(--muted-foreground)">+9</div>
<span class="lmeta" style="margin: 0 0 0 auto">Updating live</span>
</div>
</div>
<button class="btn btn-outline btn-block" style="margin-top: 14px">Show on the room display</button>
<a class="btn btn-ghost btn-block" href="tutor-attendance.html" style="margin-top: 6px">Back to roster</a>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>QR check-in</b>A rotating code the room can scan. The roster fills itself and the tutor's job shrinks to the four people it missed.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+248
View File
@@ -0,0 +1,248 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tutor course snapshot — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Tutor · Release 3</span>
<h1>Course snapshot, announcements &amp; moderation</h1>
<p>
Read-only insight plus the two write actions a tutor genuinely needs away from a desk: post an announcement and
moderate a thread. Authoring, analytics configuration, enrollment management, and org settings are deliberately
absent — they stay on the web.
</p>
<div class="proto-links">
<a href="tutor-grading.html">← Grading queue</a>
<a href="tutor-attendance.html">Attendance →</a>
<a href="community.html">Learner's community view →</a>
</div>
</header>
<div class="stage">
<!-- 1. SNAPSHOT -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="11:32"></div>
<div class="navbar bordered">
<a class="icon-btn plain" href="tutor-grading.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">HIPAA Awareness 2026</div><div class="sub">Acme Health Academy</div></div>
<button class="icon-btn" data-sheet-open="announce" aria-label="Post announcement"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m3 11 18-5v12L3 13z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/></svg></button>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 12px 0 12px">
<div class="row" style="align-items: stretch">
<div class="stat"><div class="v tnum">64</div><div class="k">Enrolled</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">71%</div><div class="k">Completed</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum" style="color: var(--destructive)">7</div><div class="k">Overdue</div></div>
</div>
<div class="divider"></div>
<div class="row" style="margin-bottom: 6px">
<div class="progress tall success"><span style="width: 71%"></span></div>
<span class="tnum" style="font-size: 12px; font-weight: 600">45/64</span>
</div>
<div class="lmeta"><span>Deadline 30 Sep 2026 · 45 days left</span></div>
</div>
<div class="sec-head" style="margin-top: 0"><h2>Needs a nudge</h2><span class="count">7</span></div>
<div class="stack">
<div class="lrow">
<div class="avatar">MB</div>
<div class="grow"><div class="ltitle">Marcus Bell</div><div class="lmeta"><span>No activity in 21 days</span><span class="sep">·</span><span>1 of 9 lessons</span></div></div>
<button class="btn btn-outline btn-sm">Remind</button>
</div>
<div class="lrow">
<div class="avatar">RS</div>
<div class="grow"><div class="ltitle">Rina Sato</div><div class="lmeta"><span>Stalled at Lesson 4</span><span class="sep">·</span><span>9 days</span></div></div>
<button class="btn btn-outline btn-sm">Remind</button>
</div>
<div class="lrow">
<div class="avatar">JW</div>
<div class="grow"><div class="ltitle">Joseph Wanjiru</div><div class="lmeta"><span>Waiting on your grading</span></div></div>
<a class="btn btn-primary btn-sm" href="tutor-grading.html">Grade</a>
</div>
</div>
<button class="btn btn-outline btn-block" style="margin-top: 12px">Remind all 7 learners</button>
<div class="sec-head"><h2>Where learners get stuck</h2></div>
<div class="card card-pad">
<div class="lrow flat">
<div class="grow"><div class="ltitle">Lesson 4 · Minimum necessary rule</div><div class="lmeta"><span>11 learners stalled here</span></div></div>
<span class="badge badge-warning-soft">17%</span>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Scenario response</div><div class="lmeta"><span>Average score 6.8 / 10</span></div></div>
<span class="badge badge-warning-soft">Low</span>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Lesson 2 · Covered entities</div><div class="lmeta"><span>4 unanswered questions</span></div></div>
<span class="badge badge-outline">Q&amp;A</span>
</div>
</div>
<div class="hint" style="margin-top: 14px">
Full analytics, exports, and course editing live in the web dashboard.
</div>
</div>
<div class="scrim" data-scrim-for="announce" style="display: none"></div>
<div class="sheet" id="announce" style="display: none">
<div class="grabber"></div>
<div class="sheet-head">
<div class="st">Post an announcement</div>
<button class="icon-btn plain" data-sheet-close="announce" style="margin-left: auto" aria-label="Close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
</div>
<div class="sheet-body">
<div class="field">
<label class="label">Audience</label>
<div class="segmented" data-select>
<button class="on" data-option>Everyone</button>
<button data-option>Overdue only</button>
<button data-option>One cohort</button>
</div>
</div>
<div class="field">
<label class="label">Message</label>
<textarea class="textarea">Reminder: the HIPAA scenario response is the last item before certification. If you're stuck on the "minimum necessary" rule, Lesson 4 has a worked example at 2:15.</textarea>
</div>
<div class="lrow" style="margin-bottom: 12px">
<div class="grow"><div class="ltitle">Also send as a push notification</div><div class="lsub">Respects each learner's quiet hours</div></div>
<label class="switch"><input type="checkbox" checked /><span class="track"></span><span class="knob"></span></label>
</div>
<button class="btn btn-primary btn-lg btn-block">Post to 64 learners</button>
</div>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Snapshot &amp; announcement</b>Enough insight to act — who's stalled and where — plus a one-tap nudge. Open the announcement sheet from the header in the live prototype.</figcaption>
</figure>
<!-- 2. MODERATION -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="11:40"></div>
<div class="navbar">
<div class="avatar brand" style="background: oklch(0.424 0.199 265.638)">AH</div>
<div class="grow"><div class="sub">Acme Health Academy</div><div class="title-sm">Teach</div></div>
</div>
<div class="tabs" data-select>
<button data-option>To grade</button>
<button data-option>Attendance</button>
<button data-option>Courses</button>
<button class="on" data-option>Moderation</button>
</div>
<div class="screen">
<div class="sec-head" style="margin-top: 12px"><h2>Unanswered questions</h2><span class="count">4</span></div>
<div class="stack">
<div class="card card-pad">
<div class="row row-top">
<div class="avatar">FA</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13.5px">Fatima Ali</div>
<span class="text-muted" style="font-size: 11px">2d</span>
</div>
<div class="lmeta" style="margin-top: 1px"><span>HIPAA Awareness · Lesson 2</span></div>
<div style="font-size: 13.5px; line-height: 1.5; margin-top: 6px">
Is a contracted cleaning company a business associate if they're never shown records?
</div>
<div class="row" style="gap: 8px; margin-top: 10px">
<button class="btn btn-primary btn-sm">Answer</button>
<button class="btn btn-outline btn-sm">Assign to tutor</button>
</div>
</div>
</div>
</div>
<div class="card card-pad">
<div class="row row-top">
<div class="avatar">LC</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13.5px">Liam Chen</div>
<span class="text-muted" style="font-size: 11px">3d</span>
</div>
<div class="lmeta" style="margin-top: 1px"><span>HIPAA Awareness · General</span></div>
<div style="font-size: 13.5px; line-height: 1.5; margin-top: 6px">
Does the training reset if I change department within the same hospital?
</div>
<div class="row" style="gap: 8px; margin-top: 10px">
<button class="btn btn-primary btn-sm">Answer</button>
<button class="btn btn-outline btn-sm">Assign to tutor</button>
</div>
</div>
</div>
</div>
</div>
<div class="sec-head"><h2>Reported</h2><span class="count">1</span></div>
<div class="card card-pad">
<div class="row row-top">
<div class="avatar">?</div>
<div class="grow">
<div class="row between">
<div class="ltitle" style="font-size: 13.5px">Anonymous learner</div>
<span class="badge badge-danger-soft">Reported ×2</span>
</div>
<div class="lmeta" style="margin-top: 1px"><span>SOC 2 Security Basics · Q&amp;A</span></div>
<div style="font-size: 13.5px; line-height: 1.5; margin-top: 6px; padding: 8px 10px; background: var(--muted); border-radius: var(--radius-md)">
"Here's the answer key from last year's exam, DM me…"
</div>
<div class="row" style="gap: 8px; margin-top: 10px">
<button class="btn btn-destructive btn-sm">Remove post</button>
<button class="btn btn-outline btn-sm">Keep</button>
<button class="btn btn-ghost btn-sm">View thread</button>
</div>
</div>
</div>
</div>
<div class="sec-head"><h2>Recently handled</h2></div>
<div class="stack stack-sm">
<div class="lrow flat">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Marked "audit trail" answer as accepted</div><div class="lmeta"><span>Yesterday</span></div></div>
</div>
<div class="lrow flat">
<div class="media is-done"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M20 6 9 17l-5-5"/></svg></div>
<div class="grow"><div class="ltitle">Removed duplicate thread</div><div class="lmeta"><span>Mon 18 Aug</span></div></div>
</div>
</div>
</div>
<nav class="tabbar" data-tabbar="teach" data-role="tutor" data-badges="teach:12"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Moderation</b>Unanswered questions and reported posts in one queue, using the same author/team permission rules the API enforces.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>
+226
View File
@@ -0,0 +1,226 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Tutor grading — ClassroomIO Mobile</title>
<link href="https://fonts.googleapis.com/css2?family=Geist:[email protected]&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="app-theme.css" />
<script>
if (localStorage.cioTheme === 'dark' || (!localStorage.cioTheme && matchMedia('(prefers-color-scheme: dark)').matches))
document.documentElement.classList.add('dark');
</script>
</head>
<body>
<header class="proto-head">
<a class="back" href="index.html"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" width="14" height="14"><path d="m15 18-6-6 6-6"/></svg> All screens</a>
<span class="eyebrow">Tutor · Release 3</span>
<h1>Grading queue</h1>
<p>
The Teach tab appears only when <code>orgRoles[activeOrg]</code> is ADMIN or TUTOR — one binary, a role-aware tab bar.
A single new endpoint, <code>GET /account/grading-queue</code>, gathers submissions awaiting grading across every
course the tutor owns, oldest first, so a backlog can be cleared between meetings.
</p>
<div class="proto-links">
<a href="tutor-attendance.html">Attendance →</a>
<a href="tutor-course.html">Course snapshot →</a>
<a href="exercise.html">Learner's view →</a>
</div>
</header>
<div class="stage">
<!-- 1. QUEUE -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="11:15"></div>
<div class="navbar">
<div class="avatar brand" style="background: oklch(0.424 0.199 265.638)">AH</div>
<div class="grow"><div class="sub">Acme Health Academy</div><div class="title-sm">Teach</div></div>
<button class="icon-btn" aria-label="Filter"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 5h18l-7 8v6l-4 2v-8z"/></svg></button>
</div>
<div class="tabs" data-select>
<button class="on" data-option>To grade</button>
<button data-option>Attendance</button>
<button data-option>Courses</button>
<button data-option>Moderation</button>
</div>
<div class="screen">
<div class="card card-pad" style="margin: 12px 0 14px">
<div class="row" style="align-items: stretch">
<div class="stat"><div class="v tnum" style="color: var(--destructive)">3</div><div class="k">Over 3 days</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">12</div><div class="k">Awaiting</div></div>
<div style="width: 1px; background: var(--border)"></div>
<div class="stat"><div class="v tnum">28</div><div class="k">Graded this week</div></div>
</div>
</div>
<div class="sec-head" style="margin-top: 0"><h2>Waiting longest</h2></div>
<div class="stack">
<a class="lrow" href="tutor-grading.html">
<div class="avatar">JW</div>
<div class="grow">
<div class="ltitle">Joseph Wanjiru</div>
<div class="lsub">HIPAA Awareness · Scenario response</div>
<div class="lmeta"><span class="badge badge-danger-soft">Waiting 4 days</span><span class="badge badge-outline">Written + video</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="tutor-grading.html">
<div class="avatar">RS</div>
<div class="grow">
<div class="ltitle">Rina Sato</div>
<div class="lsub">SOC 2 Security Basics · Incident write-up</div>
<div class="lmeta"><span class="badge badge-warning-soft">Waiting 3 days</span><span class="badge badge-outline">Written</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="tutor-grading.html">
<div class="avatar">MB</div>
<div class="grow">
<div class="ltitle">Marcus Bell</div>
<div class="lsub">HIPAA Awareness · Scenario response</div>
<div class="lmeta"><span class="badge badge-warning-soft">Waiting 3 days</span><span class="badge badge-outline">Photo evidence</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<div class="sec-head"><h2>Recent</h2><span class="count">9</span></div>
<div class="stack">
<a class="lrow" href="tutor-grading.html">
<div class="avatar">EN</div>
<div class="grow">
<div class="ltitle">Esther Nwosu</div>
<div class="lsub">SOC 2 Security Basics · Incident write-up</div>
<div class="lmeta"><span>Submitted 6h ago</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="tutor-grading.html">
<div class="avatar">LC</div>
<div class="grow">
<div class="ltitle">Liam Chen</div>
<div class="lsub">HIPAA Awareness · Scenario response</div>
<div class="lmeta"><span>Submitted 9h ago</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
<a class="lrow" href="tutor-grading.html">
<div class="avatar">FA</div>
<div class="grow">
<div class="ltitle">Fatima Ali</div>
<div class="lsub">SOC 2 Security Basics · Incident write-up</div>
<div class="lmeta"><span>Submitted yesterday</span></div>
</div>
<span class="chev"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></span>
</a>
</div>
<div class="hint" style="margin-top: 14px">
Authoring, org settings, and analytics configuration stay on the web — this tab is only the work that can't wait.
</div>
</div>
<nav class="tabbar" data-tabbar="teach" data-role="tutor" data-badges="teach:12"></nav>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Queue</b>Oldest first, with waiting time as the primary signal and the submission type visible before you open it.</figcaption>
</figure>
<!-- 2. REVIEW -->
<figure class="device">
<div class="phone">
<div class="phone-inner">
<div class="notch"></div>
<div class="statusbar" data-status="11:18"></div>
<div class="navbar bordered">
<a class="icon-btn plain" href="tutor-grading.html" aria-label="Back"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m15 18-6-6 6-6"/></svg></a>
<div class="grow"><div class="title-sm">Joseph Wanjiru</div><div class="sub">1 of 12 · HIPAA Awareness</div></div>
<button class="icon-btn" aria-label="Next submission"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="m9 18 6-6-6-6"/></svg></button>
</div>
<div class="screen">
<div class="banner banner-danger" style="margin: 12px 0 14px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
<div class="grow"><div class="bt">Waiting 4 days</div><div class="bd">Learner's certification is blocked until this is graded</div></div>
</div>
<div class="eyebrow">Question · 10 points</div>
<div style="font-size: 15px; font-weight: 600; line-height: 1.4; margin: 6px 0 12px">
A colleague asks you to look up a patient record for a friend. Describe your response and the escalation path.
</div>
<div class="card card-pad" style="margin-bottom: 10px">
<div class="eyebrow" style="margin-bottom: 8px">Written answer</div>
<div style="font-size: 13.5px; line-height: 1.6">
I would decline and explain that access has to be tied to a care relationship. I'd remind them that the audit
log records every lookup by user, so an access without a clinical reason is traceable. If they pressed, I'd
report it to the privacy officer the same day using the internal form.
</div>
</div>
<div class="card card-pad" style="margin-bottom: 14px">
<div class="eyebrow" style="margin-bottom: 8px">Video response · 0:48</div>
<div class="player" style="margin: 0; border-radius: var(--radius-md); aspect-ratio: 16/10">
<div class="play" style="width: 44px; height: 44px"><svg viewBox="0 0 24 24" fill="currentColor" stroke="none" width="18" height="18"><path d="M8 5v14l11-7z"/></svg></div>
<div class="ctrl" style="padding: 14px 10px 8px"><div class="bar"><span style="width: 0%"></span></div><div class="meta"><span>0:00</span><span style="opacity: 0.7">0:48</span></div></div>
</div>
</div>
<div class="sec-head" style="margin-top: 0"><h2>Rubric</h2></div>
<div class="card card-pad" style="margin-bottom: 12px">
<div class="lrow flat">
<div class="grow"><div class="ltitle">Declines the request</div></div>
<span class="badge badge-success-soft">3 / 3</span>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Cites the audit trail</div></div>
<span class="badge badge-success-soft">3 / 3</span>
</div>
<div class="lrow flat">
<div class="grow"><div class="ltitle">Names the escalation path</div></div>
<span class="badge badge-warning-soft">2 / 4</span>
</div>
</div>
<div class="field">
<label class="label">Score</label>
<div class="row" style="gap: 8px">
<input class="input tnum" value="8" style="width: 76px; text-align: center; font-size: 17px; font-weight: 600" />
<span class="text-muted" style="font-size: 14px">/ 10 · pass mark 7</span>
</div>
</div>
<div class="field">
<label class="label">Feedback</label>
<textarea class="textarea">Good instinct on the audit trail. Name the privacy officer's 24-hour SLA next time — that's the part auditors check.</textarea>
<div class="row" style="gap: 8px; margin-top: 8px">
<button class="btn btn-outline btn-sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v3"/></svg> Record audio</button>
<span class="hint" style="margin: 0">Faster than typing on a phone</span>
</div>
</div>
<button class="btn btn-primary btn-lg btn-block">Submit grade &amp; next</button>
<button class="btn btn-outline btn-block" style="margin-top: 8px">Request resubmission</button>
</div>
<div class="home-bar"></div>
</div>
</div>
<figcaption><b>Review</b>Answer, video, and rubric on one screen, with recorded audio feedback because typing a paragraph on a phone is the real blocker.</figcaption>
</figure>
</div>
<button class="theme-toggle" title="Toggle theme" aria-label="Toggle theme">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
</button>
<script src="proto.js"></script>
</body>
</html>