Task #303: DIN Next LT Arabic site default + working font picker

User report: "the dropdown for changing the site font does not work."
Root cause: FontSettingsSection listed Cairo / Tajawal / Noto Naskh
Arabic / Amiri, but only Tajawal had been registered with @font-face.
Picking the others was a no-op. Site default body font was also not
DIN Next LT Arabic as designed.

Changes:
- artifacts/tx-os/src/index.css: --app-font-sans now starts with
  "DIN Next LT Arabic"; dropped IBM Plex Mono.
- artifacts/tx-os/index.html: removed Google Fonts <link> + preconnect.
- artifacts/tx-os/src/pages/executive-meetings.tsx: FontSettingsSection
  dropdown replaced with the 5 actually-bundled families + system.
- artifacts/api-server/src/routes/executive-meetings.ts: FONT_FAMILIES
  Zod allowlist updated to match.
- artifacts/api-server/src/lib/sanitize.ts: FONT_NAME_PART regex now
  allows the new families (kept IBM Plex Sans Arabic for backward
  compat). Fixes a latent bug from #301 where the rich-text sanitizer
  silently stripped attendee font picks on save.
- artifacts/api-server/src/lib/pdf-renderer.ts: FAMILY_MAP and
  ARABIC_FONT_NAMES updated. Sans-style families map to the bundled
  NotoSansArabic; Naskh-style families to NotoNaskhArabic.
- artifacts/api-server/tests/executive-meetings.test.mjs: PDF
  sans-vs-naskh assertion updated from Cairo/Noto Naskh Arabic to
  DIN Next LT Arabic/Majalla, preserving its semantics. Other
  Cairo/Noto Naskh Arabic occurrences swapped to the new names.
- replit.md: documented site default font + lockstep allowlist rule.

Deviations from plan: kept DEFAULT_FONT.fontFamily = "system" on the
frontend (and the backend Zod default) instead of changing it to
"DIN Next LT Arabic". "system" semantically means "no override → use
the CSS default", which is now DIN Next LT Arabic — same end result
without forcing a migration on existing rows. The sanitizer change
was not in the original plan but was required to make the picker
actually persist.

Verification:
- Frontend tsc clean.
- Backend tsc shows the same pre-existing unrelated errors as before
  (isHighlighted boolean/number; Drizzle scope typing).
- E2E browser test verified all 7 assertions: site default font,
  no Google Fonts requests, exactly 6 dropdown options, font picker
  applies + reverts correctly, attendee font picks persist after save.
- Code review verdict: PASS.
This commit is contained in:
riyadhafraa
2026-05-01 19:55:39 +00:00
parent ef65af248f
commit 471c480824
11 changed files with 95 additions and 33 deletions
@@ -662,7 +662,7 @@ test("Font settings: valid combo 200; invalid weight/size/family 400", async ()
"/api/executive-meetings/font-settings",
{
scope: "user",
fontFamily: "Cairo",
fontFamily: "DIN Next LT Arabic",
fontSize: 16,
fontWeight: "bold",
alignment: "center",
@@ -805,16 +805,16 @@ test("PDF GET /executive-meetings/pdf returns a real PDF and archives it", async
// Font-family must affect which font is embedded in the PDF. We render
// the same day twice with different families and assert that:
// 1) Both downloads succeed (200 + %PDF magic).
// 2) The Sans family ("Cairo") embeds NotoSansArabic, while the Naskh
// default ("Noto Naskh Arabic") embeds NotoNaskhArabic. The font's
// PostScript name appears verbatim in the PDF font dictionary.
// 2) The Sans family ("DIN Next LT Arabic") embeds NotoSansArabic,
// while the Naskh-style family ("Majalla") embeds NotoNaskhArabic.
// The PostScript name appears verbatim in the PDF font dictionary.
const setSans = await api(
adminCookie,
"PUT",
"/api/executive-meetings/font-settings",
{
scope: "user",
fontFamily: "Cairo",
fontFamily: "DIN Next LT Arabic",
fontSize: 16,
fontWeight: "bold",
alignment: "center",
@@ -832,11 +832,11 @@ test("PDF GET /executive-meetings/pdf returns a real PDF and archives it", async
const sansAscii = sansBuf.toString("latin1");
assert.ok(
/NotoSansArabic/i.test(sansAscii),
"Cairo family must embed NotoSansArabic in the PDF",
"DIN Next LT Arabic family must embed NotoSansArabic in the PDF",
);
assert.ok(
!/NotoNaskhArabic/i.test(sansAscii),
"Cairo family must NOT embed NotoNaskhArabic",
"DIN Next LT Arabic family must NOT embed NotoNaskhArabic",
);
const setNaskh = await api(
@@ -845,7 +845,7 @@ test("PDF GET /executive-meetings/pdf returns a real PDF and archives it", async
"/api/executive-meetings/font-settings",
{
scope: "user",
fontFamily: "Noto Naskh Arabic",
fontFamily: "Majalla",
fontSize: 14,
fontWeight: "regular",
alignment: "start",
@@ -862,11 +862,11 @@ test("PDF GET /executive-meetings/pdf returns a real PDF and archives it", async
const naskhAscii = naskhBuf.toString("latin1");
assert.ok(
/NotoNaskhArabic/i.test(naskhAscii),
"Noto Naskh Arabic family must embed NotoNaskhArabic in the PDF",
"Majalla family must embed NotoNaskhArabic in the PDF",
);
assert.ok(
!/NotoSansArabic/i.test(naskhAscii),
"Noto Naskh Arabic family must NOT embed NotoSansArabic",
"Majalla family must NOT embed NotoSansArabic",
);
});
@@ -1227,7 +1227,7 @@ test("Font settings: PUT then GET returns the user-scoped row roundtrip", async
const patch = await api(adminCookie, "PATCH",
"/api/executive-meetings/font-settings", {
scope: "user",
fontFamily: "Cairo",
fontFamily: "DIN Next LT Arabic",
fontSize: 14,
fontWeight: "regular",
alignment: "start",
@@ -1237,7 +1237,7 @@ test("Font settings: PUT then GET returns the user-scoped row roundtrip", async
const get2 = await api(adminCookie, "GET",
"/api/executive-meetings/font-settings");
const body2 = await get2.json();
assert.equal(body2.user.fontFamily, "Cairo");
assert.equal(body2.user.fontFamily, "DIN Next LT Arabic");
assert.equal(body2.user.fontSize, 14);
assert.equal(body2.user.fontWeight, "regular");
assert.equal(body2.user.alignment, "start");