AT Mot — a bilingual (EN/FR) daily word game native to the AT Protocol.
0

Configure Feed

Select the types of activity you want to include in your feed.

Merge remote-tracking branch 'origin/trunk' into add/obfuscate-answers

# Conflicts:
# DECISIONS.md

+41 -84
+25 -34
DECISIONS.md
··· 25 25 26 26 4. **Sources:** English dictionary = **ENABLE** (Public Domain); French dictionary = 27 27 **an-array-of-french-words** (MIT); commonness ranking for both = **hermitdave/FrequencyWords** 28 - (MIT, OpenSubtitles). The French list is sourced independently with **no relation to** the 29 - `wordle-fr` / “Le Mot” project, as required. 28 + (MIT, OpenSubtitles). 30 29 31 30 5. **Validity vs. commonness are separated.** Frequency lists alone leak proper nouns, foreign 32 31 words, and apostrophe-mangled contraction fragments. So the *dictionary* decides validity and the ··· 66 65 67 66 ## Design 68 67 69 - 12. **Type = Overused Grotesk (display/body) + IBM Plex Mono (data/provenance)** — the exact 70 - skypress.blog stack, self-hosted (both OFL). 68 + 12. **Type = Overused Grotesk (display/body) + IBM Plex Mono (data/provenance)**, self-hosted 69 + (both OFL). 71 70 72 71 13. **Board palette:** teal `#0F766E` (correct), burnt amber `#B45309` (present), slate `#5B6776` 73 - (absent) — deliberately not Wordle's green/yellow. The same saturated chips are used in both 72 + (absent). The same saturated chips are used in both 74 73 light and dark themes, which guarantees AA contrast for the white tile letters in either theme. 75 74 Secondary non-hue cue: an inner ring on "present" tiles plus per-tile aria labels announcing the 76 75 state, so colour-blind and screen-reader players read the board clearly. The shared emoji grid ··· 95 94 96 95 ## Tooling 97 96 98 - 18. **Dev tooling (Vite/Vitest) bumped to latest majors** to clear all `npm audit` advisories 99 - (currently **0 vulnerabilities**). The advisories were dev-server-only (esbuild), never in 100 - shipped code. 97 + 18. **Dev tooling (Vite/Vitest) bumped to latest majors** to clear all `npm audit` advisories. The 98 + advisories were dev-server-only (esbuild), never in shipped code. 101 99 102 100 19. **atcute versions:** `@atcute/client@^5`, `@atcute/oauth-browser-client@^4`, 103 101 `@atcute/identity-resolver@^2` (newer than the spec's illustrative snippets; the API used is 104 102 equivalent). 105 103 104 + ## PWA / installability 105 + 106 + 20. **Manifest-only PWA — deliberately no service worker.** The app is installable 107 + to the home screen via `public/manifest.webmanifest` + committed PNG icons 108 + (rasterized once from `icon.svg`, flattened onto `#0e1116`) + apple/mobile 109 + web-app meta tags. There is intentionally **no service worker and no caching 110 + layer**: this is a daily game with dynamic reads (Constellation leaderboard, 111 + PDS writes, OAuth), and an over-eager cache is exactly what would serve a 112 + stale puzzle or stale bundle. Consequence: iOS add-to-home-screen and 113 + Android manual "Add to Home Screen" both work from the manifest alone; the 114 + proactive Android install banner (which needs a service worker) is forgone on 115 + purpose. Loading speed is unchanged (CDN + ~94 KB bundle) with no new failure 116 + modes. The regression guard `tests/pwa.test.ts` uses `node:*` imports, so 117 + `"node"` was added to the shared `tsconfig.json` `types` allowlist — a 118 + conscious trade-off: Node globals become visible to `src/` typechecking, but 119 + `noEmit` + Vite bundling mean it can't affect runtime; narrow later by giving 120 + `tests` its own node-typed project if that boundary is wanted. 121 + 106 122 ## Anti-cheat 107 123 108 - 22. **The daily-answer sequence ships obfuscated, not in plaintext.** The ordered 124 + 21. **The daily-answer sequence ships obfuscated, not in plaintext.** The ordered 109 125 `answers` array used to ship as plain English words, so anyone could open the 110 126 JSON (in the repo or the JS bundle / network tab), find their puzzle number, 111 127 and read today's — or any future day's — word. It now ships as `answersEnc`: ··· 123 139 reveals no day mapping); (b) French answer words that carry accents remain 124 140 individually visible as keys in the plaintext `accents` display map — but not 125 141 their day-order, so the daily mapping stays hidden. 126 - 127 - ## Handoff note 128 - 129 - 20. **No git remote is configured.** Everything is committed to the **`trunk`** branch, which is the 130 - repository's current/default branch locally (HEAD points at it). Creating a remote and marking 131 - `trunk` as the default branch *on the host* (GitHub/Tangled/etc.) is a one-step manual action for 132 - the owner, since no remote exists to push to yet. 133 - 134 - ## PWA / installability 135 - 136 - 21. **Manifest-only PWA — deliberately no service worker.** The app is installable 137 - to the home screen via `public/manifest.webmanifest` + committed PNG icons 138 - (rasterized once from `icon.svg`, flattened onto `#0e1116`) + apple/mobile 139 - web-app meta tags. There is intentionally **no service worker and no caching 140 - layer**: this is a daily game with dynamic reads (Constellation leaderboard, 141 - PDS writes, OAuth), and an over-eager cache is exactly what would serve a 142 - stale puzzle or stale bundle. Consequence: iOS add-to-home-screen and 143 - Android manual "Add to Home Screen" both work from the manifest alone; the 144 - proactive Android install banner (which needs a service worker) is forgone on 145 - purpose. Loading speed is unchanged (CDN + ~94 KB bundle) with no new failure 146 - modes. The regression guard `tests/pwa.test.ts` uses `node:*` imports, so 147 - `"node"` was added to the shared `tsconfig.json` `types` allowlist — a 148 - conscious trade-off: Node globals become visible to `src/` typechecking, but 149 - `noEmit` + Vite bundling mean it can't affect runtime; narrow later by giving 150 - `tests` its own node-typed project if that boundary is wanted.
+1 -5
NOTICE.md
··· 13 13 - **hermitdave/FrequencyWords** — English & French commonness ranking (OpenSubtitles). 14 14 Source: <https://github.com/hermitdave/FrequencyWords>. **MIT** © Hermit Dave. 15 15 16 - The French list is sourced independently and has no relation to, and reuses no code or data from, 17 - the `wordle-fr` / “Le Mot” project. 18 - 19 16 ## Fonts (bundled in `public/fonts/`) 20 17 21 18 - **Overused Grotesk** (`OverusedGrotesk-VF.woff2`). ··· 23 20 - **IBM Plex Mono** (`ibm-plex-mono-latin-*.woff2`). 24 21 By IBM. **SIL Open Font License 1.1.** <https://github.com/IBM/plex> 25 22 26 - These match the typography of [skypress.blog](https://skypress.blog) for family consistency across 27 - the author's properties. Both are OFL-licensed and freely redistributable. 23 + Both are OFL-licensed and freely redistributable. 28 24 29 25 ## Runtime dependencies 30 26
+5 -15
README.md
··· 3 3 **A bilingual (English / French) daily word game, native to the [AT Protocol](https://atproto.com).** 4 4 Guess the hidden five-letter word in six tries. Your results live in **your own PDS**, and the 5 5 leaderboard is read from the public **[Constellation](https://constellation.microcosm.blue)** 6 - backlink index — there is **no application database and no bespoke backend.** 6 + backlink index. 7 7 8 8 🌐 **[atmot.herve.bzh](https://atmot.herve.bzh)** 9 9 ··· 11 11 12 12 --- 13 13 14 - ## Why it's interesting 15 - 16 - AT Mot is a working example of the **“data on your PDS, reads via Constellation, no AppView”** 17 - architecture for atproto apps: 14 + AT Mot uses a **”data on your PDS, reads via Constellation, no AppView”** architecture for atproto 15 + apps: 18 16 19 17 - **Your data is yours.** Each finished puzzle is written as a `bzh.herve.atmot.result` record to 20 18 your repo via browser OAuth. There is no server storing your games — there is no server. ··· 26 24 gets the same word on the same calendar day, with no server in the loop. 27 25 - **Standards-conformant lexicons** under the `bzh.herve.atmot.*` authority, following the 28 26 [Lexicon Style Guide](https://atproto.com/guides/lexicon-style-guide). 29 - - **Installs to your home screen, no strings attached.** A web manifest makes it installable as a 30 - standalone app — with **no service worker and no caching**, so there's none of the stale-content 31 - machinery that fights against a daily game whose reads are always live. 32 - 33 - It is **not** a clone of any commercial word game: it ships its own word lists, its own brand and 34 - look-and-feel (a sky/atproto-leaning **teal / amber / slate** board palette, deliberately distinct), 35 - and its own lexicons. The five-in-a-row word-guessing mechanic is generic; the implementation, 36 - data, and design here are original. 27 + - **Installable to your home screen.** A web manifest makes it installable as a standalone app. 37 28 38 29 ## Architecture at a glance 39 30 ··· 96 87 | English | [ENABLE](https://github.com/dolph/dictionary) — **Public Domain** | hermitdave/FrequencyWords (MIT) | 97 88 | French | [an-array-of-french-words](https://github.com/words/an-array-of-french-words) — **MIT** | hermitdave/FrequencyWords (MIT) | 98 89 99 - The French list is sourced **independently** and has **no relation to** the `wordle-fr` / “Le Mot” 100 - project. Lists are generated by `npm run build:words` and committed (the generated JSON _is_ the game 90 + Lists are generated by `npm run build:words` and committed (the generated JSON _is_ the game 101 91 content); the build is reproducible (same sources + fixed seeds → identical output). 102 92 103 93 ## Develop
-3
src/i18n.ts
··· 45 45 backToGame: string; 46 46 langName: string; 47 47 about: string; 48 - sourceCode: string; 49 48 profile: string; 50 49 } 51 50 ··· 97 96 backToGame: 'Back to today’s game', 98 97 langName: 'English', 99 98 about: 'About', 100 - sourceCode: 'Source code on tangled.org', 101 99 profile: '@jeremy.herve.bzh on Bluesky', 102 100 }; 103 101 ··· 149 147 backToGame: 'Retour au mot du jour', 150 148 langName: 'Français', 151 149 about: 'À propos', 152 - sourceCode: 'Code source sur tangled.org', 153 150 profile: '@jeremy.herve.bzh sur Bluesky', 154 151 }; 155 152
+3 -23
src/ui/footer.ts
··· 3 3 import { navTo } from './header.js'; 4 4 import type { Ctx } from './context.js'; 5 5 6 - const SOURCE_URL = 'https://tangled.org/jeremy.herve.bzh/atmot'; 7 6 const PROFILE_URL = 'https://bsky.app/profile/jeremy.herve.bzh'; 8 7 9 8 /** 10 - * The tangled.org mark, inlined so it inherits the footer text colour 11 - * (and therefore tracks light/dark mode) via `currentColor`. Matches the icon 12 - * skypress.blog uses for its source link. 13 - */ 14 - const TANGLED_ICON = 15 - '<svg viewBox="0 0 24.122343 23.274094" fill="currentColor" aria-hidden="true" focusable="false">' + 16 - '<path transform="translate(-0.4388285,-0.8629527)" d="m 16.348974,24.09935 -0.06485,-0.03766 -0.202005,-0.0106 -0.202008,-0.01048 -0.275736,-0.02601 -0.275734,-0.02602 v -0.02649 -0.02648 l -0.204577,-0.04019 -0.204578,-0.04019 -0.167616,-0.08035 -0.167617,-0.08035 -0.0014,-0.04137 -0.0014,-0.04137 -0.266473,-0.143735 -0.266475,-0.143735 -0.276098,-0.20335 -0.2761,-0.203347 -0.262064,-0.251949 -0.262064,-0.25195 -0.22095,-0.284628 -0.220948,-0.284629 -0.170253,-0.284631 -0.170252,-0.284628 -0.01341,-0.0144 -0.0134,-0.0144 -0.141982,0.161297 -0.14198,0.1613 -0.22313,0.21426 -0.223132,0.214264 -0.186025,0.146053 -0.186023,0.14605 -0.252501,0.163342 -0.252502,0.163342 -0.249014,0.115348 -0.249013,0.115336 0.0053,0.03241 0.0053,0.03241 -0.1716725,0.04599 -0.171669,0.046 -0.3379966,0.101058 -0.3379972,0.101058 -0.1778925,0.04506 -0.1778935,0.04508 -0.3913655,0.02601 -0.3913643,0.02603 -0.3557868,-0.03514 -0.3557863,-0.03514 -0.037426,-0.03029 -0.037427,-0.03029 -0.076924,0.02011 -0.076924,0.02011 -0.050508,-0.05051 -0.050405,-0.05056 L 6.6604532,23.110188 6.451745,23.063961 6.1546135,22.960559 5.8574835,22.857156 5.5319879,22.694039 5.2064938,22.530922 4.8793922,22.302961 4.5522905,22.075005 4.247598,21.786585 3.9429055,21.49817 3.7185335,21.208777 3.4941628,20.919385 3.3669822,20.705914 3.239803,20.492443 3.1335213,20.278969 3.0272397,20.065499 2.9015252,19.7275 2.7758105,19.389504 2.6925225,18.998139 2.6092345,18.606774 2.6096814,17.91299 2.6101284,17.219208 2.6744634,16.90029 2.7387984,16.581374 2.8474286,16.242088 2.9560588,15.9028 3.1137374,15.583492 3.2714148,15.264182 3.3415068,15.150766 3.4115988,15.03735 3.3127798,14.96945 3.2139618,14.90157 3.0360685,14.800239 2.8581753,14.698908 2.5913347,14.503228 2.3244955,14.307547 2.0621238,14.055599 1.7997507,13.803651 1.6111953,13.56878 1.4226411,13.333906 1.2632237,13.087474 1.1038089,12.841042 0.97442,12.575195 0.8450307,12.30935 0.724603,11.971351 0.6041766,11.633356 0.52150365,11.241991 0.4388285,10.850626 0.44091592,10.156842 0.44300333,9.4630594 0.54235911,9.0369608 0.6417149,8.6108622 0.7741173,8.2694368 0.9065196,7.9280115 1.0736303,7.6214262 1.2407515,7.3148397 1.45931,7.0191718 1.6778685,6.7235039 1.9300326,6.4611321 2.1821966,6.1987592 2.4134579,6.0137228 2.6447193,5.8286865 2.8759792,5.6776409 3.1072406,5.526594 3.4282004,5.3713977 3.7491603,5.2162016 3.9263009,5.1508695 4.1034416,5.0855373 4.2813348,4.7481598 4.4592292,4.4107823 4.6718,4.108422 4.8843733,3.8060618 5.198353,3.4805372 5.5123313,3.155014 5.7685095,2.9596425 6.0246877,2.7642722 6.329187,2.5851365 6.6336863,2.406002 6.9497657,2.2751596 7.2658453,2.1443184 7.4756394,2.0772947 7.6854348,2.01027 8.0825241,1.931086 8.4796139,1.851902 l 0.5870477,0.00291 0.5870469,0.00291 0.4447315,0.092455 0.444734,0.092455 0.302419,0.1105495 0.302417,0.1105495 0.329929,0.1646046 0.32993,0.1646033 0.239329,-0.2316919 0.239329,-0.2316919 0.160103,-0.1256767 0.160105,-0.1256767 0.160102,-0.1021909 0.160105,-0.1021899 0.142315,-0.082328 0.142314,-0.082328 0.231262,-0.1090091 0.231259,-0.1090091 0.26684,-0.098743 0.266839,-0.098743 0.320208,-0.073514 0.320209,-0.073527 0.355787,-0.041833 0.355785,-0.041834 0.426942,0.023827 0.426945,0.023828 0.355785,0.071179 0.355788,0.0711791 0.284627,0.09267 0.284629,0.09267 0.28514,0.1310267 0.28514,0.1310255 0.238179,0.1446969 0.238174,0.1446979 0.259413,0.1955332 0.259413,0.1955319 0.290757,0.296774 0.290758,0.2967753 0.151736,0.1941581 0.151734,0.1941594 0.135326,0.2149951 0.135327,0.2149952 0.154755,0.3202073 0.154758,0.3202085 0.09409,0.2677358 0.09409,0.267737 0.06948,0.3319087 0.06948,0.3319099 0.01111,0.00808 0.01111,0.00808 0.444734,0.2173653 0.444734,0.2173665 0.309499,0.2161102 0.309497,0.2161101 0.309694,0.2930023 0.309694,0.2930037 0.18752,0.2348726 0.187524,0.2348727 0.166516,0.2574092 0.166519,0.2574108 0.15273,0.3260252 0.152734,0.3260262 0.08972,0.2668403 0.08971,0.2668391 0.08295,0.3913655 0.08295,0.3913652 -6.21e-4,0.6582049 -6.21e-4,0.658204 -0.06362,0.315725 -0.06362,0.315725 -0.09046,0.289112 -0.09046,0.289112 -0.122759,0.281358 -0.12276,0.281356 -0.146626,0.252323 -0.146629,0.252322 -0.190443,0.258668 -0.190448,0.258671 -0.254911,0.268356 -0.254911,0.268355 -0.286872,0.223127 -0.286874,0.223127 -0.320203,0.187693 -0.320209,0.187693 -0.04347,0.03519 -0.04347,0.03521 0.0564,0.12989 0.0564,0.129892 0.08728,0.213472 0.08728,0.213471 0.189755,0.729363 0.189753,0.729362 0.0652,0.302417 0.0652,0.302419 -0.0018,0.675994 -0.0018,0.675995 -0.0801,0.373573 -0.08009,0.373577 -0.09,0.266839 -0.09,0.26684 -0.190389,0.391364 -0.19039,0.391366 -0.223169,0.320207 -0.223167,0.320209 -0.303585,0.315294 -0.303584,0.315291 -0.284631,0.220665 -0.284629,0.220663 -0.220128,0.132359 -0.220127,0.132358 -0.242395,0.106698 -0.242394,0.106699 -0.08895,0.04734 -0.08895,0.04733 -0.249052,0.07247 -0.24905,0.07247 -0.322042,0.0574 -0.322044,0.0574 -0.282794,-0.003 -0.282795,-0.003 -0.07115,-0.0031 -0.07115,-0.0031 -0.177894,-0.0033 -0.177893,-0.0033 -0.124528,0.02555 -0.124528,0.02555 z m -4.470079,-5.349839 0.214838,-0.01739 0.206601,-0.06782 0.206602,-0.06782 0.244389,-0.117874 0.244393,-0.11786 0.274473,-0.206822 0.27447,-0.20682 0.229308,-0.257201 0.229306,-0.2572 0.219161,-0.28463 0.219159,-0.284629 0.188541,-0.284628 0.188543,-0.28463 0.214594,-0.373574 0.214593,-0.373577 0.133861,-0.312006 0.133865,-0.312007 0.02861,-0.01769 0.02861,-0.01769 0.197275,0.26212 0.197278,0.262119 0.163613,0.150814 0.163614,0.150814 0.201914,0.09276 0.201914,0.09276 0.302417,0.01421 0.302418,0.01421 0.213472,-0.08025 0.213471,-0.08025 0.200606,-0.204641 0.200606,-0.204642 0.09242,-0.278887 0.09241,-0.278888 0.05765,-0.302418 0.05764,-0.302416 L 18.41327,13.768114 18.39502,13.34117 18.31849,12.915185 18.24196,12.4892 18.15595,12.168033 18.06994,11.846867 17.928869,11.444534 17.787801,11.042201 17.621278,10.73296 17.454757,10.423723 17.337388,10.263619 17.220021,10.103516 17.095645,9.9837986 16.971268,9.8640816 16.990048,9.6813736 17.008828,9.4986654 16.947568,9.249616 16.886308,9.0005655 16.752419,8.7159355 16.618521,8.4313217 16.435707,8.2294676 16.252892,8.0276114 16.079629,7.9004245 15.906366,7.773238 l -0.20429,0.1230127 -0.204289,0.1230121 -0.26702,0.059413 -0.267022,0.059413 -0.205761,-0.021508 -0.205766,-0.021508 -0.23495,-0.08844 -0.234953,-0.08844 -0.118429,-0.090334 -0.118428,-0.090333 h -0.03944 -0.03944 L 13.711268,7.8540732 13.655958,7.9706205 13.497227,8.1520709 13.338499,8.3335203 13.168394,8.4419112 12.998289,8.550301 12.777045,8.624223 12.5558,8.698155 H 12.275611 11.995429 L 11.799973,8.6309015 11.604513,8.5636472 11.491311,8.5051061 11.37811,8.446565 11.138172,8.2254579 10.898231,8.0043497 l -0.09565,-0.084618 -0.09565,-0.084613 -0.218822,0.198024 -0.218822,0.1980231 -0.165392,0.078387 -0.1653925,0.078387 -0.177894,0.047948 -0.177892,0.047948 L 9.3635263,8.4842631 9.144328,8.4846889 8.9195029,8.4147138 8.6946778,8.3447386 8.5931214,8.4414036 8.491565,8.5380686 8.3707618,8.7019598 8.2499597,8.8658478 8.0802403,8.9290726 7.9105231,8.9922974 7.7952769,9.0780061 7.6800299,9.1637148 7.5706169,9.2778257 7.4612038,9.3919481 7.1059768,9.9205267 6.7507497,10.449105 l -0.2159851,0.449834 -0.2159839,0.449834 -0.2216572,0.462522 -0.2216559,0.462523 -0.1459343,0.337996 -0.1459342,0.337998 -0.055483,0.220042 -0.055483,0.220041 -0.015885,0.206903 -0.015872,0.206901 0.034307,0.242939 0.034307,0.24294 0.096281,0.196632 0.096281,0.196634 0.143607,0.125222 0.1436071,0.125222 0.1873143,0.08737 0.1873141,0.08737 0.2752084,0.002 0.2752084,0.002 0.2312297,-0.09773 0.231231,-0.09772 0.1067615,-0.07603 0.1067614,-0.07603 0.3679062,-0.29377 0.3679065,-0.293771 0.026804,0.01656 0.026804,0.01656 0.023626,0.466819 0.023626,0.466815 0.088326,0.513195 0.088326,0.513193 0.08897,0.364413 0.08897,0.364411 0.1315362,0.302418 0.1315352,0.302418 0.1051964,0.160105 0.1051954,0.160103 0.1104741,0.11877 0.1104731,0.118769 0.2846284,0.205644 0.2846305,0.205642 0.144448,0.07312 0.144448,0.07312 0.214787,0.05566 0.214787,0.05566 0.245601,0.03075 0.245602,0.03075 0.204577,-0.0125 0.204578,-0.0125 z m 0.686342,-3.497495 -0.11281,-0.06077 -0.106155,-0.134033 -0.106155,-0.134031 -0.04406,-0.18371 -0.04406,-0.183707 0.02417,-0.553937 0.02417,-0.553936 0.03513,-0.426945 0.03513,-0.426942 0.07225,-0.373576 0.07225,-0.373575 0.05417,-0.211338 0.05417,-0.211339 0.0674,-0.132112 0.0674,-0.132112 0.132437,-0.10916 0.132437,-0.109161 0.187436,-0.04195 0.187438,-0.04195 0.170366,0.06469 0.170364,0.06469 0.114312,0.124073 0.114313,0.124086 0.04139,0.18495 0.04139,0.184951 -0.111218,0.459845 -0.111219,0.459844 -0.03383,0.26584 -0.03382,0.265841 -0.03986,0.818307 -0.03986,0.818309 -0.0378,0.15162 -0.03779,0.151621 -0.11089,0.110562 -0.110891,0.110561 -0.114489,0.04913 -0.114489,0.04913 -0.187932,-0.0016 -0.187929,-0.0016 z m -2.8087655,-0.358124 -0.146445,-0.06848 -0.088025,-0.119502 -0.088024,-0.119502 -0.038581,-0.106736 -0.038581,-0.106736 -0.02237,-0.134956 -0.02239,-0.134957 -0.031955,-0.46988 -0.031955,-0.469881 0.036203,-0.444733 0.036203,-0.444731 0.048862,-0.215257 0.048862,-0.215255 0.076082,-0.203349 0.076081,-0.203348 0.0936,-0.111244 0.0936,-0.111245 0.143787,-0.06531 0.1437865,-0.06532 h 0.142315 0.142314 l 0.142314,0.06588 0.142316,0.06588 0.093,0.102325 0.093,0.102325 0.04042,0.120942 0.04042,0.120942 v 0.152479 0.152477 l -0.03347,0.08804 -0.03347,0.08805 -0.05693,0.275653 -0.05693,0.275651 2.11e-4,0.430246 2.12e-4,0.430243 0.04294,0.392646 0.04295,0.392647 -0.09189,0.200702 -0.09189,0.200702 -0.148688,0.0984 -0.148687,0.0984 -0.20136,0.01212 -0.2013595,0.01212 z"></path>' + 17 - '</svg>'; 18 - 19 - /** 20 9 * Monospace status line — the quiet "developer tool" signature. Carries the 21 - * puzzle lineage plus links out to the source and the author's profile. The 22 - * architecture details (records, backlink reads, lexicon authority) now live on 23 - * the About page rather than crowding every footer. 10 + * puzzle lineage plus a link to the author's profile. The architecture details 11 + * (records, backlink reads, lexicon authority) now live on the About page 12 + * rather than crowding every footer. 24 13 */ 25 14 export function renderFooter(ctx: Ctx, puzzleNumber?: number): HTMLElement { 26 15 const lines: HTMLElement[] = []; ··· 36 25 { class: 'line links', style: puzzleNumber != null ? 'margin-top:6px' : undefined }, 37 26 el('a', { href: '/about', onClick: navTo(ctx, '/about') }, t(ctx.lang).about), 38 27 el('span', { class: 'spacer' }), 39 - el('a', { 40 - href: SOURCE_URL, 41 - target: '_blank', 42 - rel: 'noopener noreferrer', 43 - class: 'iconlink', 44 - title: t(ctx.lang).sourceCode, 45 - ariaLabel: t(ctx.lang).sourceCode, 46 - html: TANGLED_ICON, 47 - }), 48 28 el( 49 29 'a', 50 30 {
+7 -4
tests/footer.test.ts
··· 18 18 } 19 19 20 20 describe('renderFooter', () => { 21 - it('links to the source repo and the Bluesky profile', () => { 21 + it('links to the Bluesky profile', () => { 22 22 const footer = renderFooter(makeCtx()); 23 23 const hrefs = [...footer.querySelectorAll('a')].map((a) => a.getAttribute('href')); 24 - expect(hrefs).toContain('https://tangled.org/jeremy.herve.bzh/atmot'); 25 24 expect(hrefs).toContain('https://bsky.app/profile/jeremy.herve.bzh'); 26 25 }); 27 26 28 - it('renders the tangled mark inline and the avatar as an image', () => { 27 + it('does not link out to the source repository', () => { 28 + const footer = renderFooter(makeCtx()); 29 + expect(footer.querySelector('a[href*="tangled.org"]')).toBeNull(); 30 + }); 31 + 32 + it('renders the avatar as an image', () => { 29 33 const footer = renderFooter(makeCtx()); 30 - expect(footer.querySelector('a[href*="tangled.org"] svg')).not.toBeNull(); 31 34 const avatar = footer.querySelector('a[href*="bsky.app"] img'); 32 35 expect(avatar?.getAttribute('src')).toBe('/jeremy-herve.svg'); 33 36 });