From 41af1801688f638be214cc5922e91ede5b342e91 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Wed, 20 May 2026 08:36:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(theme):=20Plan=202.5=20P2=20=E2=80=94=20In?= =?UTF-8?q?ter=20typography=20(AD-2.5-T1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per RFC-WS-PRIMEVUE-PLAN-2-5 ยง4 AD-2.5-T1. Establishes Inter as the canonical Crewli body font via @fontsource/inter (local package, no Google Fonts CDN). Audit findings (pre-change): - No @fontsource/public-sans package was installed. - No tag in index.html loaded Public Sans. - Only one Public Sans reference existed in source: the vendored Vuexy SCSS variable $font-family-custom at src/@core/scss/template/libs/vuetify/_variables.scss, which drives Vuetify's $body-font-family on legacy surfaces during F4. - No src/main.css exists; the Tailwind v4 entry lives at src/assets/styles/tailwind.css with no @theme block yet. Changes: - @fontsource/inter@^5.2.8 added to dependencies; weights 400/500/600/700 imported at main.ts ahead of tailwind.css. - src/assets/styles/tailwind.css: new @theme block declaring --font-sans Inter-first, plus :root --crewli-font-family and html/body font-family applying that variable cascade-wide. - src/@core/scss/template/libs/vuetify/_variables.scss: $font-family-custom switched from the historical body font to Inter (vendored edit, narrowly scoped, F6 removes @core/ entirely). - tests/unit/styles/typography.spec.ts: 3-spec regression lock (Tailwind direct stacks, Vuexy SCSS variable, zero historical references in either file). File-content inspection โ€” jsdom does not cascade from imported stylesheets, so getComputedStyle would always pass. Suite delta: 570 โ†’ 573 (+3; the prompt's template was +2 but the audit revealed two distinct font-config files, so each gets its own assertion per the prompt's "cover all sites" rule). vue-tsc clean. Scoped ESLint clean. Co-Authored-By: Claude Opus 4.7 --- apps/app/package.json | 1 + apps/app/pnpm-lock.yaml | 16 +++- .../template/libs/vuetify/_variables.scss | 8 +- apps/app/src/assets/styles/tailwind.css | 32 +++++++ apps/app/src/main.ts | 10 ++ apps/app/tests/unit/styles/typography.spec.ts | 93 +++++++++++++++++++ 6 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 apps/app/tests/unit/styles/typography.spec.ts diff --git a/apps/app/package.json b/apps/app/package.json index c381d3a9..d9f7d75b 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -26,6 +26,7 @@ "@casl/ability": "6.7.3", "@casl/vue": "2.2.2", "@floating-ui/dom": "1.6.8", + "@fontsource/inter": "^5.2.8", "@primeuix/themes": "^2.0.3", "@primevue/forms": "^4.5.5", "@sentry/vue": "10.52.0", diff --git a/apps/app/pnpm-lock.yaml b/apps/app/pnpm-lock.yaml index 08f10fd3..b51d5f15 100644 --- a/apps/app/pnpm-lock.yaml +++ b/apps/app/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@floating-ui/dom': specifier: 1.6.8 version: 1.6.8 + '@fontsource/inter': + specifier: ^5.2.8 + version: 5.2.8 '@primeuix/themes': specifier: ^2.0.3 version: 2.0.3 @@ -978,6 +981,9 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@fontsource/inter@5.2.8': + resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==} + '@fullcalendar/core@6.1.19': resolution: {integrity: sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ==} @@ -6303,8 +6309,8 @@ packages: vue-component-type-helpers@3.2.7: resolution: {integrity: sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==} - vue-component-type-helpers@3.2.9: - resolution: {integrity: sha512-S3BiWYaLSzHxTpln665ELSrMR9UYmrIDUmhik7nVZxmJjTKL2/a+ew1hvGxksKelivm0ujjWfG1fYOiU/2e8rA==} + vue-component-type-helpers@3.3.1: + resolution: {integrity: sha512-pu58kqxmVyEH6VfNYW1UyEfR3XAnJ27ZXT3yzXxxpjLxVzAbyC35Zk/nm/RMs7ijWnJNSd9fWkeex2OhUsx3MA==} vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} @@ -7152,6 +7158,8 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@fontsource/inter@5.2.8': {} + '@fullcalendar/core@6.1.19': dependencies: preact: 10.12.1 @@ -7897,7 +7905,7 @@ snapshots: storybook: 10.4.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@testing-library/dom@9.3.4)(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) type-fest: 2.19.0 vue: 3.5.22(typescript@5.9.3) - vue-component-type-helpers: 3.2.9 + vue-component-type-helpers: 3.3.1 '@stylistic/eslint-plugin-js@0.0.4': dependencies: @@ -13092,7 +13100,7 @@ snapshots: vue-component-type-helpers@3.2.7: {} - vue-component-type-helpers@3.2.9: {} + vue-component-type-helpers@3.3.1: {} vue-demi@0.14.10(vue@3.5.22(typescript@5.9.3)): dependencies: diff --git a/apps/app/src/@core/scss/template/libs/vuetify/_variables.scss b/apps/app/src/@core/scss/template/libs/vuetify/_variables.scss index e4c0bc7b..9a944213 100644 --- a/apps/app/src/@core/scss/template/libs/vuetify/_variables.scss +++ b/apps/app/src/@core/scss/template/libs/vuetify/_variables.scss @@ -1,6 +1,12 @@ @use "sass:math"; -$font-family-custom: "Public Sans",sans-serif,-apple-system,blinkmacsystemfont, +// AD-2.5-T1 (RFC-WS-PRIMEVUE-PLAN-2-5): Inter is the canonical Crewli +// body font. The declaration is made in this vendored Vuexy file because +// $font-family-custom drives Vuetify's $body-font-family on legacy +// surfaces during the F4 migration window. Inter is loaded via +// @fontsource/inter in main.ts; the same family is mirrored in the +// Tailwind v4 @theme --font-sans token in src/assets/styles/tailwind.css. +$font-family-custom: "Inter",sans-serif,-apple-system,blinkmacsystemfont, "Segoe UI",roboto,"Helvetica Neue",arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; /* ๐Ÿ‘‰ Typography custom variables */ diff --git a/apps/app/src/assets/styles/tailwind.css b/apps/app/src/assets/styles/tailwind.css index a258726e..4d27f2f5 100644 --- a/apps/app/src/assets/styles/tailwind.css +++ b/apps/app/src/assets/styles/tailwind.css @@ -12,3 +12,35 @@ @import "tailwindcss"; @plugin "tailwindcss-primeui"; @source "../../**/*.{vue,ts,js,tsx,jsx}"; + +/* AD-2.5-T1 โ€” typography (RFC-WS-PRIMEVUE-PLAN-2-5). + * + * Inter is loaded via @fontsource/inter in main.ts (weights 400/500/600/700, + * local package, no Google Fonts CDN). The stack falls back to the + * platform UI sans-family if Inter fails to load. + * + * Two declarations are required: + * 1. The Tailwind v4 `@theme` token `--font-sans` so utility classes + * (e.g. `font-sans`) resolve to Inter. + * 2. The cascade-level `html, body { font-family }` so PrimeVue and + * Vuetify components โ€” which inherit from `body` โ€” render in Inter + * too. `--crewli-font-family` is the named CSS variable carrying + * the stack so other consumers can read it without re-declaring. + */ + +@theme { + --font-sans: + "Inter", system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; +} + +:root { + --crewli-font-family: + "Inter", system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, sans-serif; +} + +html, +body { + font-family: var(--crewli-font-family); +} diff --git a/apps/app/src/main.ts b/apps/app/src/main.ts index 85ae2154..2d2f26ba 100644 --- a/apps/app/src/main.ts +++ b/apps/app/src/main.ts @@ -15,6 +15,16 @@ import { registerPlugins } from '@core/utils/plugins' import { installPrimeVue } from '@/plugins/primevue' // Styles + +// AD-2.5-T1: Inter typography (RFC-WS-PRIMEVUE-PLAN-2-5). +// 400 = body, 500 = medium/labels, 600 = semibold/buttons, 700 = display. +// Loaded before tailwind.css so the @theme --font-sans token resolves +// to a face that's already in the document. +import '@fontsource/inter/400.css' +import '@fontsource/inter/500.css' +import '@fontsource/inter/600.css' +import '@fontsource/inter/700.css' + import '@styles/tailwind.css' import '@core/scss/template/index.scss' import '@styles/styles.scss' diff --git a/apps/app/tests/unit/styles/typography.spec.ts b/apps/app/tests/unit/styles/typography.spec.ts new file mode 100644 index 00000000..b8f0ac54 --- /dev/null +++ b/apps/app/tests/unit/styles/typography.spec.ts @@ -0,0 +1,93 @@ +/** + * AD-2.5-T1 โ€” typography regression lock (RFC-WS-PRIMEVUE-PLAN-2-5). + * + * Reads the font-config sources directly (file-content inspection) rather + * than relying on getComputedStyle in jsdom โ€” jsdom does not cascade + * from imported stylesheets, so a runtime check would always pass. + * + * Two files are inspected: + * 1. apps/app/src/assets/styles/tailwind.css โ€” Tailwind v4 @theme + * --font-sans token + the cascade-level html/body font-family + + * --crewli-font-family CSS variable. + * 2. apps/app/src/@core/scss/template/libs/vuetify/_variables.scss โ€” + * the vendored Vuexy SCSS variable $font-family-custom that feeds + * Vuetify's $body-font-family on legacy surfaces. + * + * If either file is moved, update the path constants below. + */ + +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +const TAILWIND_CSS_PATH = resolve( + __dirname, + '../../../src/assets/styles/tailwind.css', +) + +const VUEXY_VARIABLES_PATH = resolve( + __dirname, + '../../../src/@core/scss/template/libs/vuetify/_variables.scss', +) + +const tailwindCss = readFileSync(TAILWIND_CSS_PATH, 'utf-8') +const vuexyVariables = readFileSync(VUEXY_VARIABLES_PATH, 'utf-8') + +/** + * Pulls every `font-family: โ€ฆ;` or `--font: โ€ฆ;` declaration + * from a CSS source. Skips `var(...)` indirections โ€” those are pointers + * to other declarations that are themselves asserted directly. + */ +function fontDeclarations(css: string): string[] { + // `[^;]+` already accepts leading whitespace; explicit `\s*` between + // the colon and the value would overlap with it and trip + // regexp/no-super-linear-backtracking. We `.trim()` later. + const matches = css.match(/(?:font-family|--[\w-]*font[\w-]*):([^;]+);/gi) + + return (matches ?? []).filter(decl => !/\bvar\(/.test(decl)) +} + +function firstFamily(decl: string): string { + return decl + .split(':')[1] + .split(',')[0] + .trim() + .replace(/['"]/g, '') +} + +describe('Typography regression lock (AD-2.5-T1)', () => { + it('tailwind.css declares Inter as the first font in every direct stack', () => { + const decls = fontDeclarations(tailwindCss) + + expect(decls.length).toBeGreaterThan(0) + + for (const decl of decls) + expect(firstFamily(decl).toLowerCase()).toBe('inter') + }) + + it('Vuexy $font-family-custom declares Inter as the first family', () => { + // SCSS variable shape โ€” `$font-family-custom: "Inter", โ€ฆ;` (spans + // two lines in the source; the regex eats the value up to the `;`). + const match = vuexyVariables.match( + + // Same backtracking-avoidance as fontDeclarations above: drop \s*. + /\$font-family-custom:([^;]+);/i, + ) + + expect(match).not.toBeNull() + + const family = match![1] + .split(',')[0] + .trim() + .replace(/['"]/g, '') + + expect(family.toLowerCase()).toBe('inter') + }) + + it('no Public Sans reference survives in either font-config file', () => { + expect(tailwindCss).not.toMatch(/Public Sans/i) + expect(tailwindCss).not.toMatch(/public-sans/i) + expect(vuexyVariables).not.toMatch(/Public Sans/i) + expect(vuexyVariables).not.toMatch(/public-sans/i) + }) +})