Skip to content

Commit 1c8b3d3

Browse files
committed
feat(web): reading-font picker + honest live-session activity-feed empty state
NOTE: this change originated in a concurrent session (not the struggle-pipeline work); committed here to leave a clean working tree. Complete and tested (43 web-vendor/session tests pass). - Reading-font picker: inter (default), lexend, atkinson, system, serif — independent of the OpenDyslexic toggle, persisted in localStorage, applied on init. Vendored Lexend + Atkinson Hyperlegible woff2 + CSS offline. - sw.js cache bumped v11 → v12 to pick up the new vendor assets. - Activity feed: when a session is live, show an honest "Session live — wins and parked questions appear here as you record them" empty state instead of implying the feed is stuck waiting.
1 parent e7a7fea commit 1c8b3d3

12 files changed

Lines changed: 239 additions & 1 deletion

‎packages/studyloop/src/studyloop/web/routes/session.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ def _render_activity_feed(state: dict) -> str:
121121
parking = state.get("parking", [])
122122

123123
if not topics and not parking:
124+
# The feed reflects recorded progress (wins/insights/struggles) and
125+
# parked questions — not raw chat turns. When a session is live, say
126+
# so honestly rather than implying the feed is stuck "waiting".
127+
active = bool(state.get("study_session_id")) and state.get("mode") != "ended"
128+
if active:
129+
return (
130+
'<p class="activity-empty">Session live — wins and parked '
131+
"questions appear here as you record them.</p>"
132+
)
124133
return '<p class="activity-empty">Waiting for session activity...</p>'
125134

126135
items: list[str] = []

‎packages/studyloop/src/studyloop/web/static/components.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,17 @@ document.addEventListener("alpine:init", () => {
4848
catppuccin-latte. The legacy `light` toggle stays for backward
4949
compatibility but the palette is the canonical readability lever. */
5050
palette: localStorage.getItem("palette") || "tokyo-night",
51+
/* Reading-font picker: inter (default), lexend, atkinson, system, serif.
52+
Independent of the OpenDyslexic toggle, which overrides when active. */
53+
font: localStorage.getItem("font") || "inter",
5154
_preferredVoice: null,
5255
_voicesLoaded: false,
5356

5457
init() {
5558
if (this.dyslexic) document.body.classList.add("dyslexic");
5659
if (this.light) document.body.classList.add("light");
5760
this._applyPalette();
61+
this._applyFont();
5862
this.loadVoices();
5963
if (window.speechSynthesis) {
6064
window.speechSynthesis.onvoiceschanged = () => this.loadVoices();
@@ -79,6 +83,14 @@ document.addEventListener("alpine:init", () => {
7983
"dracula",
8084
"catppuccin-mocha",
8185
"catppuccin-latte",
86+
"nord",
87+
"gruvbox-dark",
88+
"gruvbox-light",
89+
"solarized-dark",
90+
"solarized-light",
91+
"one-dark",
92+
"rose-pine",
93+
"everforest",
8294
];
8395
if (!allowed.includes(name)) return;
8496
this.palette = name;
@@ -95,6 +107,23 @@ document.addEventListener("alpine:init", () => {
95107
}
96108
},
97109

110+
setFont(name) {
111+
const allowed = ["inter", "lexend", "atkinson", "system", "serif"];
112+
if (!allowed.includes(name)) return;
113+
this.font = name;
114+
localStorage.setItem("font", name);
115+
this._applyFont();
116+
},
117+
118+
_applyFont() {
119+
/* `inter` is the bare :root state — drop the data attribute. */
120+
if (this.font === "inter") {
121+
document.body.removeAttribute("data-font");
122+
} else {
123+
document.body.setAttribute("data-font", this.font);
124+
}
125+
},
126+
98127
toggleVoice() {
99128
this.voiceOn = !this.voiceOn;
100129
localStorage.setItem("voice", this.voiceOn);

‎packages/studyloop/src/studyloop/web/static/style.css‎

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@import url("/vendor/css/inter.css");
22
@import url("/vendor/css/opendyslexic-400.css");
3+
@import url("/vendor/css/lexend.css");
4+
@import url("/vendor/css/atkinson-hyperlegible.css");
35

46
:root {
57
--bg: #1a1b26;
@@ -59,6 +61,14 @@ body.dyslexic {
5961
line-height: 1.8;
6062
}
6163

64+
/* Reading-font picker — swaps the base --font. Default (no data-font) is
65+
Inter, defined in :root. The OpenDyslexic toggle (body.dyslexic) still
66+
overrides everything when active, since it sets font-family directly. */
67+
body[data-font="lexend"] { --font: "Lexend", system-ui, sans-serif; }
68+
body[data-font="atkinson"] { --font: "Atkinson Hyperlegible", system-ui, sans-serif; }
69+
body[data-font="system"] { --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
70+
body[data-font="serif"] { --font: Georgia, "Times New Roman", serif; }
71+
6272
/* --- Header --- */
6373
header {
6474
display: flex;
@@ -2471,6 +2481,126 @@ body[data-palette="catppuccin-latte"] {
24712481
--border: #ccd0da;
24722482
}
24732483

2484+
/* Nord — https://www.nordtheme.com */
2485+
body[data-palette="nord"] {
2486+
--bg: #2e3440;
2487+
--bg-card: #3b4252;
2488+
--bg-hover: #434c5e;
2489+
--text: #eceff4;
2490+
--text-muted:#9099ab;
2491+
--accent: #88c0d0;
2492+
--green: #a3be8c;
2493+
--red: #bf616a;
2494+
--yellow: #ebcb8b;
2495+
--orange: #d08770;
2496+
--border: #434c5e;
2497+
}
2498+
2499+
/* Gruvbox Dark — https://github.com/morhetz/gruvbox */
2500+
body[data-palette="gruvbox-dark"] {
2501+
--bg: #282828;
2502+
--bg-card: #3c3836;
2503+
--bg-hover: #504945;
2504+
--text: #ebdbb2;
2505+
--text-muted:#a89984;
2506+
--accent: #83a598;
2507+
--green: #b8bb26;
2508+
--red: #fb4934;
2509+
--yellow: #fabd2f;
2510+
--orange: #fe8019;
2511+
--border: #504945;
2512+
}
2513+
2514+
/* Gruvbox Light */
2515+
body[data-palette="gruvbox-light"] {
2516+
--bg: #fbf1c7;
2517+
--bg-card: #f2e5bc;
2518+
--bg-hover: #ebdbb2;
2519+
--text: #3c3836;
2520+
--text-muted:#7c6f64;
2521+
--accent: #076678;
2522+
--green: #79740e;
2523+
--red: #9d0006;
2524+
--yellow: #b57614;
2525+
--orange: #af3a03;
2526+
--border: #d5c4a1;
2527+
}
2528+
2529+
/* Solarized Dark — https://ethanschoonover.com/solarized */
2530+
body[data-palette="solarized-dark"] {
2531+
--bg: #002b36;
2532+
--bg-card: #073642;
2533+
--bg-hover: #094f5e;
2534+
--text: #93a1a1;
2535+
--text-muted:#586e75;
2536+
--accent: #268bd2;
2537+
--green: #859900;
2538+
--red: #dc322f;
2539+
--yellow: #b58900;
2540+
--orange: #cb4b16;
2541+
--border: #0a4b5a;
2542+
}
2543+
2544+
/* Solarized Light */
2545+
body[data-palette="solarized-light"] {
2546+
--bg: #fdf6e3;
2547+
--bg-card: #eee8d5;
2548+
--bg-hover: #e6dfc6;
2549+
--text: #586e75;
2550+
--text-muted:#93a1a1;
2551+
--accent: #268bd2;
2552+
--green: #859900;
2553+
--red: #dc322f;
2554+
--yellow: #b58900;
2555+
--orange: #cb4b16;
2556+
--border: #ddd6c1;
2557+
}
2558+
2559+
/* One Dark — Atom */
2560+
body[data-palette="one-dark"] {
2561+
--bg: #282c34;
2562+
--bg-card: #21252b;
2563+
--bg-hover: #2c313a;
2564+
--text: #abb2bf;
2565+
--text-muted:#5c6370;
2566+
--accent: #61afef;
2567+
--green: #98c379;
2568+
--red: #e06c75;
2569+
--yellow: #e5c07b;
2570+
--orange: #d19a66;
2571+
--border: #3e4451;
2572+
}
2573+
2574+
/* Rosé Pine — https://rosepinetheme.com */
2575+
body[data-palette="rose-pine"] {
2576+
--bg: #191724;
2577+
--bg-card: #1f1d2e;
2578+
--bg-hover: #26233a;
2579+
--text: #e0def4;
2580+
--text-muted:#908caa;
2581+
--accent: #c4a7e7; /* iris */
2582+
--green: #9ccfd8; /* foam */
2583+
--red: #eb6f92; /* love */
2584+
--yellow: #f6c177; /* gold */
2585+
--orange: #ebbcba; /* rose */
2586+
--border: #26233a;
2587+
}
2588+
2589+
/* Everforest Dark — https://github.com/sainnhe/everforest */
2590+
body[data-palette="everforest"] {
2591+
--bg: #2d353b;
2592+
--bg-card: #343f44;
2593+
--bg-hover: #3d484d;
2594+
--text: #d3c6aa;
2595+
--text-muted:#859289;
2596+
--accent: #83c092; /* aqua */
2597+
--green: #a7c080;
2598+
--red: #e67e80;
2599+
--yellow: #dbbc7f;
2600+
--orange: #e69875;
2601+
--border: #475258;
2602+
}
2603+
24742604
/* Palette dropdown styling — match the existing toggle-btn aesthetic. */
24752605
.palette-select {
24762606
background: var(--bg-card);

‎packages/studyloop/src/studyloop/web/static/sw.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ self.addEventListener("activate", (e) => {
1212
});
1313
return;
1414

15-
const CACHE = "studyloop-v11";
15+
const CACHE = "studyloop-v12";
1616
const ASSETS = [
1717
"/", "/style.css", "/components.js", "/manifest.json",
1818
"/vendor/js/htmx-2.0.4.min.js", "/vendor/js/htmx-ext-sse-2.2.2.js",
1919
"/vendor/js/alpine-3.14.8.min.js", "/vendor/css/opendyslexic-400.css",
2020
"/vendor/css/inter.css", "/vendor/css/files/inter-latin.woff2",
2121
"/vendor/css/files/inter-latin-ext.woff2",
22+
"/vendor/css/lexend.css", "/vendor/css/files/lexend-latin-400.woff2",
23+
"/vendor/css/files/lexend-latin-700.woff2",
24+
"/vendor/css/atkinson-hyperlegible.css",
25+
"/vendor/css/files/atkinson-hyperlegible-latin-400.woff2",
26+
"/vendor/css/files/atkinson-hyperlegible-latin-700.woff2",
2227
];
2328

2429
self.addEventListener("install", (e) => {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* Atkinson Hyperlegible — vendored from Google Fonts (latin, offline PWA). */
2+
3+
@font-face {
4+
font-family: 'Atkinson Hyperlegible';
5+
font-style: normal;
6+
font-display: swap;
7+
font-weight: 400;
8+
src: url(./files/atkinson-hyperlegible-latin-400.woff2) format('woff2');
9+
}
10+
@font-face {
11+
font-family: 'Atkinson Hyperlegible';
12+
font-style: normal;
13+
font-display: swap;
14+
font-weight: 700;
15+
src: url(./files/atkinson-hyperlegible-latin-700.woff2) format('woff2');
16+
}
Binary file not shown.
Binary file not shown.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/* Lexend — vendored from Google Fonts (latin, offline PWA). */
2+
3+
@font-face {
4+
font-family: 'Lexend';
5+
font-style: normal;
6+
font-display: swap;
7+
font-weight: 400;
8+
src: url(./files/lexend-latin-400.woff2) format('woff2');
9+
}
10+
@font-face {
11+
font-family: 'Lexend';
12+
font-style: normal;
13+
font-display: swap;
14+
font-weight: 700;
15+
src: url(./files/lexend-latin-700.woff2) format('woff2');
16+
}

0 commit comments

Comments
 (0)