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:
co-authored by
Rotimi Best
Cursor Agent
parent
368a7fe5ae
commit
967e81d33f
@@ -8,6 +8,7 @@ Use this file to keep a single prioritized view of all PRDs.
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| - | - | `prd/super-admin` | draft |
|
| - | - | `prd/super-admin` | draft |
|
||||||
| - | - | `prd/embed-link-platforms` | draft |
|
| - | - | `prd/embed-link-platforms` | draft |
|
||||||
|
| - | - | `prd/mobile-app` | draft (blocked on `prd/notification-system` for push) |
|
||||||
| 1 | - | `prd/notification-system` | todo |
|
| 1 | - | `prd/notification-system` | todo |
|
||||||
| 2 | 14 | `prd/course-templates` | todo |
|
| 2 | 14 | `prd/course-templates` | todo |
|
||||||
| 3 | 5 | `prd/course-import` | todo |
|
| 3 | 5 | `prd/course-import` | todo |
|
||||||
|
|||||||
@@ -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
@@ -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 & 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>
|
||||||
@@ -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 & 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 & 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 & 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 & 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>
|
||||||
@@ -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 & newsfeed</h1>
|
||||||
|
<p>
|
||||||
|
Org and course Q&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&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&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>
|
||||||
@@ -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 & 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 & 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>
|
||||||
@@ -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 & 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 & 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>
|
||||||
@@ -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 & 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 & 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 & 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 & 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>
|
||||||
@@ -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 & 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>
|
||||||
@@ -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 & 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 & 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>
|
||||||
@@ -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 & 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 & 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 & newsfeed</div>
|
||||||
|
<p class="d">Mentions and followed threads, a Q&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 & 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 & 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 & 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>
|
||||||
@@ -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 & 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 & 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 & 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&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>
|
||||||
@@ -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 & 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 & 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>
|
||||||
@@ -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 & 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 & 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 & 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 & 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 & 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>
|
||||||
@@ -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 & 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 & 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 & 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>
|
||||||
Vendored
+118
@@ -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));
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -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>
|
||||||
@@ -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 & 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&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 & 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&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>
|
||||||
@@ -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 & 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>
|
||||||
Reference in New Issue
Block a user