From 35bc9a3d3a72541088851420e4ef51e8cc10a58f Mon Sep 17 00:00:00 2001 From: Konstantine Tsafatinos Date: Mon, 19 Oct 2026 23:58:40 -0400 Subject: [PATCH 1/4] feat: ship site branding (stylesheet, Rubik, logos, favicon) with nma-kit sync-assets and nma.brand colours --- AGENTS.md | 34 ++- README.md | 70 +++++- js/src/cells/assets/cells.css | 3 +- js/src/cells/assets/page.css | 2 +- js/src/cells/src/editor.mjs | 2 +- python/README.md | 2 +- python/src/nma_kit/assets/favicon.png | Bin 0 -> 11780 bytes python/src/nma_kit/assets/fonts/OFL.txt | 93 ++++++++ .../assets/fonts/Rubik-400-italic.woff2 | Bin 0 -> 19140 bytes .../assets/fonts/Rubik-400-normal.woff2 | Bin 0 -> 18936 bytes .../assets/fonts/Rubik-500-normal.woff2 | Bin 0 -> 19036 bytes .../assets/fonts/Rubik-700-normal.woff2 | Bin 0 -> 19112 bytes python/src/nma_kit/assets/logo-dark.svg | 16 ++ python/src/nma_kit/assets/logo.svg | 9 + python/src/nma_kit/assets/site.css | 205 ++++++++++++++++++ python/src/nma_kit/branding.py | 161 ++++++++++++++ python/src/nma_kit/cli.py | 18 +- python/src/nma_kit/convert.py | 3 + tests/e2e/test_branding_browser.py | 31 +++ tests/example-course/_static/custom.css | 7 +- tests/example-course/myst.yml | 9 +- tests/python/test_branding.py | 153 +++++++++++++ 22 files changed, 798 insertions(+), 20 deletions(-) create mode 100644 python/src/nma_kit/assets/favicon.png create mode 100644 python/src/nma_kit/assets/fonts/OFL.txt create mode 100644 python/src/nma_kit/assets/fonts/Rubik-400-italic.woff2 create mode 100644 python/src/nma_kit/assets/fonts/Rubik-400-normal.woff2 create mode 100644 python/src/nma_kit/assets/fonts/Rubik-500-normal.woff2 create mode 100644 python/src/nma_kit/assets/fonts/Rubik-700-normal.woff2 create mode 100644 python/src/nma_kit/assets/logo-dark.svg create mode 100644 python/src/nma_kit/assets/logo.svg create mode 100644 python/src/nma_kit/assets/site.css create mode 100644 python/src/nma_kit/branding.py create mode 100644 tests/e2e/test_branding_browser.py create mode 100644 tests/python/test_branding.py diff --git a/AGENTS.md b/AGENTS.md index c0853c9..91e9cd6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,8 @@ python/src/nma_kit/ convert.py # content rules, jupytext conversion, badges, prepare-site media.py solutions.py feedback.py scrolly.py cells.py # Python side of each feature pin.py # pin course-kit CDN URLs in myst.yml to one version + branding.py # sync-assets: _nma/site.css (+ brand colours, extra CSS), logos, favicon + assets/ # the branding source: site.css, logos, favicon, fonts/ (Rubik, OFL) publish.py # push notebooks/ to the notebooks branch (orphan branch if missing) tests/ python/ js/ e2e/ # unit tests (pytest, node:test) and the browser smoke test @@ -88,9 +90,35 @@ templates/wheels.yml # optional caller for course-wheels.yml - **One version everywhere.** `tests/python/test_release.py` checks that `js/package.json`, `python/pyproject.toml`, `nma_kit.__version__`, `.release-please-manifest.json` and `KIT_VERSION` in the reusable workflows agree. -- Courses keep one rule in their own CSS (a plugin can't add site-wide CSS): - `.myst-jp-nb-toolbar { display: none !important; }` (editable cells replace - JupyterLite's power button). +- The toolbar rule `.myst-jp-nb-toolbar { display: none !important; }` + (editable cells replace JupyterLite's power button) is in the kit's + `site.css`; courses no longer carry it. + +## Site branding (internals) + +- **Source of truth**: `python/src/nma_kit/assets/` (`site.css`, logos, favicon, + `fonts/`). It lives inside the Python package, not in a top-level folder, + because `uv build` makes the wheel from the sdist, which can't include files + from outside `python/`. `tests/python/test_branding.py` checks they ship. +- **`nma-kit sync-assets`** (also run by `prepare-site`, not by `--dry-run`) + writes `_nma/` in the course: `site.css`, `logo.svg`, `logo-dark.svg`, + `favicon.png`, and a `.gitignore` containing `*`, so courses never commit it. +- **Courses name the files themselves** in `site.options` (`style`, `logo`, + `logo_dark`, `favicon`). The kit's `nma.yml` does not set them: MyST lets the + course's values win over an extended file's, and how it resolves relative + file paths of a *remote* extended config is unverified. Don't move them into + `nma.yml` without testing that in a real course. +- **One `style` slot**: `site.css` = kit CSS (Rubik embedded as data URIs, so + no relative `url()` depends on where MyST serves the file and no request + leaves the site) + `:root` overrides from `nma.brand` + the course's + `nma.style.extra` file. +- **CSS variables are the API**: `--nma-brand-*` (the five colours a course may + change), `--nma-cell-*` and `--nma-tok-*` (read by `cells/assets/cells.css`). + Syntax-token colours are fixed on purpose (chosen for contrast). +- Settings are validated by `branding.brand_errors` inside `check`. +- `tests/scripts/build-example.sh` ends with `git checkout -- tutorials myst.yml` + to undo `prepare-site`: stage (`git add`) edits to the example course's + `myst.yml` before running it, or they are reverted too. ## Editable cells (internals) diff --git a/README.md b/README.md index 3ce5451..a3c8080 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ in one versioned package: `{stage}`) and editable, runnable code cells. - **`nma-kit` CLI**: content checks, notebook generation (student, instructor, solutions), scrolly pre-rendering, site preparation, notebook publishing. +- **Site branding**: the Neuromatch stylesheet, Rubik font, logos and favicon, + written into a course by `nma-kit sync-assets`. - **Reusable GitHub workflows**: check pull requests, publish notebooks and the site to GitHub Pages. @@ -27,6 +29,16 @@ which teaches the authoring conventions. project: github: https://github.com// # ... + site: + template: book-theme + options: + folders: true + logo_text: My Course Title # your course's name next to the logo + # The kit's branding, written to _nma/ by `nma-kit sync-assets`: + style: _nma/site.css + logo: _nma/logo.svg + logo_dark: _nma/logo-dark.svg + favicon: _nma/favicon.png ``` 2. Copy [`templates/course.yml`](templates/course.yml) to @@ -35,16 +47,21 @@ which teaches the authoring conventions. the site. 3. In the repo's **Settings → Pages**, set **Source** to **GitHub Actions** (and the custom domain, if any; no `CNAME` file needed). -4. Keep this rule in your site CSS (a plugin can't add site-wide CSS): +4. Generate the branding files once for local previews (CI does it for you): - ```css - .myst-jp-nb-toolbar { display: none !important; } + ```bash + uv run nma-kit sync-assets ``` + This writes `_nma/` (stylesheet, logos, favicon). The folder ignores itself, + so nothing gets committed. Re-run it after upgrading the kit. See + [Site branding](#site-branding). + 5. List the packages your tutorials import in `requirements.txt` (CI installs them next to the kit; `nma-kit render-scrolly` executes scrolly pages). -Optional settings, in the `nma:` block of `myst.yml` (defaults shown): +Optional settings, in the `nma:` block of `myst.yml` (defaults shown, except +`brand` and `style`, which are unset by default): ```yaml nma: @@ -58,8 +75,52 @@ nma: user_key: ... scrolly: pyodide_url: https://cdn.jsdelivr.net/pyodide/v314.0.7/full/ + brand: # colours, see "Site branding" + primary: "#008CBF" + style: + extra: _static/extra.css # your own CSS, appended after the kit's ``` +## Site branding + +The kit carries the Neuromatch look: the colour palette, the Rubik font +(embedded in the stylesheet, so readers' browsers don't contact Google), the +logos and the favicon. It also styles the kit's own widgets (editable cells, +scrollytelling) and hides JupyterLite's toolbar, so courses no longer carry +that rule themselves. + +`nma-kit sync-assets` writes these files to `_nma/` in the course: + +| File | Used as | +|---|---| +| `_nma/site.css` | `site.options.style`: the kit stylesheet, then your colour overrides, then your own CSS | +| `_nma/logo.svg`, `_nma/logo-dark.svg` | `site.options.logo`, `logo_dark` | +| `_nma/favicon.png` | `site.options.favicon` | + +CI runs it as part of `nma-kit prepare-site`. Locally, run it once and again +after upgrading the kit. MyST accepts a single `style` file, which is why the +kit stylesheet and your own CSS are combined into `_nma/site.css`. + +**Change the colours** with the `nma.brand` block (all keys optional, each a +`"#RRGGBB"` string): + +```yaml +nma: + brand: + primary: "#D9480F" # links, accents, buttons + primary_hover: "#A63700" # default: primary, 25% darker + primary_light: "#FF8A57" # accent on dark backgrounds, default: primary, 35% lighter + secondary: "#45429C" # header surface, tips + accent: "#FA7A7A" # exercise cells +``` + +**Add your own CSS** with `nma.style.extra`: the file (relative to the course +root) is appended after the kit's CSS, so its rules win. The brand colours are +CSS variables (`--nma-brand-primary`, ...), so your CSS can use them too. + +`nma-kit check` reports unknown keys and malformed colours. The course title +and `logo_text` stay in your `myst.yml`. + ## Versions and updates The kit is on **0.x** until it is stable. A breaking change bumps the minor @@ -95,6 +156,7 @@ Local previews cache the downloaded plugin for 30 days in `_build/cache`; ```bash uv venv && uv pip install -r requirements.txt neuromatch-course-kit==0.1.* uv run nma-kit check +uv run nma-kit sync-assets # site branding, written to _nma/ uv run nma-kit render-scrolly # optional: real outputs on scrolly pages uv run myst start ``` diff --git a/js/src/cells/assets/cells.css b/js/src/cells/assets/cells.css index c9927e4..6eaf4fe 100644 --- a/js/src/cells/assets/cells.css +++ b/js/src/cells/assets/cells.css @@ -1,6 +1,7 @@ /* Editable cell panel (in each panel's shadow root). Layout: header, editor, action bar, output. Colours follow the site theme. - Public tokens, set on :root / html.dark in _static/custom.css: + Public tokens, set on :root / html.dark in the kit stylesheet (python/src/nma_kit/assets/site.css); + a course overrides them in its own CSS (nma.style.extra): --nma-cell-{bg,fg}-{light,dark}, --nma-cell-head-{bg,fg}-{light,dark}, --nma-cell-bar-bg-{light,dark}, --nma-cell-out-bg-{light,dark}, --nma-cell-gutter-{bg,fg}-{light,dark}, --nma-cell-active-line-{light,dark}, diff --git a/js/src/cells/assets/page.css b/js/src/cells/assets/page.css index 3991697..0011863 100644 --- a/js/src/cells/assets/page.css +++ b/js/src/cells/assets/page.css @@ -2,7 +2,7 @@ .nma-cells-runtime { display: none; } /* Floating "Run all" control (runtime-widget.mjs). Brand-coloured via the theme - variables set in _static/custom.css, with safe fallbacks. */ + variables set in the kit stylesheet (site.css), with safe fallbacks. */ .nma-run-all-bar { position: fixed; right: 1rem; diff --git a/js/src/cells/src/editor.mjs b/js/src/cells/src/editor.mjs index 6ce2483..6debd69 100644 --- a/js/src/cells/src/editor.mjs +++ b/js/src/cells/src/editor.mjs @@ -1,5 +1,5 @@ // CodeMirror 6 editor for one cell. Colors come from CSS variables (cells.css), -// so a course restyles panels from _static/custom.css without touching JS. +// so a course restyles panels from its own CSS (nma.style.extra) without touching JS. // No basicSetup: the explicit list is smaller and leaves out search/autocomplete popups. import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection } from '@codemirror/view'; diff --git a/python/README.md b/python/README.md index 2982730..69d021d 100644 --- a/python/README.md +++ b/python/README.md @@ -1,5 +1,5 @@ # neuromatch-course-kit The `nma-kit` command line tool for Neuromatch MyST courses: notebook -generation, content checks and site preparation. See the +generation, content checks, site branding and site preparation. See the [course kit repository](https://github.com/neuromatch/course-kit) for docs. diff --git a/python/src/nma_kit/assets/favicon.png b/python/src/nma_kit/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..fbaeba66c6c80ce8736ea8c55e1e206784c2d30f GIT binary patch literal 11780 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

87SoyFlB(RXlolI9 zWpFPQffOkMDgnXAP@(f)%)5oaFMxMv_+ybG3J@t m4cDW3<;gVyvPh9~rT+(4u~q`Tt}BxO0000x8c0A9D|E0Z z03a(6F>nL~2tH`AB6t)M3^{K>VxO%C1{D?3ayKSXAFgRV1Lx@I z#;`B-54+{h;uJ3U-(R1%nUh%=^JKvksR9x9K&7IY>*C6Nh-bZgLV2k@RPZ;IXrX3; zcyYkl#oA}H(z=+E6^VnZ@Te!ims;#Q`} zH|Av>$^IS&0TX~KkatjAJim~{EFw_L6&M2Cs2R%~Sg=T7&PvUDimRAlwM7h|gWJmDWkkn4^sitER%B@4A=PxP*&#beI>7MQJlkAxX?ArP3DS0p_PBEu^g!3zq zMh+|JfK+1*H1)hnmKzHhA4;Xf?{%X{)=2ZhNVjdGC?k*q!pDy$fZrRXzn9px?sJPc z&vC@R*KRUc&Qn>j?Q_d8?L+^a*_I8>DH=vKEMLqZOB1lGIz=^-O)DB`hx@)XTrYr8 zy62?L?P)a+OY2=>{VQ3{Wu>i)=1#;>>-GCY!=5yKoehN!45`r>u_FxYgW*9t(?Dn> z5?<7a3*V&7Zd{N0+cqlN*Y||Erz6N&*fCDi6}h*9SgjEwv?i!l+8c5n$#X+2P*OS_ zp*I`T)no@?%Sdq<80H6}iq}%@8r80(y!^6&yB+?Cfk|AkgYxcf8uPzI+1}e;?n-oH zzZqwh6e8~HkV7tbmoR%Kv{<`z$9v7Dokb;V{)4Tzly!MRAp6Q=bcWHFS4Rj_IMZLdR zETK|Rttjx>R5(sI7Gez6c$$TdYi_(%Vd|sHg7x>I+P@=H%C}#azJuiQy1mjn953Oo zYd`L$;NJq_w8FQBn1}TSUb4ibA19hLq`$p^(1Y4mEBj6(U?HcW330YvY=IoJ!Woqm zuZk}N?+D|_h6@k_!bGd2g$?yIw$sMAvsW~```Mb0j)*|>Bap9hjndv{%zmn#50=7S z-llG?0j7yVhtd%!qi-sPWeTh-v{WS233U&ufCcW=>BB-7Ah(PMr*PfGLgIAh3+lBDHt!H<`>JhZjbe{D0~vdO#l9{-k8 zs8My6pw&EJUxgH#)ExoPG4rZf=dphYxw4}&)jtW z!ZQ%XkMQcHrWmnuNEa(VaXcWjmv0b9PVx7-niQtgvrL1Zso|7lylq#?zfK4V7gcT* zdSXjl02INLV(KAM+rC*X;rcbNPw3bz2hjEqPZVJgVEG?yAdOOT`f-evaV{+$+%Ns*b3=Xs1s$-vP84^oH7Ag&3S&~9?WjtI`pM0iiUP&l} z7st=+w>x%!zWmm1)$fcxeBJWkG&kBbpa)9Bp8VkfjyXmmZ|ccW?dA|%^to}isZ6Ro zs%%KN=eh=NWix*n>qam^ExQpUK4+{6=h|x^H!Dy+mTB0K{_b^NQ&eDL!9_(~6tBlR z3dn)iMtu%$d#SJ*vZP?CAUVi}au{>$pD#GV&9+u4?i2Sg+R1J%cDq`_$0~CWzt~;X zYvJ{u@0XKZjRE>AW++yz>yIoIoFx7*{f`uap?B__P{oO zSbk2JkW|q$(dE8OUm3vlxi`AHiYI*SU)~~3!Z2n)L#a8OB|*&io@`a>Ys7TQtVg~- zs7fIjuHBLEkp(RxDS$#C%h2dF!j)T^FaM!3FfCcF)(plYzKn@Sr%!Tkzx{#|V+$8C zU>H)P4G)AIG=Oe21T~O>Nr-bKIDmFMggzWVKOTV}&EcWo14xOCsOBRCA0D#NP#xG^ z6F>ipNW`D;@v3l+pSCw18C~Wj(gIwJrZ=5E$}GXC6<#qfCvD#_z%Z;MZQs0K4pG>1 zo9_=dyoF}AB3Ujys;JEts#py(CKb`S}MusUwX9}~Ga(AYT8x^ z81cxM%S9!?ZVnyHjLv@IvR~?BvcLp2^OcSasV@k(uueux8($K39X}&e+yEA4{paFhNe&qCYLYdukeDu@5hOo z+~bj>FW3jz!_y9|IZgND)E4%mih;W+1RlISAH~@$>JSXDcU{)ocqlgkZONj?N|E4w zjG<5>NkGE#x{7iZ=c1ohFVU6IWLW(DLhYfVrb2?Eu5GbJz>h$e2 z$7Cx(CH_;(OX|x$mR=nFH4?+9My()*DaaPzMZ9AlRc9F*QXqr%hY&d6pGMGSsL9gB z)0}lKHI0&}64)X%vviAp#%Osf9_x)|bjv)eZtNF97fpzg^)7YEy;QQ{cMwD(2xoYt ze@uW{Cl_u4I_sNv)zY^IK?)i5ZnKom3=kwkVCC6-*&7TSIzE^jqwEh^npQuLP7hsS zV;3c-DMXsEN>u>Wjb9Pj`n!N3j>E?@{@N*auws=Z5gD>Uw4aR>vK zgOb$k{*VC^2SHj61c9cAP?kt6kxZfux?Pse?S9vcgRQ1-Gu4SiDI1oyAXk#D6D+Az z9*WOo)R@P_9X-+qTIa0=^luFOYrbf$jegk2LvV6 z<6DUpiC2tqDu`h(7LIeu{IQ6Y*!#X|8kP<60 zbEjaJ;C0l#$BM{k#Q|6s;X*tbTRInfE=qE@`x6$NywV3zyigAaBpPG&!v>0xf$n^5 z;&_^Jl8545F@X-44j-~IhMq){VlqXUT7lw9eL$IN4etDWDWc3N{VmG|V<{_^g@Ba> zxXbx+$yXP9_M=2{H%8#>P9FMdTf$>`>h39A!?0W(If_W(cN*Du9R(KU3 z>SX6Hm;ybxBt1J&grHI3XMSA)G_bivVTppqRfn`F3sI(&X5#7>_Lq?ZW%OvBfwU%z z=OEXIG6A*iiVT4)sgliUK+FH-vQ*LR6f!FN3yOHK7eW<&wqM``x`|sQ-O>h9RDo5O zr{(R~`{saXqeZ#guip$O_{raAvI}pvd-UHmW`sAl#~jZFk%5xX?SA)avFt6tu$n*_ zGzoM`9@786#F0%!Dw#}Ps*v|{UpYu4^)@VsGNKQ&8Uf=9wZ(!X`f1QPvKbqP%%1KL z>QQDPP`j$x#$mB(F|KzhYPv9dZ6JE*hC_Ie)lP8MK5C}ojhwJxeSePMEyZD($)#Aw z)IE@nRTrmEr;Mpxlkpp>9fTlh^5a-lM|EUCG2B`#;!|{arVOcE*XLDkl<%!yA|Dl3 zA5&7-)Tkg1UNWJakaawsOsdOA1DnBTV$jm*Bh#c?)n%MKW?$vNPJ7i8^JRr`Kpo_{ zZmk|O<`KC~^ouW1mHN=mf;p+=hof=`X0`~D&Fz2XlhlDm*5TzT zW`4{hQYQ=}#;SkI+|p6asjk{#vO1l8U3OJ|M!(|?TC&t+Q!O=EC#$twCXMq&uMY<1 zesBw5o&!<+zSYd@xG4Y2@;=O^*IJ|=?DObEb1_AkR;rf&=>X@JrNXHXDjdokm>vyb z-Ls_{VJIPoiiTnpj3>{O%9WBW8nCV_$p@N3FYREV|I?iwx@5df37{)A@=Yvqtb>}R zod0{XUSDQ@)8dRMnATXIQ}iZoW4Y1HKMj7^T=qa@Q^R_-xE(4^mAPCaJDeG33B8{T z;dEo&+V|-K^hiDd{u6#^eRRayOc-Kw{+DN1n}fItj7WfCe5T=~gTmYfHst>wy>}|S zv*@C6f-*RqWZcQ)W>~74gI708SDmikWMNrgjfjhExX?skj%FDVJ1}1~w_e6$4o!Q~ z+Up9&3}y!an*X_6~^=&JOcBBWpO%>)JI$I#g#L_7!jy$rud8p z9@-e%^$KNmM6=JboBk}hWpFPYW$&83oS+MZ77UaI$zl4uYIg6{Wn*MWznR<7^G1#6 z+nv#GU|qhurN_mddkKmc7+(`nx>>B=ZFZKPLB~9)A2IH=H;RI2FPKh%pE3 zF$eA7s8WkY-2%X+@(JwGn+AM?2lwtVR-<-4c1Gv5`pMhD|BB#Zy~jXBc5F;KuHxEh znPH#W!djS5l7x=Zd z>Q3}6*LK5=`PF`opgWp#~W2xps8UIgFo z8ct0h|Ap5Kg!+QqzVH61>XqykTS7GAS;G*8>8ZHTRhmck-A2nU2;Tdg2=8mSjQ6R7 z?`P#uyYIfF&ie*y?{oIe_r9p_=V{+@-u~Y%8exFAjM(WA0@V(dNiq&vpuIdT7_ELR z4mNCTRA-Un)J?)Sw}fw0M-T!5498Om$csBEIAD5svx3;h3GyivG|x+_tj9k@EKn%A zhArcUdN~jmP7)w>*aj8<0ucYG6LR5jod|G=h}sckb!ff0Ei{rbW(CMO3E_k8WZ;f? z@DEN|_2%UE-vWkVmVEnb*&P}eg9sGqrw|zAEMV?CZBW4DAru#4pO;1nDUQov#=A$r zdxrj{^}qZl8!(?^vg>-hOps_=cW*X;zk&d30Y9Min&`GH;A*|XpECE4qYYCCfM3G` zm~323jIsIHoDxW2HNbUkaf#Y9lPdb%e<4rD_CD6Y!56Q%U|^Y958fG19qI@}44S>V zw24#4wT^Nfs=54do$Z}a+BdsW{yWOez12B+$YAT6#yQ&xv=`LZZ@W?e%+^r*3r1IG z)3bkJe@qX^0QYBa$VbRn$UDd;YPEX4_8+YNfq=TsXaIf2zSzF|2yf3tJ+~a!CK&hg zKLcNUD++foT&CmcJFg%?2!!=}zz9)6W05pmuh`<_>`6`sqhWY{fXYA?(ukDO?NOsT zP@u9R!oeUBiT}+CCY9D9rs)=`gd~;!mdjP@0wOX*npn?xTnIqkhj{0DUI($}zR}4@ zU)oWTu;0jMxgp_PH&fg#?(2dQnuO}Md>@+haQFr7nSnwENQ`Gf{nLra2)%1jwmt3- zFL>mk3ebqtQ|mYx2t^{0X;iz3IV~1V$m6NxdQnE6B~vPtaRI#h%N3G1G^bZ5wNkKb zGwo+w)CBBzn}GU}0mfSZ;(%R!K>G`@0O>n1<-Gv-1%P+rpBNeqBnHhqR)Nm|9x(P8 zs-Jnu>fww!?;9bA;!-P!93lo1@e96SM z(xx=m7YCz0sr>&l+?1}TT#2NUG~i6@b;KHU(k0`P+^g~*iF1GCDBpBJ`hyIeXZp?qCJ;3a!< zxAL*F>KUh*pMH6*PkRD=XVPFv%*G^rC-?>G>Xs!lpbEg^|EtKz zI8V3a%9Dy(yP)Kf-g3K2w1oO@uhjr#D@Q~i|wZFA_F0QWj7q>-Ac_$h~=0Du`7t}-?zFnxSL#-?VHJofr zpGs^cmIxNz${++dNQ#fFj&Jn$vEFxQB0LitQYRSUKrQCYKydF_M44E>@5>O5*CPKj zW99GS)}R`iHc;yG5RYj{#@N)0tWeHE?v+l6C}HPhvpKi0M@gdZ6b!l-$5XMaw-i6f zWfJiy73)FIvgvWF4Fwe(WM){HFuLwU^JA7$UeERq1GDH(U8dILQ_G%^v8F+2%9Y{~ z+RP~vK8Y19cYcW){wam-b&U~z?y zFH>Q!dsZ0g#K?a~_=}nzk-6>hX?U8}m3faYiT-$xVcGajNKc5$f>COzqz|UO;{G-= z;6tJf?&JOvkilM-z~w&=sTg5S$(qP>H#7TFpmGeen9N{ELbHABb0ejpuM}wwAu&pG zuBy`Mw9O~A7{Axy_@@E>#soh83L!^@xb?J@8U)zSo1p|{WEab)@*an*Hk!{`} zgaqR#QIHiA?-bGp6@0r*jRLuS6+10DYSi;qO_!c5N$>Vl*hbDvAv0f{nz$H)2!*9M zqISUw(sYCxAQZ6B*!Yc~2G{%kr>{f^dq1EpBdBa=^Qf*Rt;?Wx(`{`BZ~cf~F&W26 zqbjl6ISK3I#ZB%z5|MIQz)6OWBl={i4lgYh?3^HB@biz&sLX*M6i_|P`N`6t@D^Ki zssU+^FnG6)5r3@CT;s1$Ytq$C0&4llHqA^+moQNUQ9W5a8NkxW2WK=>qOrW7-^(t+ zKK{KbEt7AuClIuL0vLKaF&9y}w0dgJ#5uBt%|Lx7k%mb`FkXA<>{If=zo_pBC1$u1~4~gp26;t~1!*KFqWu!l10@hWa-@M__ z{lz<=Xzjk;&Qdzl#B*d&=;y&^$Lrew-M#l<6 z?5Z0k$ruQFonl_@-bD5u!u>u{3CYZr;#(?WR?>ldcQ+Fw;s`y!!&D+$zrE7F7wZ;x zOukb}FLAr+8)M#V{H5q|-uP0|7)t~iIy(F9z0Yk3u;4C@#oe@kqg`+&$|67~x9eOB zQ%8487?~w|`-8d<=c8YvB?GQ-i{$iyLkaRr@lT3u#!C#e-FLLy@Bv!*#TBmb^BZ}^(K<0I>Glz;&Jn@tZ^0|M3oC=x+Q?S49R=CL8ZM2t_0f+i6L##-I-EBhkT zkrW%xW1^1yzY%MH1mu??WRtUYAR{uk#g&VR^j85g6PlilgV4bRLHLv(TA*V}$qH z0@2(@b%Y-_yt9`XFb94TzfAfaA?BRgdSe=8Uj$jnRW$Dw-LrgLc=(~dBp=;h9u;9; z{|3<^_!F&1 z8OPq!fiOtpW_JE`B>f1JN zTW$M_iFA`v)B8U*BssrPkUPNs#by@lfg|8dTritY#bb?*MH!$4{oAE;Yp+qpfnjoz zsIuMf*KAvcNvhF->K;Wq4~%D)2zLDnR&p@!iVaTxT zszOGtNbo>`r+#40M)?7zsj7gt5am3+Rq_y_(~9wQr^j?fmhr>0dIo)vv$1hPLseL- zGO8rNd6F!+q^H56*oj$C@KI>MKt(s!`a8`m+mqh^q>fO6CG_Z^dKOJ|c*z5+$+&j@ zjAe-pqF`Q%E!A;V35cqavr>32pW4(@gkn4esGLs3U;0ywtFI@cLq(r2jSUg)$%%TE zxx}ZqvdAtN4JPMZ(@E8Cv)rmeuYv>Gq;TaEDw5>&JA=%_i3C*u;m0eQ`gIR#Egkr- z)BM9L#=0t17wd&b_W#kW!j;Lk-g5ULF6(Ik8}vXIbfq1F%+8)D`KrjZyqkD(<3A&l zx|Y6TujThz($c=~34Z?aPPd3(oO~E^CSPbyjAS-d(YLP?IkOO9I3pd!Em`N>-&UUn zSX2#g*A7Iz2AN)Gpa|-7;-+}wmeQqen>|DEM9&gcHh6TXb{&5atqZ~L za>5%};U#%H)_oZ!hAFIA&zvHYY;BT9%?Xsm7L9&VDht{fkN-Im)0u&OL$OG&rx@DY zfUBhMph)KBY_>n9chjGFG3@fq#wD7Oi5|4Q$;o7F9{Nook=y4V>BvH{PXx>kA+Qdv zTi(JY3#htFfS&P-Sf}8FGjVE_b`%;VWsW6K0c!{=$6&B-6v?yQTfa@HxeiExP-=7~ zRL=yBM$&dn_bYehw`-;>tS~nY-%fZ7*gp{Zp+AkDd3rNdpTT857p`I0Aa>Bra8b-b zgkjdbH-99U-AE4r>)_>@he<8G?)K9uH%2%}DXf*>*$k|B%qUdLyew&H6&W|# z8mcM4j69^FZVlTaq@k2)SxR-7Yf%F7-qiv0Unp2!6X(3Pk0L(9(e=wx9yw-;tUMt57p zPX%5@5!JOk3aj!1#MDp!AtLhnq}F?oG!z-6Gi5Uz6DuS3Lr;-I|8bwuAiCLdWjCO< zo5%4A*s$5yBoZ`2E)DjYXbRiN2L#$}Za%JDFZqnr_3QAM3$Th}tyy0T3MopzPrFEb zlRXw;G$d;tnK*ujC9mtoHo_$sAB@WMLNFwC{HLvZ+^h_M4TABSQ0gy5eq4Hd^T6h( zmpnV3!(MgI9F?8N$)|ue(Dl~%oGSMayEMtYahvp~=5vjY=3~-1jG}^L9Zi81;oA9` zdMSBuxg2MR^8?XW++IB+?L+}Hdy6C}6Ai6}pkYMrfT>xWoV_?DUJ-o^Z5<}Fk_hMh zWp1{mrlqbm+hq8VC*t_-u07-j#fMnSz{`ZM19qt0T*1(JE#KT!Zzrz?rB+NA-S|d8 zDnh-56rV)G!hV*DVAXo~^hsnzO(H84#7r{w3V||F&|#wimu6|p`CUs2UwqDGB|GU zm<*x_0q-D^05#;@PP0u9XwvhWbLcf|gO)I91ew zI9!h)4Gi;U)hVH<$6x&>jux|jZGC5!TCQ}{!{@}5exIWlp|@;q0o7eiV~>*N$QX@; zAwz~kiE##noCI>`Oo15H#WD?uJ^Q&f5ECrWR1faD3$RvDF(knP{p`vy z^H&P&DJgNr=HzUa#!v5rnEUfT*F~>HcvnJ`ivujVyKeCly@$KC zc|c)yVX)EFuY0*~q@A^gUT@KuP&K?E>{H?83vOByl^au-LukObXAZc2yM{da7noJI zdJo@!MbRLv`{1M8KPi@QypE%J)yp#e;{^nIMtQT+aUyzp!Iks;S0kV&s~l2$eb@EP zG$BJ7?sy$zrMa~2%5Y`3)uy7-wI^TVvD)Sma9!LRoE{131ShdA|Kl&)rnR%|8?cVJ z%EvSBXk@+x^l^52Bt89bQ^$%0t!c9C>wUr9y@YpGajsWje=@5`|M`%cDt=}#880

hwI$kQb%SZIR7rt7$4whZh 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

TQ4q<9(C@(A+ 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%0D!Cqrn9Nh{5-u7p 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 PG + + + + + + + + + + + \ No newline at end of file diff --git a/python/src/nma_kit/assets/logo.svg b/python/src/nma_kit/assets/logo.svg new file mode 100644 index 0000000..93b3915 --- /dev/null +++ b/python/src/nma_kit/assets/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/python/src/nma_kit/assets/site.css b/python/src/nma_kit/assets/site.css new file mode 100644 index 0000000..8df1094 --- /dev/null +++ b/python/src/nma_kit/assets/site.css @@ -0,0 +1,205 @@ +/* Neuromatch course kit: site-wide styles + * + * `nma-kit sync-assets` writes this file, with the fonts embedded, to + * _nma/site.css. Courses list it as `site.options.style`. It applies the + * Neuromatch brand (palette, typography, accents) and styles the kit's own + * interactive widgets (editable cells, scrollytelling). + * + * Brand palette (Neuromatch Brand Book 2026): + * Primary Purple #45429C + * Blue #008CBF + * Light teal #BDCFCF + * Accent Blush #FA7A7A + * Neutrals Off white #F7F8FA Light grey #E6E6E6 + * Mid grey #7A7A7A Black #1A1A1A + * + * A course changes the colours through the `nma.brand` block in myst.yml + * (see README); `nma-kit sync-assets` then appends a `:root { ... }` block + * that redefines the --nma-brand-* variables below. Everything else follows. + */ + +/* The Rubik font faces (SIL OFL 1.1) are inserted below by sync-assets. */ +/*FONTS*/ + +/* ------------------------------------------------------------------------- + * Brand variables: the only values a course is meant to change. + * ---------------------------------------------------------------------- */ +:root { + --nma-brand-primary: #008CBF; /* Neuromatch Blue: links, accents, buttons */ + --nma-brand-primary-hover: #006B8F; /* hover / pressed */ + --nma-brand-primary-light: #35B4DC; /* the same accent on dark backgrounds */ + --nma-brand-secondary: #45429C; /* Purple: header surface, tips */ + --nma-brand-accent: #FA7A7A; /* Blush: exercise cells */ + + --nma-font-sans: 'Rubik', Arial, ui-sans-serif, system-ui, sans-serif; +} + +body, +button, +input, +select, +textarea, +h1, h2, h3, h4, h5, h6 { + font-family: var(--nma-font-sans); +} + +/* ------------------------------------------------------------------------- + * Book theme colours, light mode (:root) overrides the theme defaults. + * ---------------------------------------------------------------------- */ +:root { + --myst-color-primary: var(--nma-brand-primary); + --myst-color-primary-hover: var(--nma-brand-primary-hover); + + --myst-color-link: var(--nma-brand-primary); + --myst-color-link-hover: var(--nma-brand-primary-hover); + --myst-color-link-underline: var(--nma-brand-primary); + + --myst-color-active: var(--nma-brand-primary); + --myst-color-active-bg: color-mix(in srgb, var(--nma-brand-primary) 15%, transparent); + --myst-color-active-surface: #F7F8FA; + + --myst-color-focus-ring: var(--nma-brand-primary); + --myst-color-focus-outline: #BDCFCF; + + --myst-color-accent-text: var(--nma-brand-primary); + --myst-color-accent-surface: #F7F8FA; + --myst-color-accent-surface-border: #BDCFCF; + --myst-color-accent-surface-text: var(--nma-brand-primary-hover); + + --myst-color-inverse-bg: var(--nma-brand-secondary); + --myst-color-code: var(--nma-brand-primary); + + /* Neutrals */ + --myst-color-text: #1A1A1A; + --myst-color-prose-body: #1A1A1A; + --myst-color-text-secondary: #7A7A7A; + --myst-color-text-tertiary: #7A7A7A; + --myst-color-bg-secondary: #F7F8FA; + --myst-color-surface: #F7F8FA; + --myst-color-surface-hover: #E6E6E6; + --myst-color-border: #E6E6E6; + --myst-color-border-strong: #BDCFCF; + + /* Admonition accents on brand */ + --myst-color-info: var(--nma-brand-primary); + --myst-color-info-text: var(--nma-brand-primary-hover); + --myst-color-tip: var(--nma-brand-secondary); + --myst-color-tip-text: var(--nma-brand-secondary); + + /* Interactive code panels (editable cells), light */ + --nma-cell-accent-light: var(--nma-brand-primary); + --nma-cell-exercise-accent-light: var(--nma-brand-accent); + --nma-cell-font-family: var(--nma-font-sans); + + /* Syntax highlighting tokens (CodeMirror cells), light. Fixed values: + chosen for AA contrast on the light cell background. */ + --nma-tok-keyword-light: #45429C; + --nma-tok-function-light: #006B8F; + --nma-tok-definition-light: #006B8F; + --nma-tok-string-light: #0E7C86; + --nma-tok-number-light: #B5484B; + --nma-tok-comment-light: #7A7A7A; +} + +/* ------------------------------------------------------------------------- + * Book theme colours, dark mode. The accent shifts to the lighter brand + * colour so it stays legible on the dark background. + * ---------------------------------------------------------------------- */ +html.dark, +.dark { + --myst-color-primary: var(--nma-brand-primary); + --myst-color-primary-hover: var(--nma-brand-primary-light); + + --myst-color-link: var(--nma-brand-primary-light); + --myst-color-link-hover: #BDCFCF; + --myst-color-link-underline: var(--nma-brand-primary); + + --myst-color-active: var(--nma-brand-primary-light); + --myst-color-active-bg: color-mix(in srgb, var(--nma-brand-primary) 25%, transparent); + + --myst-color-focus-ring: var(--nma-brand-primary-light); + --myst-color-focus-outline: var(--nma-brand-primary-light); + + --myst-color-accent-text: #BDCFCF; + --myst-color-accent-surface-text: #BDCFCF; + + --myst-color-inverse-bg: var(--nma-brand-primary); + --myst-color-code: var(--nma-brand-primary-light); + + --myst-color-info: var(--nma-brand-primary-light); + --myst-color-info-text: var(--nma-brand-primary-light); + --myst-color-tip: var(--nma-brand-primary-light); + --myst-color-tip-text: var(--nma-brand-primary-light); + + /* Dark neutrals (restated: the :root overrides above would otherwise leak + into dark mode, since this file loads after the theme and :root / .dark + have equal specificity). */ + --myst-color-text: #F7F8FA; + --myst-color-prose-body: #E6E6E6; + --myst-color-text-secondary: #BDCFCF; + --myst-color-text-tertiary: #7A7A7A; + --myst-color-bg-secondary: #232323; + --myst-color-surface: #232323; + --myst-color-surface-hover: #2E2E2E; + --myst-color-border: #3A3A3A; + --myst-color-border-strong: #4A4A4A; + + /* Interactive code panels (editable cells), dark */ + --nma-cell-accent-dark: var(--nma-brand-primary-light); + --nma-cell-exercise-accent-dark: var(--nma-brand-accent); + + /* Syntax highlighting tokens (CodeMirror cells), dark. */ + --nma-tok-keyword-dark: #B9B8FF; + --nma-tok-function-dark: #5EC8EC; + --nma-tok-definition-dark: #5EC8EC; + --nma-tok-string-dark: #5EFBD7; + --nma-tok-number-dark: #FF9E9E; + --nma-tok-comment-dark: #9AA7A7; +} + +/* ------------------------------------------------------------------------- + * Static code blocks (highlight.js) — brand palette matching the CodeMirror + * token colours above, so the two highlighters look consistent. Light is the + * default (no html class); dark is html.dark. Selectors are one step more + * specific than the theme's so they win. + * ---------------------------------------------------------------------- */ +html:not(.dark) .hljs-keyword, +html:not(.dark) .hljs-literal, +html:not(.dark) .hljs-selector-tag, +html:not(.dark) .hljs-name, +html:not(.dark) .hljs-symbol, +html:not(.dark) .hljs-tag { color: #45429C; } +html:not(.dark) .hljs-built_in, +html:not(.dark) .hljs-type, +html:not(.dark) .hljs-title, +html:not(.dark) .hljs-section { color: #006B8F; } +html:not(.dark) .hljs-string, +html:not(.dark) .hljs-quote, +html:not(.dark) .hljs-attr, +html:not(.dark) .hljs-attribute { color: #0E7C86; } +html:not(.dark) .hljs-number, +html:not(.dark) .hljs-meta { color: #B5484B; } +html:not(.dark) .hljs-comment { color: #7A7A7A; font-style: italic; } + +html.dark .hljs-keyword, +html.dark .hljs-literal, +html.dark .hljs-selector-tag, +html.dark .hljs-name, +html.dark .hljs-symbol, +html.dark .hljs-tag { color: #B9B8FF; } +html.dark .hljs-built_in, +html.dark .hljs-type, +html.dark .hljs-title, +html.dark .hljs-section { color: #5EC8EC; } +html.dark .hljs-string, +html.dark .hljs-quote, +html.dark .hljs-attr, +html.dark .hljs-attribute { color: #5EFBD7; } +html.dark .hljs-number, +html.dark .hljs-meta { color: #FF9E9E; } +html.dark .hljs-comment { color: #9AA7A7; font-style: italic; } + +/* The editable cells replace JupyterLite's power button on every page. Hide + the whole toolbar wrapper, not just the button: the wrapper otherwise + leaves a small ghost dot. */ +.myst-jp-nb-toolbar { display: none !important; } diff --git a/python/src/nma_kit/branding.py b/python/src/nma_kit/branding.py new file mode 100644 index 0000000..bcf4a56 --- /dev/null +++ b/python/src/nma_kit/branding.py @@ -0,0 +1,161 @@ +"""Site branding: the kit's stylesheet, logos, favicon and fonts. + +`nma-kit sync-assets` (also run by `prepare-site`) writes them to `_nma/` in +the course. The course's myst.yml points MyST at the files: + + site: + options: + style: _nma/site.css + logo: _nma/logo.svg + logo_dark: _nma/logo-dark.svg + favicon: _nma/favicon.png + +and may adjust them in the `nma:` block: + + nma: + brand: # all optional, "#RRGGBB" + primary: "#008CBF" # links, accents, buttons + primary_hover: "#006B8F" # default: primary, darkened + primary_light: "#35B4DC" # accent on dark backgrounds, default: primary, lightened + secondary: "#45429C" # header surface, tips + accent: "#FA7A7A" # exercise cells + style: + extra: _static/extra.css # the course's own CSS, appended after the kit's + +`_nma/site.css` is the kit stylesheet (Rubik embedded as data URIs, so no +request leaves the site), then the brand overrides, then the course's CSS: +MyST accepts a single `style` file. The folder gets its own .gitignore, so +courses never commit it. +""" + +import base64 +import re +import shutil +from pathlib import Path + +from nma_kit.config import Settings + +ASSETS = Path(__file__).parent / "assets" +OUT_DIR = Path("_nma") + +BRAND_KEYS = ("primary", "primary_hover", "primary_light", "secondary", "accent") +HEX_RE = re.compile(r"^#[0-9A-Fa-f]{6}$") + +# (weight, style, file) of the embedded Rubik faces +FONTS = ( + ("400", "normal", "Rubik-400-normal.woff2"), + ("400", "italic", "Rubik-400-italic.woff2"), + ("500", "normal", "Rubik-500-normal.woff2"), + ("700", "normal", "Rubik-700-normal.woff2"), +) +FONT_PLACEHOLDER = "/*FONTS*/" + +# kit asset -> file in _nma/ (the names courses use in myst.yml) +COPIED = {"logo.svg": "logo.svg", "logo-dark.svg": "logo-dark.svg", "favicon.png": "favicon.png"} + + +class BrandingError(Exception): + """A problem with the `nma.brand` / `nma.style` settings.""" + + +def _section(nma: dict, name: str) -> dict: + section = nma.get(name) + return section if isinstance(section, dict) else {} + + +def brand_errors(nma: dict) -> list[str]: + """Problems with `nma.brand` and `nma.style.extra` (the extra file's existence + is checked at sync time, relative to the course root).""" + errors = [] + brand = _section(nma, "brand") + for key, value in brand.items(): + if key not in BRAND_KEYS: + errors.append(f"myst.yml: nma.brand.{key} is not a known colour " + f"(one of {', '.join(BRAND_KEYS)})") + elif not isinstance(value, str) or not HEX_RE.match(value): + errors.append(f"myst.yml: nma.brand.{key} must be a colour like \"#008CBF\", got {value!r}") + for key in _section(nma, "style"): + if key != "extra": + errors.append(f"myst.yml: nma.style.{key} is not a known setting (only `extra`)") + return errors + + +def _mix(color: str, other: tuple[int, int, int], share: float) -> str: + """`color` with `share` (0..1) of `other` mixed in, as #RRGGBB.""" + rgb = [int(color[i:i + 2], 16) for i in (1, 3, 5)] + mixed = [round(c * (1 - share) + o * share) for c, o in zip(rgb, other, strict=True)] + return "#" + "".join(f"{v:02X}" for v in mixed) + + +def brand_overrides(nma: dict) -> str: + """A `:root { ... }` block for the colours the course set, or ''. + + Setting only `primary` also derives `primary_hover` (25% darker) and + `primary_light` (35% lighter), because those two follow the primary + colour unless the course says otherwise. + """ + brand = {k: v.upper() for k, v in _section(nma, "brand").items() if k in BRAND_KEYS} + if "primary" in brand: + brand.setdefault("primary_hover", _mix(brand["primary"], (0, 0, 0), 0.25)) + brand.setdefault("primary_light", _mix(brand["primary"], (255, 255, 255), 0.35)) + if not brand: + return "" + lines = [f" --nma-brand-{key.replace('_', '-')}: {brand[key]};" for key in BRAND_KEYS if key in brand] + return "/* Course colours (nma.brand in myst.yml) */\n:root {\n" + "\n".join(lines) + "\n}\n" + + +def font_face_css() -> str: + rules = [] + for weight, style, name in FONTS: + data = base64.b64encode((ASSETS / "fonts" / name).read_bytes()).decode("ascii") + rules.append( + "@font-face {\n" + " font-family: 'Rubik';\n" + f" font-style: {style};\n" + f" font-weight: {weight};\n" + " font-display: swap;\n" + f" src: url(data:font/woff2;base64,{data}) format('woff2');\n" + "}" + ) + return "\n".join(rules) + + +def extra_css_path(settings: Settings) -> Path | None: + extra = _section(settings.nma, "style").get("extra") + return Path(extra) if isinstance(extra, str) and extra.strip() else None + + +def build_css(settings: Settings, root: Path = Path(".")) -> str: + """The complete site stylesheet for a course.""" + base = (ASSETS / "site.css").read_text(encoding="utf-8") + if FONT_PLACEHOLDER not in base: + raise BrandingError("kit stylesheet has no font placeholder (broken install?)") + parts = [base.replace(FONT_PLACEHOLDER, font_face_css())] + overrides = brand_overrides(settings.nma) + if overrides: + parts.append(overrides) + extra = extra_css_path(settings) + if extra is not None: + path = root / extra + if not path.is_file(): + raise BrandingError(f"nma.style.extra: {extra.as_posix()} not found (relative to the course root)") + parts.append(f"/* Course CSS ({extra.as_posix()}) */\n" + path.read_text(encoding="utf-8")) + return "\n".join(part.rstrip("\n") + "\n" for part in parts) + + +def sync_assets(settings: Settings, root: Path = Path(".")) -> list[Path]: + """Write the branding files to `/_nma/`. Returns the files written.""" + errors = brand_errors(settings.nma) + if errors: + raise BrandingError("; ".join(errors)) + css = build_css(settings, root) # before touching the disk, so a bad setting leaves it alone + out = root / OUT_DIR + out.mkdir(exist_ok=True) + (out / ".gitignore").write_text("# written by `nma-kit sync-assets`\n*\n", encoding="utf-8") + written = [] + (out / "site.css").write_text(css, encoding="utf-8") + written.append(out / "site.css") + for source, name in COPIED.items(): + shutil.copyfile(ASSETS / source, out / name) + written.append(out / name) + return written diff --git a/python/src/nma_kit/cli.py b/python/src/nma_kit/cli.py index 254fa75..ccb6120 100644 --- a/python/src/nma_kit/cli.py +++ b/python/src/nma_kit/cli.py @@ -3,8 +3,10 @@ nma-kit check validate content and myst.yml nma-kit notebooks [--dry-run] generate student/instructor notebooks and solution files nma-kit render-scrolly pre-render scrolly page outputs into _build/scrolly/ + nma-kit sync-assets write the site branding (CSS, logos, favicon) to _nma/ nma-kit prepare-site add badges + fallback cells to the .md pages and pin the - kit URL in myst.yml (CI only, never commit the result) + kit URL in myst.yml, and run sync-assets (CI only, + never commit the result) nma-kit publish-notebooks push notebooks/ to the notebooks branch Exit codes: 0 ok, 1 content problems, 2 usage or configuration errors. @@ -16,7 +18,7 @@ from pathlib import Path from nma_kit import __version__ -from nma_kit import convert, scrolly +from nma_kit import branding, convert, scrolly from nma_kit.config import MYST_CONFIG, MissingConfig, read_settings from nma_kit.pin import pin_kit_url from nma_kit.publish import PublishError, publish_notebooks @@ -65,12 +67,20 @@ def cmd_render_scrolly(args) -> int: return scrolly.render_all(read_settings().tutorials_dir) +def cmd_sync_assets(args) -> int: + settings = read_settings() + for path in branding.sync_assets(settings): + print(f" wrote {path.as_posix()}") + return 0 + + def cmd_prepare_site(args) -> int: settings = _settings() if not _validate(check_injected=False): return 1 count = convert.process_tutorials(settings, args.dry_run, prepare_site=True) if not args.dry_run: + branding.sync_assets(settings) text = MYST_CONFIG.read_text(encoding="utf-8") pinned = pin_kit_url(text, args.kit_version) if pinned != text: @@ -105,6 +115,8 @@ def build_parser() -> argparse.ArgumentParser: sub.add_parser("render-scrolly", help="pre-render scrolly pages").set_defaults(func=cmd_render_scrolly) + sub.add_parser("sync-assets", help="write the site branding to _nma/").set_defaults(func=cmd_sync_assets) + p = sub.add_parser("prepare-site", help="prepare the .md pages for `myst build` (CI only)") p.add_argument("--dry-run", action="store_true") p.add_argument("--kit-version", default=__version__, @@ -126,7 +138,7 @@ def main(argv: list[str] | None = None) -> int: return 2 try: return args.func(args) - except (MissingConfig, UsageError) as exc: + except (MissingConfig, UsageError, branding.BrandingError) as exc: print(f"nma-kit: {exc}", file=sys.stderr) return 2 except PublishError as exc: diff --git a/python/src/nma_kit/convert.py b/python/src/nma_kit/convert.py index e393d0a..99ef147 100644 --- a/python/src/nma_kit/convert.py +++ b/python/src/nma_kit/convert.py @@ -67,8 +67,10 @@ Settings, load_settings, page_location, + read_nma_block, read_settings, ) +from nma_kit import branding as nma_branding from nma_kit import feedback as nma_feedback from nma_kit import scrolly as nma_scrolly from nma_kit import solutions as nma_solutions @@ -173,6 +175,7 @@ def validate_content(check_injected: bool = True) -> list[str]: tutorials_dir = Path(load_settings(config_text).tutorials_dir) config_problems = nma_feedback.config_errors(config_text) errors += config_problems + errors += nma_branding.brand_errors(read_nma_block(config_text)) has_config = not config_problems and nma_feedback.load_config(config_text) is not None for path in sorted(tutorials_dir.rglob("*.ipynb")): if ".ipynb_checkpoints" in path.parts: diff --git a/tests/e2e/test_branding_browser.py b/tests/e2e/test_branding_browser.py new file mode 100644 index 0000000..925d65f --- /dev/null +++ b/tests/e2e/test_branding_browser.py @@ -0,0 +1,31 @@ +"""Browser checks for the kit's site branding (_nma/site.css via `nma-kit sync-assets`).""" + +import pytest + +pytest.importorskip("playwright") + +PAGE = "/tutorials/w2d1-examples/w2d1-tutorial1/" + + +def test_brand_font_and_colours_apply_without_external_requests(page, base_url): + requests = [] + page.on("request", lambda r: requests.append(r.url)) + page.goto(base_url + PAGE) + page.wait_for_load_state("networkidle") + + # Rubik comes from the embedded data URI, so it must be loaded and in use. + assert page.evaluate("document.fonts.ready.then(() => document.fonts.check('1em Rubik'))") + assert "Rubik" in page.evaluate("getComputedStyle(document.body).fontFamily") + + primary = page.evaluate( + "getComputedStyle(document.documentElement).getPropertyValue('--nma-brand-primary').trim()") + assert primary.upper() == "#008CBF" + + assert not [u for u in requests if "fonts.googleapis.com" in u or "fonts.gstatic.com" in u] + + +def test_favicon_and_logo_come_from_the_kit(page, base_url): + page.goto(base_url + PAGE) + page.wait_for_load_state("networkidle") + assert page.locator("link[rel~='icon']").count() >= 1 + assert page.locator("header img, nav img").count() >= 1 diff --git a/tests/example-course/_static/custom.css b/tests/example-course/_static/custom.css index 5fd0118..7432654 100644 --- a/tests/example-course/_static/custom.css +++ b/tests/example-course/_static/custom.css @@ -1,5 +1,2 @@ -/* Required by the course kit: editable cells replace JupyterLite's power - button site-wide (a plugin can't add site-wide CSS, so every course keeps - this rule in its own stylesheet). Hide the whole toolbar wrapper, not just - the power button: the wrapper otherwise leaves a small ghost dot. */ -.myst-jp-nb-toolbar { display: none !important; } +/* Course-specific CSS, appended after the kit's stylesheet (nma.style.extra in + myst.yml). The kit already carries the brand and the toolbar rule. */ diff --git a/tests/example-course/myst.yml b/tests/example-course/myst.yml index c8775f8..2c126f7 100644 --- a/tests/example-course/myst.yml +++ b/tests/example-course/myst.yml @@ -24,10 +24,17 @@ site: template: book-theme options: folders: true # URLs keep the folder, like the template (/tutorials/w2d1-examples/...) - style: _static/custom.css # holds the toolbar rule every course needs + # Written by `nma-kit sync-assets` (prepare-site runs it in CI). + style: _nma/site.css + logo: _nma/logo.svg + logo_dark: _nma/logo-dark.svg + logo_text: Course Kit Example Course + favicon: _nma/favicon.png # Course kit settings (read by the kit's plugin and the nma-kit CLI). nma: + style: + extra: _static/custom.css # the course's own CSS, appended after the kit's feedback: url: https://pmyvdlilci.execute-api.us-east-1.amazonaws.com/klab project: neuromatch_cn diff --git a/tests/python/test_branding.py b/tests/python/test_branding.py new file mode 100644 index 0000000..aa32fe5 --- /dev/null +++ b/tests/python/test_branding.py @@ -0,0 +1,153 @@ +import re + +import pytest + +from nma_kit import branding +from nma_kit.cli import main +from nma_kit.config import load_settings + +MYST = "version: 1\nproject:\n github: https://github.com/org/repo\n" + + +def settings(nma_block: str = ""): + return load_settings(MYST + nma_block) + + +def test_packaged_assets_exist(): + for name in ("site.css", "logo.svg", "logo-dark.svg", "favicon.png", "fonts/OFL.txt"): + assert (branding.ASSETS / name).is_file(), name + for _, _, name in branding.FONTS: + assert (branding.ASSETS / "fonts" / name).is_file(), name + + +def test_favicon_is_small(): + assert (branding.ASSETS / "favicon.png").stat().st_size < 50_000 + + +def test_css_embeds_fonts_and_makes_no_external_requests(): + css = branding.build_css(settings()) + assert css.count("@font-face") == len(branding.FONTS) + assert css.count("data:font/woff2;base64,") == len(branding.FONTS) + assert branding.FONT_PLACEHOLDER not in css + assert "googleapis" not in css + assert not re.search(r"@import|url\(\s*['\"]?https?:", css) + + +def test_css_keeps_the_toolbar_rule_courses_used_to_carry(): + assert ".myst-jp-nb-toolbar { display: none !important; }" in branding.build_css(settings()) + + +def test_no_brand_block_means_no_overrides(): + assert branding.brand_overrides({}) == "" + assert "Course colours" not in branding.build_css(settings()) + + +def test_brand_overrides_follow_the_primary_colour(): + css = branding.brand_overrides({"brand": {"primary": "#ff0000"}}) + assert "--nma-brand-primary: #FF0000;" in css + assert "--nma-brand-primary-hover: #BF0000;" in css # 25% darker + assert "--nma-brand-primary-light: #FF5959;" in css # 35% lighter + + +def test_explicit_hover_and_light_win_over_derived(): + css = branding.brand_overrides({"brand": {"primary": "#112233", "primary_hover": "#000000", + "primary_light": "#FFFFFF", "accent": "#abcdef"}}) + assert "--nma-brand-primary-hover: #000000;" in css + assert "--nma-brand-primary-light: #FFFFFF;" in css + assert "--nma-brand-accent: #ABCDEF;" in css + + +def test_overrides_come_after_the_base_so_they_win(): + css = branding.build_css(settings('nma:\n brand:\n primary: "#ff0000"\n')) + assert css.index("--nma-brand-primary: #008CBF") < css.index("--nma-brand-primary: #FF0000") + + +def test_secondary_only_does_not_touch_primary(): + css = branding.brand_overrides({"brand": {"secondary": "#123456"}}) + assert "--nma-brand-secondary: #123456;" in css and "primary" not in css + + +@pytest.mark.parametrize("brand, message", [ + ({"primary": "red"}, "must be a colour"), + ({"primary": "#fff"}, "must be a colour"), + ({"primry": "#ffffff"}, "not a known colour"), +]) +def test_brand_errors(brand, message): + errors = branding.brand_errors({"brand": brand}) + assert len(errors) == 1 and message in errors[0] + + +def test_unknown_style_setting_is_an_error(): + assert "nma.style.css" in branding.brand_errors({"style": {"css": "x.css"}})[0] + + +def test_extra_css_is_appended_last(tmp_path): + (tmp_path / "extra.css").write_text("h1 { color: hotpink; }\n") + css = branding.build_css(settings("nma:\n style:\n extra: extra.css\n"), tmp_path) + assert css.rstrip().endswith("h1 { color: hotpink; }") + assert css.index("toolbar") < css.index("hotpink") + + +def test_missing_extra_css_is_an_error(tmp_path): + with pytest.raises(branding.BrandingError, match=r"extra\.css not found"): + branding.build_css(settings("nma:\n style:\n extra: extra.css\n"), tmp_path) + + +def test_sync_writes_the_files_the_course_references(tmp_path): + written = branding.sync_assets(settings(), tmp_path) + names = {p.name for p in written} + assert names == {"site.css", "logo.svg", "logo-dark.svg", "favicon.png"} + assert all(p.parent == tmp_path / "_nma" for p in written) + assert (tmp_path / "_nma" / ".gitignore").read_text().splitlines()[-1] == "*" + + +def test_sync_is_repeatable(tmp_path): + first = branding.sync_assets(settings(), tmp_path)[0].read_text() + assert branding.sync_assets(settings(), tmp_path)[0].read_text() == first + + +def test_bad_settings_write_nothing(tmp_path): + with pytest.raises(branding.BrandingError): + branding.sync_assets(settings('nma:\n brand:\n primary: "red"\n'), tmp_path) + assert not (tmp_path / "_nma").exists() + + +# --- through the CLI -------------------------------------------------------- + +def course(tmp_path, nma_block=""): + (tmp_path / "myst.yml").write_text(MYST + nma_block) + (tmp_path / "tutorials").mkdir() + return tmp_path + + +def test_cli_sync_assets(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(course(tmp_path)) + assert main(["sync-assets"]) == 0 + assert (tmp_path / "_nma" / "site.css").is_file() + assert "_nma/site.css" in capsys.readouterr().out + + +def test_cli_sync_assets_reports_bad_settings(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(course(tmp_path, 'nma:\n brand:\n primary: "red"\n')) + assert main(["sync-assets"]) == 2 + assert "nma.brand.primary" in capsys.readouterr().err + assert not (tmp_path / "_nma").exists() + + +def test_cli_check_rejects_bad_brand(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(course(tmp_path, 'nma:\n brand:\n primary: "red"\n')) + assert main(["check"]) == 1 + assert "nma.brand.primary" in capsys.readouterr().err + + +def test_cli_check_accepts_good_brand(tmp_path, monkeypatch): + monkeypatch.chdir(course(tmp_path, 'nma:\n brand:\n primary: "#112233"\n')) + assert main(["check"]) == 0 + + +def test_prepare_site_syncs_assets_but_dry_run_does_not(tmp_path, monkeypatch): + monkeypatch.chdir(course(tmp_path)) + assert main(["prepare-site", "--dry-run"]) == 0 + assert not (tmp_path / "_nma").exists() + assert main(["prepare-site"]) == 0 + assert (tmp_path / "_nma" / "site.css").is_file() From ddac30936f12dd5861dcb0795a70d73cd091b7b2 Mon Sep 17 00:00:00 2001 From: Konstantine Tsafatinos Date: Tue, 20 Oct 2026 00:13:59 -0400 Subject: [PATCH 2/4] feat(scrolly): follow the site's light/dark mode and brand colours The scrolly section was a fixed dark-indigo panel with cream cards. It now uses --sc-* tokens: Neuromatch neutrals in light mode, a restated set under .dark, brand colours from --nma-brand-* (so nma.brand recolours it) and the editable cells' syntax colours. Adds guard tests for the tokens. --- AGENTS.md | 10 ++ js/src/scrolly/assets/engine.mjs | 14 +-- js/src/scrolly/assets/runnable.css | 65 ++++++------- js/src/scrolly/assets/scrolly.css | 144 +++++++++++++++++++++-------- tests/js/scrolly.test.mjs | 39 ++++++++ 5 files changed, 195 insertions(+), 77 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 832093f..497996f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,6 +146,16 @@ templates/wheels.yml # optional caller for course-wheels.yml - The section sits between the book theme's TOC sidebar and outline (`.scrolly-col` in `scrolly.css`) and uses container queries on its own width (stage beside the cards from 760px, `WIDE` in `engine.mjs`). +- **Colours follow the site.** `scrolly.css` defines `--sc-*` tokens on `.scrolly` + (light) and overrides them under `.dark .scrolly`; `runnable.css` and the HUD + styles in `engine.mjs` read the same tokens (custom properties inherit into + the shadow DOM, so no JS switches modes). Brand colours come from the kit + stylesheet's `--nma-brand-*`, so `nma.brand` recolours scrolly pages too; + syntax colours reuse `--nma-tok-*` (shared with the editable cells). Hex + values in `var(..., #hex)` are the Neuromatch defaults for courses that + don't load `site.css`. `tests/js/scrolly.test.mjs` checks that every + `--sc-*` in use is defined and that dark mode only overrides known tokens. + Don't add fixed colours: add a token. - Engine (HUD), stage and steps share one grid cell (`grid-area: 1 / 1`), not negative margins (a sticky element with `margin-bottom: -height` covered the footer links; regression test in `tests/js/scrolly.test.mjs`). diff --git a/js/src/scrolly/assets/engine.mjs b/js/src/scrolly/assets/engine.mjs index 128f7d8..e7d9bee 100644 --- a/js/src/scrolly/assets/engine.mjs +++ b/js/src/scrolly/assets/engine.mjs @@ -11,14 +11,14 @@ const STYLE = ` .hud { position: absolute; top: 0; left: 0; right: 0; display: flex; align-items: center; gap: 16px; padding: 14px 28px; pointer-events: none; font-family: var(--nm-font-head, system-ui, sans-serif); } -.sec { font-weight: 600; font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--nm-mint, #5efbd7); min-width: 12ch; } -.bar { flex: 1; height: 3px; background: rgba(255,255,255,0.14); border-radius: 3px; overflow: hidden; } -.bar i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--nm-salmon, #fb7a7a), var(--nm-mint, #5efbd7)); transition: width 0.4s; } +.sec { font-weight: 600; font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--sc-accent, #008cbf); min-width: 12ch; } +.bar { flex: 1; height: 3px; background: var(--sc-track, rgba(26,26,26,0.12)); border-radius: 3px; overflow: hidden; } +.bar i { display: block; height: 100%; width: 0; background: linear-gradient(90deg, var(--sc-warm, #fa7a7a), var(--sc-accent, #008cbf)); transition: width 0.4s; } .py[hidden] { display: none; } -.py { pointer-events: auto; flex: none; font: 600 0.74rem var(--nm-font-head, system-ui, sans-serif); letter-spacing: 0.04em; border: 1px solid var(--nm-mint, #5efbd7); border-radius: 999px; padding: 0.3rem 0.85rem; cursor: pointer; max-width: 34ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: var(--nm-mint, #5efbd7); color: var(--nm-indigo, #2a2952); } -.py[data-state="loading"] { background: transparent; color: var(--nm-mint, #5efbd7); cursor: progress; } -.py[data-state="ready"] { background: transparent; color: var(--nm-mint, #5efbd7); cursor: default; } -.py[data-state="error"] { background: var(--nm-salmon, #fb7a7a); border-color: var(--nm-salmon, #fb7a7a); } +.py { pointer-events: auto; flex: none; font: 600 0.74rem var(--nm-font-head, system-ui, sans-serif); letter-spacing: 0.04em; border: 1px solid var(--sc-accent, #008cbf); border-radius: 999px; padding: 0.3rem 0.85rem; cursor: pointer; max-width: 34ch; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background: var(--sc-accent, #008cbf); color: var(--sc-accent-fg, #fff); } +.py[data-state="loading"] { background: transparent; color: var(--sc-accent, #008cbf); cursor: progress; } +.py[data-state="ready"] { background: transparent; color: var(--sc-accent, #008cbf); cursor: default; } +.py[data-state="error"] { background: var(--sc-warm, #fa7a7a); border-color: var(--sc-warm, #fa7a7a); color: #1a1a1a; } `; // Section width (px) from which the stage sits beside the cards. diff --git a/js/src/scrolly/assets/runnable.css b/js/src/scrolly/assets/runnable.css index c62a421..05c8950 100644 --- a/js/src/scrolly/assets/runnable.css +++ b/js/src/scrolly/assets/runnable.css @@ -1,27 +1,28 @@ /* Runnable Python panel (inside the widget's shadow DOM). - Colours come from the --nm-* variables set on .scrolly in scrolly.css. */ + Colours come from the --sc-* variables set on .scrolly in scrolly.css (they + inherit into the shadow DOM and switch with the site's light/dark mode). */ :host { display: block; width: 100%; } * { box-sizing: border-box; } [hidden] { display: none !important; } .fl-codebox { width: 100%; - background: #14131f; - border: 1px solid rgba(185, 184, 255, 0.28); + background: var(--sc-code-bg); + border: 1px solid var(--sc-line); border-radius: 12px; overflow: hidden; - box-shadow: 0 12px 34px rgba(8, 7, 30, 0.5); - color: #ecebff; + box-shadow: var(--sc-shadow); + color: var(--sc-code-text); font-family: var(--nm-font-body, system-ui, sans-serif); text-align: left; } -.fl-codehead { display: flex; justify-content: space-between; padding: 0.45rem 1rem; background: #22214a; font: 600 0.72rem var(--nm-font-head, system-ui, sans-serif); letter-spacing: 0.1em; text-transform: uppercase; color: var(--nm-lilac, #b9b8ff); } +.fl-codehead { display: flex; justify-content: space-between; padding: 0.45rem 1rem; background: var(--sc-code-bar); border-bottom: 1px solid var(--sc-line); font: 600 0.72rem var(--nm-font-head, system-ui, sans-serif); letter-spacing: 0.1em; text-transform: uppercase; color: var(--sc-code-head-fg); } -.fl-params { display: flex; flex-wrap: wrap; gap: 0.4rem 1.4rem; padding: 0.55rem 1rem; background: #1b1a36; border-bottom: 1px solid rgba(185, 184, 255, 0.2); } -.fl-params label { display: flex; align-items: center; gap: 0.5rem; font: 600 0.8rem ui-monospace, Consolas, monospace; color: var(--nm-lilac, #b9b8ff); } -.fl-params input[type=range] { width: 9rem; accent-color: var(--nm-mint, #5efbd7); } -.fl-params output { min-width: 4ch; color: var(--nm-mint, #5efbd7); text-align: right; } -.fl-params select { font: inherit; background: #14131f; color: var(--nm-mint, #5efbd7); border: 1px solid rgba(185, 184, 255, 0.4); border-radius: 6px; padding: 0.15rem 0.4rem; } +.fl-params { display: flex; flex-wrap: wrap; gap: 0.4rem 1.4rem; padding: 0.55rem 1rem; background: var(--sc-code-bar); border-bottom: 1px solid var(--sc-line); } +.fl-params label { display: flex; align-items: center; gap: 0.5rem; font: 600 0.8rem ui-monospace, Consolas, monospace; color: var(--sc-code-head-fg); } +.fl-params input[type=range] { width: 9rem; accent-color: var(--sc-accent); } +.fl-params output { min-width: 4ch; color: var(--sc-accent); text-align: right; } +.fl-params select { font: inherit; background: var(--sc-code-bg); color: var(--sc-accent); border: 1px solid var(--sc-line); border-radius: 6px; padding: 0.15rem 0.4rem; } .fl-editor { position: relative; max-height: 46vh; overflow: auto; } .fl-editor .fl-hl, .fl-editor .fl-ta { @@ -31,37 +32,37 @@ white-space: pre; tab-size: 4; } -.fl-editor .fl-hl { color: #ecebff; overflow: hidden; pointer-events: none; } +.fl-editor .fl-hl { color: var(--sc-code-text); overflow: hidden; pointer-events: none; } .fl-editor .fl-ta { position: absolute; inset: 0; width: 100%; height: 100%; - color: transparent; caret-color: var(--nm-mint, #5efbd7); background: transparent; border: 0; outline: 0; resize: none; overflow: hidden; + color: transparent; caret-color: var(--sc-accent); background: transparent; border: 0; outline: 0; resize: none; overflow: hidden; } -.fl-editor .fl-ta::selection { background: rgba(94, 251, 215, 0.28); } -.fl-editor:focus-within { box-shadow: inset 0 0 0 2px rgba(94, 251, 215, 0.55); } +.fl-editor .fl-ta::selection { background: color-mix(in srgb, var(--sc-accent) 30%, transparent); } +.fl-editor:focus-within { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--sc-accent) 55%, transparent); } -.fl-actions { display: flex; align-items: center; gap: 0.6rem; padding: 0.45rem 1rem; background: #1b1a36; border-top: 1px solid rgba(185, 184, 255, 0.2); } +.fl-actions { display: flex; align-items: center; gap: 0.6rem; padding: 0.45rem 1rem; background: var(--sc-code-bar); border-top: 1px solid var(--sc-line); } .mode-output .fl-actions { border-top: 0; background: transparent; padding: 0.35rem 0.6rem; } .fl-actions button { font: 600 0.78rem var(--nm-font-head, system-ui, sans-serif); border: 0; border-radius: 999px; padding: 0.3rem 0.95rem; cursor: pointer; } -.fl-run { background: var(--nm-mint, #5efbd7); color: var(--nm-indigo, #2a2952); } +.fl-run { background: var(--sc-accent); color: var(--sc-accent-fg); } .fl-run:disabled { opacity: 0.55; cursor: progress; } -.fl-reset, .fl-toggle { background: transparent; color: var(--nm-lilac, #b9b8ff); border: 1px solid rgba(185, 184, 255, 0.5) !important; } -.fl-status { font-size: 0.76rem; color: var(--nm-muted, #b4b3e6); } +.fl-reset, .fl-toggle { background: transparent; color: var(--sc-code-text); border: 1px solid var(--sc-line) !important; } +.fl-status { font-size: 0.76rem; color: var(--sc-muted); } -.fl-out { border-top: 1px solid rgba(185, 184, 255, 0.2); background: #0e0d18; padding: 0.55rem 1.1rem 0.75rem; } +.fl-out { border-top: 1px solid var(--sc-line); background: var(--sc-code-out); padding: 0.55rem 1.1rem 0.75rem; } .mode-output .fl-out { border-top: 0; } -.fl-out pre { margin: 0.25rem 0 0; font: 0.84rem/1.55 ui-monospace, "Cascadia Code", Consolas, monospace; color: #c9f5ea; white-space: pre-wrap; max-height: 30vh; overflow: auto; } +.fl-out pre { margin: 0.25rem 0 0; font: 0.84rem/1.55 ui-monospace, "Cascadia Code", Consolas, monospace; color: var(--sc-code-text); white-space: pre-wrap; max-height: 30vh; overflow: auto; } .fl-figs img { display: block; max-width: 100%; max-height: 48vh; margin: 0.5rem auto 0; border-radius: 8px; background: #fff; } -.fl-err { color: var(--nm-salmon, #fb7a7a); font-size: 0.86rem; margin-top: 0.3rem; } -.fl-err details { margin-top: 0.25rem; color: var(--nm-muted, #b4b3e6); } -.fl-err pre { font: 0.74rem/1.5 ui-monospace, Consolas, monospace; color: #f5c1c1; } -.fl-note { margin: 0 0 0.3rem; font-size: 0.78rem; color: var(--nm-salmon, #fb7a7a); } -.fl-empty { margin: 0; font-size: 0.8rem; color: var(--nm-muted, #b4b3e6); font-style: italic; } +.fl-err { color: var(--sc-warm-text); font-size: 0.86rem; margin-top: 0.3rem; } +.fl-err details { margin-top: 0.25rem; color: var(--sc-muted); } +.fl-err pre { font: 0.74rem/1.5 ui-monospace, Consolas, monospace; color: var(--sc-warm-text); } +.fl-note { margin: 0 0 0.3rem; font-size: 0.78rem; color: var(--sc-warm-text); } +.fl-empty { margin: 0; font-size: 0.8rem; color: var(--sc-muted); font-style: italic; } -.tk-c { color: #8b8ac7; font-style: italic; } -.tk-s { color: var(--nm-mint, #5efbd7); font-style: normal; } -.tk-k { color: var(--nm-salmon, #fb7a7a); font-style: normal; } -.tk-n { color: var(--nm-lilac, #b9b8ff); font-style: normal; } -.tk-f { color: #7cc4ff; font-style: normal; } -.tk-b { font-style: normal; color: #fff; background: rgba(251, 122, 122, 0.35); border-radius: 4px; } +.tk-c { color: var(--sc-tok-comment); font-style: italic; } +.tk-s { color: var(--sc-tok-string); font-style: normal; } +.tk-k { color: var(--sc-tok-keyword); font-style: normal; } +.tk-n { color: var(--sc-tok-number); font-style: normal; } +.tk-f { color: var(--sc-tok-function); font-style: normal; } +.tk-b { font-style: normal; color: inherit; background: color-mix(in srgb, var(--sc-warm) 30%, transparent); border-radius: 4px; } .is-collapsed .fl-editor { display: none; } diff --git a/js/src/scrolly/assets/scrolly.css b/js/src/scrolly/assets/scrolly.css index 58a7a5d..21d241d 100644 --- a/js/src/scrolly/assets/scrolly.css +++ b/js/src/scrolly/assets/scrolly.css @@ -1,4 +1,4 @@ -/* Scrollytelling page layout (plugins/scrolly/). +/* Scrollytelling page layout and colours (js/src/scrolly/). Attached to the engine widget so MyST bundles it; engine.mjs copies it into . Without JavaScript the page is a plain linear document; the pinned layout switches on when the engine sets data-ready. @@ -26,28 +26,95 @@ body:has(.myst-primary-sidebar.lg\:hidden) .article-grid:not(:has(> .lg\:col-margin-right)) > .scrolly-col { grid-column: screen-inset; } } +/* ---- colours ---- + The section follows the site: light and dark mode (the theme's `.dark` class + on ), the brand colours from the kit stylesheet (--nma-brand-*, which a + course changes with `nma.brand`) and the kit's syntax colours (--nma-tok-*, + shared with the editable cells). The hex values are the Neuromatch defaults, + used when the kit stylesheet is not loaded. runnable.css and the HUD in + engine.mjs read the same --sc-* tokens through the shadow DOM. */ .scrolly { --scrolly-top: 60px; - --nm-indigo: #2a2952; - --nm-indigo-deep: #1c1b3e; - --nm-purple: #45429c; - --nm-blue: #008cbf; - --nm-salmon: #fa7a7a; - --nm-mint: #5efbd7; - --nm-lilac: #b9b8ff; - --nm-cream: #f3f0ec; - --nm-ink: #161922; - --nm-muted: #b4b3e6; - --nm-text: #f3f0ec; + --sc-primary: var(--nma-brand-primary, #008cbf); + --sc-primary-light: var(--nma-brand-primary-light, #35b4dc); + --sc-secondary: var(--nma-brand-secondary, #45429c); + --sc-warm: var(--nma-brand-accent, #fa7a7a); + + --sc-bg: #f7f8fa; + --sc-text: #1a1a1a; + --sc-muted: #7a7a7a; + --sc-line: #e6e6e6; + --sc-heading: var(--sc-secondary); + --sc-title: var(--sc-secondary); + --sc-link: var(--sc-primary); + --sc-accent: var(--sc-primary); + --sc-accent-fg: #ffffff; + --sc-warm-text: color-mix(in srgb, var(--sc-warm) 60%, #1a1a1a); + --sc-glow-1: color-mix(in srgb, var(--sc-secondary) 10%, transparent); + --sc-glow-2: color-mix(in srgb, var(--sc-primary) 12%, transparent); + --sc-track: rgba(26, 26, 26, 0.12); + --sc-chip-bg: color-mix(in srgb, var(--sc-primary) 6%, #ffffff); + --sc-chip-line: color-mix(in srgb, var(--sc-primary) 28%, transparent); + + --sc-card-bg: #ffffff; + --sc-card-text: #1a1a1a; + --sc-card-code: color-mix(in srgb, var(--sc-primary) 10%, #ffffff); + --sc-shadow: 0 10px 30px rgba(26, 26, 26, 0.1); + + --sc-code-bg: #ffffff; + --sc-code-bar: #f7f8fa; + --sc-code-out: #f7f8fa; + --sc-code-text: #1a1a1a; + --sc-code-head-fg: var(--sc-secondary); + --sc-tok-comment: var(--nma-tok-comment-light, #7a7a7a); + --sc-tok-string: var(--nma-tok-string-light, #0e7c86); + --sc-tok-keyword: var(--nma-tok-keyword-light, #45429c); + --sc-tok-number: var(--nma-tok-number-light, #b5484b); + --sc-tok-function: var(--nma-tok-function-light, #006b8f); + --nm-font-body: inherit; --nm-font-head: inherit; position: relative; margin-block: 1.5rem; - color: var(--nm-text); - background: var(--nm-indigo-deep); + color: var(--sc-text); + background: var(--sc-bg); + border: 1px solid var(--sc-line); border-radius: 12px; padding: 1rem; } +.dark .scrolly { + --sc-bg: #232323; + --sc-text: #f7f8fa; + --sc-muted: #bdcfcf; + --sc-line: #3a3a3a; + --sc-heading: var(--nma-tok-keyword-dark, #b9b8ff); + --sc-title: #f7f8fa; + --sc-link: var(--sc-primary-light); + --sc-accent: var(--sc-primary-light); + --sc-accent-fg: #14131f; + --sc-warm-text: color-mix(in srgb, var(--sc-warm) 75%, #ffffff); + --sc-glow-1: color-mix(in srgb, var(--sc-secondary) 45%, transparent); + --sc-glow-2: color-mix(in srgb, var(--sc-primary) 18%, transparent); + --sc-track: rgba(255, 255, 255, 0.14); + --sc-chip-bg: rgba(255, 255, 255, 0.05); + --sc-chip-line: color-mix(in srgb, var(--sc-primary-light) 30%, transparent); + + --sc-card-bg: #2e2e2e; + --sc-card-text: #f7f8fa; + --sc-card-code: rgba(255, 255, 255, 0.1); + --sc-shadow: 0 14px 40px rgba(0, 0, 0, 0.45); + + --sc-code-bg: #1b1b1b; + --sc-code-bar: #262626; + --sc-code-out: #151515; + --sc-code-text: #e6e6e6; + --sc-code-head-fg: #bdcfcf; + --sc-tok-comment: var(--nma-tok-comment-dark, #9aa7a7); + --sc-tok-string: var(--nma-tok-string-dark, #5efbd7); + --sc-tok-keyword: var(--nma-tok-keyword-dark, #b9b8ff); + --sc-tok-number: var(--nma-tok-number-dark, #ff9e9e); + --sc-tok-function: var(--nma-tok-function-dark, #5ec8ec); +} .scrolly *, .scrolly *::before, .scrolly *::after { box-sizing: border-box; } .scrolly-engine { display: block; } .scrolly-stage { margin-bottom: 1.5rem; } @@ -77,8 +144,8 @@ body:has(.scrolly) .myst-jp-nb-toolbar-power { display: none !important; } z-index: 0; border-radius: 12px; background: - radial-gradient(ellipse 60% 50% at 78% 20%, rgba(68, 67, 156, 0.55), transparent 70%), - radial-gradient(ellipse 40% 40% at 10% 95%, rgba(0, 137, 247, 0.16), transparent 70%); + radial-gradient(ellipse 60% 50% at 78% 20%, var(--sc-glow-1), transparent 70%), + radial-gradient(ellipse 40% 40% at 10% 95%, var(--sc-glow-2), transparent 70%); } .scrolly[data-ready="true"] .scrolly-view { position: absolute; @@ -104,59 +171,60 @@ body:has(.scrolly) .myst-jp-nb-toolbar-power { display: none !important; } pointer-events: auto; max-width: 32rem; min-width: 0; /* flex item in .scrolly-step: don't grow to the widest code line */ - background: var(--nm-cream); - color: var(--nm-ink); + background: var(--sc-card-bg); + color: var(--sc-card-text); + border: 1px solid var(--sc-line); border-radius: 14px; - border-left: 6px solid var(--nm-salmon); + border-left: 6px solid var(--sc-warm); padding: 1.1rem 1.35rem 1.25rem; - box-shadow: 0 14px 40px rgba(8, 7, 30, 0.45); + box-shadow: var(--sc-shadow); line-height: 1.6; } .scrolly[data-ready="true"] .scrolly-card { opacity: 0.32; transform: translateX(-10px) scale(0.985); transition: opacity 0.35s, transform 0.35s; } .scrolly[data-ready="true"] .scrolly-step.is-active .scrolly-card { opacity: 1; transform: none; } .scrolly-card > :first-child { margin-top: 0; } .scrolly-card > :last-child { margin-bottom: 0; } -.scrolly-card :is(h1, h2, h3, h4, h5, h6) { display: block; border: 0; padding: 0; margin: 0.2rem 0 0.45rem; font-weight: 600; font-size: 1.35rem; line-height: 1.2; color: var(--nm-indigo); } +.scrolly-card :is(h1, h2, h3, h4, h5, h6) { display: block; border: 0; padding: 0; margin: 0.2rem 0 0.45rem; font-weight: 600; font-size: 1.35rem; line-height: 1.2; color: var(--sc-heading); } .scrolly-card p { margin: 0 0 0.6rem; font-size: 1rem; } -.scrolly-card :is(p, li, strong, em, span, b, i, td, th) { color: var(--nm-ink); } -.scrolly-card code { background: #e4e0f7; color: var(--nm-indigo); padding: 0.05em 0.4em; border-radius: 5px; font-size: 0.88em; } -.scrolly-card a { color: var(--nm-purple); } +.scrolly-card :is(p, li, strong, em, span, b, i, td, th) { color: var(--sc-card-text); } +.scrolly-card code { background: var(--sc-card-code); color: var(--sc-heading); padding: 0.05em 0.4em; border-radius: 5px; font-size: 0.88em; } +.scrolly-card a { color: var(--sc-link); } .scrolly-card .katex-display { overflow-x: auto; overflow-y: hidden; } .scrolly-card details { margin: 0.5rem 0; } .scrolly-card pre { max-width: 100%; overflow-x: auto; } .scrolly-card .nma-solution-link p { margin: 0.4rem 0; } .scrolly-kicker { margin: 0 0 0.1rem; } -.scrolly-kicker p { margin: 0; font-weight: 600; font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--nm-purple) !important; } +.scrolly-kicker p { margin: 0; font-weight: 600; font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--sc-heading) !important; } /* ---- stage content ---- */ -.scrolly-view { color: var(--nm-text); } +.scrolly-view { color: var(--sc-text); } .scrolly-view :is(p, li, strong, em, span) { color: inherit; } -.scrolly-view :is(h1, h2, h3, h4) { display: block; border: 0; padding: 0; color: #fff; } -.scrolly-view a { color: var(--nm-mint); } -.scrolly-view img { max-width: 100%; max-height: 70vh; width: auto; height: auto; border-radius: 10px; background: #fff; margin: 0 auto; display: block; } +.scrolly-view :is(h1, h2, h3, h4) { display: block; border: 0; padding: 0; color: var(--sc-title); } +.scrolly-view a { color: var(--sc-link); } +.scrolly-view img { max-width: 100%; max-height: 70vh; width: auto; height: auto; border-radius: 10px; background: #fff; border: 1px solid var(--sc-line); margin: 0 auto; display: block; } .scrolly-view video { max-width: 100%; max-height: 70vh; border-radius: 10px; margin: 0 auto; display: block; } .scrolly-view figure { margin: 0; } -.scrolly-view figcaption, .scrolly-view figcaption * { color: var(--nm-muted); } +.scrolly-view figcaption, .scrolly-view figcaption * { color: var(--sc-muted); } .scrolly-view iframe { width: 100%; aspect-ratio: 16 / 9; height: auto; border: 0; border-radius: 10px; background: #000; } .scrolly-view .nma-video p, .scrolly-view .nma-slides p { margin: 0.3rem 0; font-size: 0.85rem; } -.scrolly-view .nma-video [role="tab"] { color: var(--nm-lilac); } +.scrolly-view .nma-video [role="tab"] { color: var(--sc-link); } .scrolly-view .scrolly-runnable { width: 100%; } .scrolly-view.is-list ol, .scrolly-view.is-list ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.75rem; counter-reset: chip; } -.scrolly-view.is-list li { counter-increment: chip; display: grid; grid-template-columns: 2.6rem 1fr; align-items: center; gap: 0.9rem; font-size: 1.05rem; line-height: 1.45; background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(185, 184, 255, 0.2); border-radius: 12px; padding: 0.75rem 1rem; margin: 0; } -.scrolly-view.is-list li::before { content: counter(chip); display: grid; place-items: center; width: 2.6rem; height: 2.6rem; border-radius: 50%; background: var(--nm-mint); color: var(--nm-indigo); font-weight: 700; font-size: 1.1rem; } +.scrolly-view.is-list li { counter-increment: chip; display: grid; grid-template-columns: 2.6rem 1fr; align-items: center; gap: 0.9rem; font-size: 1.05rem; line-height: 1.45; background: var(--sc-chip-bg); border: 1px solid var(--sc-chip-line); border-radius: 12px; padding: 0.75rem 1rem; margin: 0; } +.scrolly-view.is-list li::before { content: counter(chip); display: grid; place-items: center; width: 2.6rem; height: 2.6rem; border-radius: 50%; background: var(--sc-accent); color: var(--sc-accent-fg); font-weight: 700; font-size: 1.1rem; } .scrolly-view.is-list li > p { margin: 0; } .scrolly-view.is-list :is(h2, h3, h4) { margin: 0 0 0.5rem; font-size: clamp(1.4rem, 4cqi, 2.2rem); font-weight: 700; } /* ---- hero (title and closing screens) ---- MyST re-levels headings, so the title is styled by position, not by tag. */ .scrolly-view.is-hero { max-width: 72rem; margin-inline: auto; padding: 0 clamp(0.5rem, 5cqi, 4rem); } -.scrolly-view.is-hero .scrolly-kicker p { font-size: 0.85rem; letter-spacing: 0.32em; color: var(--nm-mint) !important; margin-bottom: 0.6rem; } +.scrolly-view.is-hero .scrolly-kicker p { font-size: 0.85rem; letter-spacing: 0.32em; color: var(--sc-accent) !important; margin-bottom: 0.6rem; } .scrolly-view.is-hero :is(h1, h2, h3, h4, h5, h6) { display: block; margin: 0; padding: 0; border: 0; } -.scrolly-view.is-hero :is(h1, h2, h3, h4, h5, h6):not(:is(h1, h2, h3, h4, h5, h6) ~ *) { font-weight: 700; font-size: clamp(2rem, 8cqi, 4.6rem); line-height: 1.05; color: #fff; } -.scrolly-view.is-hero :is(h1, h2, h3, h4, h5, h6) ~ :is(h1, h2, h3, h4, h5, h6) { font-weight: 500; font-size: 1.15rem; color: var(--nm-lilac); margin: 0.9rem 0 0.3rem; } -.scrolly-view.is-hero :is(h1, h2, h3, h4, h5, h6) em { font-style: normal; background: linear-gradient(90deg, var(--nm-salmon), var(--nm-mint)); -webkit-background-clip: text; background-clip: text; color: transparent; } +.scrolly-view.is-hero :is(h1, h2, h3, h4, h5, h6):not(:is(h1, h2, h3, h4, h5, h6) ~ *) { font-weight: 700; font-size: clamp(2rem, 8cqi, 4.6rem); line-height: 1.05; color: var(--sc-title); } +.scrolly-view.is-hero :is(h1, h2, h3, h4, h5, h6) ~ :is(h1, h2, h3, h4, h5, h6) { font-weight: 500; font-size: 1.15rem; color: var(--sc-link); margin: 0.9rem 0 0.3rem; } +.scrolly-view.is-hero :is(h1, h2, h3, h4, h5, h6) em { font-style: normal; background: linear-gradient(90deg, var(--sc-warm), var(--sc-accent)); -webkit-background-clip: text; background-clip: text; color: transparent; } .scrolly-view.is-hero p { margin: 0.5rem 0; font-size: 0.98rem; line-height: 1.5; max-width: 52rem; } -.scrolly-view.is-hero p strong { color: var(--nm-salmon); } +.scrolly-view.is-hero p strong { color: var(--sc-warm-text); } /* ---- narrow sections: stage on top, cards below it ---- Keep the 760px breakpoint in sync with WIDE in engine.mjs. */ diff --git a/tests/js/scrolly.test.mjs b/tests/js/scrolly.test.mjs index 9c279c3..6273703 100644 --- a/tests/js/scrolly.test.mjs +++ b/tests/js/scrolly.test.mjs @@ -314,3 +314,42 @@ test('cards stay in their column when they hold wide content (code blocks)', () assert.ok(pre, 'no .scrolly-card pre rule'); assert.match(pre.body, /overflow-x:\s*auto/); }); + +// ---- colours: light and dark mode share one token set ----------------------- + +const STYLE_DIR = join(here, '..', '..', 'js', 'src', 'scrolly', 'assets'); +const cssFiles = ['scrolly.css', 'runnable.css', 'engine.mjs'].map((f) => readFileSync(join(STYLE_DIR, f), 'utf8')); +const layoutCss = cssFiles[0]; +const block = (selector) => { + const start = layoutCss.indexOf(`${selector} {`); + assert.ok(start >= 0, `${selector} block exists`); + return layoutCss.slice(start, layoutCss.indexOf('\n}', start)); +}; +const defined = (css) => new Set([...css.matchAll(/^\s*(--sc-[\w-]+):/gm)].map((m) => m[1])); + +test('every --sc-* token used by the scrolly styles is defined on .scrolly', () => { + const light = defined(block('.scrolly')); + const used = new Set(cssFiles.flatMap((css) => [...css.matchAll(/var\((--sc-[\w-]+)/g)].map((m) => m[1]))); + const undefinedTokens = [...used].filter((t) => !light.has(t)); + assert.deepEqual(undefinedTokens, []); +}); + +test('dark mode only overrides tokens that exist in light mode, and covers the surfaces', () => { + const light = defined(block('.scrolly')); + const dark = defined(block('.dark .scrolly')); + assert.deepEqual([...dark].filter((t) => !light.has(t)), []); + for (const surface of ['--sc-bg', '--sc-text', '--sc-card-bg', '--sc-card-text', '--sc-code-bg', '--sc-code-text', '--sc-line']) { + assert.ok(dark.has(surface), `dark mode sets ${surface}`); + } +}); + +test('scrolly styles use the kit brand variables, with the Neuromatch defaults as fallbacks', () => { + const light = block('.scrolly'); + for (const v of ['--nma-brand-primary', '--nma-brand-primary-light', '--nma-brand-secondary', '--nma-brand-accent']) { + assert.match(light, new RegExp(`var\\(${v}, #[0-9a-f]{6}\\)`), `${v} has a fallback`); + } +}); + +test('the old fixed indigo palette is gone', () => { + for (const css of cssFiles) assert.doesNotMatch(css, /--nm-(indigo|mint|salmon|cream|lilac|ink|purple|blue|text|muted)\b/); +}); From d371b8713a520aa1c2aa4a63c134b84df2acd086 Mon Sep 17 00:00:00 2001 From: Konstantine Tsafatinos Date: Tue, 20 Oct 2026 00:34:03 -0400 Subject: [PATCH 3/4] test(e2e): report who asks for Google fonts; scope the Rubik check to what the kit controls; soften font claims --- AGENTS.md | 7 +++++-- README.md | 2 +- python/README.md | 4 ++-- tests/e2e/test_branding_browser.py | 29 +++++++++++++++++++++++++---- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 497996f..14979cc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,8 +109,11 @@ templates/wheels.yml # optional caller for course-wheels.yml file paths of a *remote* extended config is unverified. Don't move them into `nma.yml` without testing that in a real course. - **One `style` slot**: `site.css` = kit CSS (Rubik embedded as data URIs, so - no relative `url()` depends on where MyST serves the file and no request - leaves the site) + `:root` overrides from `nma.brand` + the course's + no relative `url()` depends on where MyST serves the file and the kit's CSS + makes no font requests; the built site still requests Roboto from + `fonts.gstatic.com` from something that isn't ours, see + `tests/e2e/test_branding_browser.py::test_no_google_font_requests`) + + `:root` overrides from `nma.brand` + the course's `nma.style.extra` file. - **CSS variables are the API**: `--nma-brand-*` (the five colours a course may change), `--nma-cell-*` and `--nma-tok-*` (read by `cells/assets/cells.css`). diff --git a/README.md b/README.md index 48b2da5..ab6fd26 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ nma: ## Site branding The kit carries the Neuromatch look: the colour palette, the Rubik font -(embedded in the stylesheet, so readers' browsers don't contact Google), the +(embedded in the stylesheet, so the kit's own CSS makes no font requests), the logos and the favicon. It also styles the kit's own widgets (editable cells, scrollytelling) and hides JupyterLite's toolbar, so courses no longer carry that rule themselves. diff --git a/python/README.md b/python/README.md index 377b6c3..c512103 100644 --- a/python/README.md +++ b/python/README.md @@ -68,8 +68,8 @@ site: favicon: _nma/favicon.png ``` -The stylesheet is the kit's (Neuromatch palette, Rubik embedded so readers' -browsers don't contact Google, styles for the kit's widgets), then your +The stylesheet is the kit's (Neuromatch palette, Rubik embedded, so the +kit's own CSS makes no font requests, styles for the kit's widgets), then your `nma.brand` colours, then your `nma.style.extra` CSS. Brand keys: `primary`, `primary_hover`, `primary_light`, `secondary`, `accent`. Setting only `primary` derives the hover and light variants. `_nma/` ignores itself, so don't commit diff --git a/tests/e2e/test_branding_browser.py b/tests/e2e/test_branding_browser.py index 925d65f..2c186a9 100644 --- a/tests/e2e/test_branding_browser.py +++ b/tests/e2e/test_branding_browser.py @@ -5,13 +5,25 @@ pytest.importorskip("playwright") PAGE = "/tutorials/w2d1-examples/w2d1-tutorial1/" +GOOGLE_FONTS = ("fonts.googleapis.com", "fonts.gstatic.com") -def test_brand_font_and_colours_apply_without_external_requests(page, base_url): - requests = [] - page.on("request", lambda r: requests.append(r.url)) +def load(page, base_url): + """Open the page; return the font requests it made as (url, referer, resource type).""" + fonts = [] + + def record(request): + if any(host in request.url for host in GOOGLE_FONTS): + fonts.append((request.url, request.headers.get("referer", "?"), request.resource_type)) + + page.on("request", record) page.goto(base_url + PAGE) page.wait_for_load_state("networkidle") + return fonts + + +def test_brand_font_and_colours_apply(page, base_url): + fonts = load(page, base_url) # Rubik comes from the embedded data URI, so it must be loaded and in use. assert page.evaluate("document.fonts.ready.then(() => document.fonts.check('1em Rubik'))") @@ -21,7 +33,16 @@ def test_brand_font_and_colours_apply_without_external_requests(page, base_url): "getComputedStyle(document.documentElement).getPropertyValue('--nma-brand-primary').trim()") assert primary.upper() == "#008CBF" - assert not [u for u in requests if "fonts.googleapis.com" in u or "fonts.gstatic.com" in u] + # What the kit controls: Rubik is never fetched from Google. + assert not [f for f in fonts if "rubik" in f[0].lower()], fonts + + +def test_no_google_font_requests(page, base_url): + """The kit's stylesheet makes no font requests. Anything reported here comes + from something else on the page (the theme, thebe, ...): the Referer is the + stylesheet that asked for it.""" + fonts = load(page, base_url) + assert not fonts, "\n".join(f"{kind}: {url}\n asked by {referer}" for url, referer, kind in sorted(set(fonts))) def test_favicon_and_logo_come_from_the_kit(page, base_url): From 694206bbbd69a8b6b8460a69fc6913d7a976c2d2 Mon Sep 17 00:00:00 2001 From: Konstantine Tsafatinos Date: Tue, 20 Oct 2026 00:37:47 -0400 Subject: [PATCH 4/4] ci: speed up the browser smoke test (shared browser context, cached Playwright, parallel with unit tests, durations) --- .github/workflows/ci.yml | 15 +++++++++++---- tests/e2e/conftest.py | 30 ++++++++++++++++++++++++++---- tests/e2e/test_branding_browser.py | 3 +++ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6dcfce..762c212 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,9 +44,9 @@ jobs: # The slowest job. A release PR only changes version strings and the # changelog, so it is skipped there (a skipped job counts as passing for # required checks). + # It builds the JS bundle itself, so it runs in parallel with the unit tests. example-course: if: github.head_ref != 'release-please--branches--main--components--course-kit' - needs: [python, js] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -65,14 +65,21 @@ jobs: (cd js && npm ci --no-audit --no-fund) - name: Build the example course run: tests/scripts/build-example.sh + - uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('python/pyproject.toml', 'python/uv.lock') }} + restore-keys: playwright-${{ runner.os }}- + - name: Install the browser + run: python/.venv/bin/playwright install --with-deps chromium # Cold Pyodide/JupyterLite downloads make the first run timing-sensitive, - # so failed tests are retried once. + # so failed tests are retried once. The tests share one browser context + # (see tests/e2e/conftest.py); --durations shows where the time goes. - name: Browser smoke test run: | - python/.venv/bin/playwright install --with-deps chromium (cd tests/example-course/_build/html && python3 -m http.server 8765 >/dev/null 2>&1 &) sleep 2 - NMA_E2E_URL=http://localhost:8765 python/.venv/bin/pytest -q --reruns 1 tests/e2e + NMA_E2E_URL=http://localhost:8765 python/.venv/bin/pytest -v --durations=15 --reruns 1 tests/e2e course-check: needs: [python, js] diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 9fbc3b1..585a5a4 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -8,6 +8,9 @@ loads require.js from cdnjs; in sandboxes where cdnjs is blocked, set this to serve it locally. Service workers are then blocked, because Playwright can't intercept requests that go through JupyterLite's service worker. + +NMA_E2E_FRESH_CONTEXT=1 (optional): a new browser context per test instead of one +shared by the whole run (slower: every test downloads Pyodide again). """ import os @@ -39,21 +42,40 @@ def _browser(): browser.close() -@pytest.fixture -def page(_browser): +def _new_context(browser): options = {"ignore_https_errors": True} if REQUIREJS: options["service_workers"] = "block" - context = _browser.new_context(**options) + context = browser.new_context(**options) if REQUIREJS: context.route( "**/require.min.js", lambda route: route.fulfill(path=REQUIREJS, content_type="application/javascript"), ) + return context + + +@pytest.fixture(scope="session") +def _shared_context(_browser): + """One context for the whole run. Pages in a context share the HTTP cache, so + Pyodide and its packages are downloaded once, not once per test (the slow + part of this suite). Set NMA_E2E_FRESH_CONTEXT=1 for a new context per test, + to rule out state leaking between tests.""" + context = _new_context(_browser) + yield context + context.close() + + +@pytest.fixture +def page(_browser, _shared_context): + fresh = os.environ.get("NMA_E2E_FRESH_CONTEXT") == "1" + context = _new_context(_browser) if fresh else _shared_context page = context.new_page() page.set_default_timeout(120_000) yield page - context.close() + page.close() + if fresh: + context.close() @pytest.fixture diff --git a/tests/e2e/test_branding_browser.py b/tests/e2e/test_branding_browser.py index 2c186a9..eb3eaf7 100644 --- a/tests/e2e/test_branding_browser.py +++ b/tests/e2e/test_branding_browser.py @@ -4,6 +4,9 @@ pytest.importorskip("playwright") +# Deterministic checks: a failure is not cold-cache flakiness, so don't rerun it. +pytestmark = pytest.mark.flaky(reruns=0) + PAGE = "/tutorials/w2d1-examples/w2d1-tutorial1/" GOOGLE_FONTS = ("fonts.googleapis.com", "fonts.gstatic.com")