Files
classroomio/prototypes/mobile-app/proto.js
T
967e81d33f 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]>
2026-08-18 12:02:35 +01:00

119 lines
5.6 KiB
JavaScript

/*
* 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));
});
})();