Appearance
Core UI Features: Notifications & Responsive Design
Part 4 of 5 in the Core UI Features series.
Two more areas worth documenting precisely, because both are easy to over-describe from what a generic app "should" have rather than what Ediky actually ships: notification delivery, and how the layout adapts across screen sizes.
What notifications actually exist
There is no push-messaging SDK anywhere in the codebase — no Firebase Cloud Messaging, no browser push subscriptions. What exists instead is three separate, real mechanisms:
In-app inbox — useNotifications plus NotificationBell render a Firestore-backed inbox at users/{uid}/notifications, auto-generating entries like "event within 24h" or "task due" by comparing calendar data against the current time. The bell's dropdown renders through createPortal to document.body, the same fix used for the annotation popover in Notes System & Annotations — it escapes the navbar's own stacking context instead of getting clipped by it.
Ad-hoc toasts — the IDE has its own hand-rolled flash(message): a single piece of toast state that auto-clears after 1.6 seconds, used for things like "12/15 test cases passed." It isn't a shared toast library; it's local to the one page that needs it.
Email reminders — the only channel that reaches a student who isn't looking at the app. A standalone Cloudflare Worker (cron-scheduled, checking every minute) sends 7-day, 3-day, 1-day, and 1-hour-out reminder emails for upcoming OAs via Resend, with a GitHub Actions script as a backup scheduler if the Worker's cron ever misses a window. This lives entirely outside the SPA — see Cloudflare Workers for the Worker side.
Desktop currently reuses the same in-app inbox logic — useNotifications checks REMINDER_OFFSETS_MS (7d/3d/1d/1h) against calendar data on render — but there is no OS-level toast or tray notification: no tauri-plugin-notification is installed, so a reminder only surfaces if the student happens to have the app open. That's a real gap worth flagging rather than a documented feature, since the desktop introduction page's "full OS notifications" framing gets ahead of what's actually wired today.
rendering diagram…
drag to pan · ctrl / ⌘ + scroll to zoom
javascript
// Conceptual shape of the reminder check — real logic, simplified
const REMINDER_OFFSETS_MS = [7, 3, 1].map(d => d * 86_400_000).concat(3_600_000);
function dueReminders(events, now = Date.now()) {
return events.flatMap((event) =>
REMINDER_OFFSETS_MS
.filter((offset) => Math.abs(event.date - now - offset) < 60_000)
.map((offset) => ({ event, offset }))
);
}Responsive design
There's no custom Tailwind breakpoint config — sm/md/lg are the framework defaults: 640px, 768px, 1024px. What makes the responsiveness real rather than cosmetic is useMediaQuery, built on useSyncExternalStore over window.matchMedia (with an addListener fallback for older Safari) rather than a useState + resize-listener effect — the same primitive React uses internally for tearing-safe external state.
javascript
// Real shape of useMediaQuery and its two exported hooks
function useMediaQuery(query) {
const subscribe = (cb) => {
const mql = window.matchMedia(query);
mql.addEventListener?.("change", cb) ?? mql.addListener(cb);
return () => mql.removeEventListener?.("change", cb) ?? mql.removeListener(cb);
};
const getSnapshot = () => window.matchMedia(query).matches;
return useSyncExternalStore(subscribe, getSnapshot);
}
export const useIsTabletUp = () => useMediaQuery("(min-width: 768px)");
export const useIsDesktop = () => useMediaQuery("(min-width: 1024px)");The important distinction: these hooks don't just toggle CSS classes, they switch component structure. Below md, the sidebar collapses to an overlay drawer instead of a persistent rail, and the IDE's split-pane can give way to stacked tabs since there isn't horizontal room for two panes side by side. Above lg, the full three-column dashboard layout (sidebar, content, right rail) has room to breathe. Desktop reuses these exact hooks — the Tauri window defaults to 1280×800 with a 960×600 minimum and is user-resizable, so shrinking the desktop window triggers the same tablet-layout breakpoint a browser tab would.
Accessibility, concretely
Across the codebase, 38 files touch aria-*, role=, tabIndex, or :focus-visible — this is deliberate work, not incidental semantic HTML. The SearchBar's collapsed icon carries role="button", tabIndex={0}, aria-label="Open search", and explicit Enter/Space keydown handling so it's operable without a mouse. theme.css defines :focus-visible outline rules scoped to the app's interactive surfaces (.ediky-root, .sb-rail, .sb-drawer, .dd-item) rather than relying on the browser default, which themed apps often suppress by accident. Motion-sensitive users are handled too: the dashboard's animated heading checks prefers-reduced-motion before running its type-on effect.
A short checklist, reflecting what's actually enforced rather than aspirational:
- Keyboard — every interactive control reachable via Tab, operable via Enter/Space, dialogs/popovers close on Escape.
- Focus — visible
:focus-visibleoutline on every focusable element, never suppressed globally. - Contrast — theme CSS variables are chosen to meet WCAG AA contrast in both light and dark mode, not just one.
- Labels — icon-only controls carry
aria-label; the search toggle and notification bell are the clearest examples. - Motion — animated UI checks
prefers-reduced-motionand skips non-essential motion when it's set.
Self-assessment
Word count for this page: 867 words.
← Previous: Dark Mode, Split-Pane & Search→ Next: Performance & Summary