@smartsoft001-mobilems/claude-plugins 2.87.0 → 2.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartsoft001-mobilems/claude-plugins",
3
- "version": "2.87.0",
3
+ "version": "2.88.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "mobilems",
3
3
  "description": "MobilEMS-specific Claude Code skills (MuzaCMS / MobileMS indexer API)",
4
- "version": "2.87.0"
4
+ "version": "2.88.0"
5
5
  }
@@ -75,6 +75,30 @@ and the reference repo path (same one init used).
75
75
  library exists, the `forFeature` carries the chosen `entity` (and template, if given), the route is
76
76
  registered, and the article card shared components are built and exported. Fix and re-run.
77
77
 
78
+ ## Known QA pitfalls (from live deployments — fix while copying, not after)
79
+
80
+ Each of these was a real Linear bug on a museum project built with this skill (Ostrowiec MOB-962,
81
+ Tomaszów MOB-957/993). The cross-cutting rules live in `mobilems-project-init`
82
+ `references/qa-hardening.md`; these are the article-module-specific ones:
83
+
84
+ - **Author filter dictionary key is lowercase `authors`.** `dictionaryKey="authors"` routes to
85
+ `api/article/authors/...`; `"Author"` silently falls through to `api/dictionary/Author/...` →
86
+ HTTP 409 and an empty options list (one-character bug that shipped to QA).
87
+ - **alt from CMS titles must strip HTML.** The detail header image shipped with
88
+ `alt="<p>Tytuł</p>"` — pipe the title through `smartRemoveHtml` before binding it to `alt`.
89
+ - **Detail page structure:** an `h1` (article title — copied reference had only `h2`), breadcrumbs
90
+ (`/ Artykuły / {tytuł}`), and gallery image captions rendered as text, NOT `h3` (an `h1 → h3`
91
+ jump is a recurring axe-core finding).
92
+ - **List item field order per mockup:** date ABOVE the title (implementations kept shipping it at
93
+ the bottom), keywords as inline links styled per the design, and mockup-exact sort labels
94
+ ("Sortowanie: Od najnowszych", not "Sortuj: Data publikacji - od najnowszych").
95
+ - **List spacing:** in list view the items need a real gap (`gap-4` fixed the "too close together"
96
+ QA bug).
97
+ - **Search debounce 250 ms** (`@debounce(250)`), total count + shared `NextPageComponent` on the
98
+ list — consistent with objects/collections.
99
+ - **Warning-clean templates:** remove needless `?.` on non-nullable signals (NG8107) and unused
100
+ `@let` declarations (NG8112) after copying.
101
+
78
102
  ## Notes
79
103
 
80
104
  - One run builds one named article module. Run the skill again — with a different name + template — for
@@ -45,6 +45,31 @@ run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`;
45
45
  (top-level + search child), the `collections-grid-item` component is built and exported, and the
46
46
  three public-collections deps are present. Fix and re-run.
47
47
 
48
+ ## Known QA pitfalls (from live deployments — fix while copying, not after)
49
+
50
+ Each of these was a real Linear bug on a museum project built with this skill (Ostrowiec MOB-961/994,
51
+ Tomaszów MOB-956). The cross-cutting rules live in `mobilems-project-init`
52
+ `references/qa-hardening.md`; these are the collections-specific ones:
53
+
54
+ - **One card component, reused everywhere.** The home page and the collection detail must render the
55
+ SAME shared `collections-grid-item` / object card components as the list views — a second ad-hoc
56
+ card on the home page was a High-priority bug (MOB-994). Never fork a card per page.
57
+ - **No hardcoded UI strings.** The detail heading was shipped as a literal `Zbiory {{ title }}` and
58
+ stayed Polish in EN — every static label goes through a translation key (PL + EN in
59
+ `translations.json`). The list page needs a VISIBLE translated heading (not only `sr-only`), and
60
+ exactly one `h1` per page.
61
+ - **Card image fallback:** when a collection thumbnail fails to load, show the app's `no-image`
62
+ placeholder instead of a broken image.
63
+ - **Hover:** underline the card **title** on hover (not the "Zobacz więcej" link) — same pattern as
64
+ objects.
65
+ - **Search debounce 250 ms** in the filters (`@debounce(250)`), total count + shared
66
+ `NextPageComponent` on the list — consistent with objects/news.
67
+ - **Keyword/cycle chips wrap on 360 px** — chips caused page-level horizontal scroll on mobile.
68
+ - **Heading font must cover Polish diacritics** — collection card titles rendered in a fallback
69
+ font because the configured serif (Bacasime Antique) lacks `ą ć ę ł ń ś ź ż`; verify the Tailwind
70
+ heading `fontFamily` is non-empty and latin-ext complete (see qa-hardening).
71
+ - **Warning-clean templates:** remove needless `?.` on non-nullable signals (NG8107) after copying.
72
+
48
73
  ## Notes
49
74
 
50
75
  - This skill builds only the collections module + its shared component + its route. It never adds
@@ -45,6 +45,34 @@ run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`;
45
45
  and exported, the widget map registers the object gallery widget, and `ISpecificObject` is present.
46
46
  Fix and re-run.
47
47
 
48
+ ## Known QA pitfalls (from live deployments — fix while copying, not after)
49
+
50
+ Each of these was a real Linear bug on a museum project built with this skill (Ostrowiec MOB-960/991,
51
+ Tomaszów MOB-949/993). The cross-cutting rules live in `mobilems-project-init`
52
+ `references/qa-hardening.md`; these are the objects-specific ones:
53
+
54
+ - **Search debounce.** The panel full-text search (`app-objects-searchbar variant="panel"`) must
55
+ re-emit through a handler with `@debounce(250)` (`lodash-decorators`) — without it every keystroke
56
+ hits the API. Collections and news filters already do this; keep the modules consistent.
57
+ - **List page:** show the total result count (`facade.totalCount()`) and add the shared
58
+ `NextPageComponent` at the bottom (same `selectedPage`/`lastPageIndex` pattern as the news list).
59
+ - **Map tab coordinates.** Read coordinates through ONE helper (`getObjectCoordinates()`) used by
60
+ both the tab-visibility predicate and the map render: `archeoMap.latitude/longitude` first, then
61
+ fall back to the first valid `createPlaces[]` entry; accept **both `lon` and `long`** (upstream
62
+ `MsObjectPlace` says `long`, the real API sends `lon`); validate with `Number.isFinite` (lat `0`
63
+ is valid; `Number('') === 0` — reject empty strings explicitly). Duplicating the predicate in two
64
+ components caused a "map never renders" bug.
65
+ - **Field labels match the client's field spec everywhere** — list card, detail and filters must use
66
+ the same names (QA rejected "Materiał" vs "Tworzywo", "Miejsca:" vs "Miejsce powstania").
67
+ - **Keywords section** maps the API `tags[]` field (not `keywords[]`) and renders clickable links
68
+ that filter the list.
69
+ - **Warning-clean templates:** after copying, remove needless `?.` on non-nullable signals (NG8107)
70
+ and drop unused entries from `imports: []` (NG8113 — the tab container often imports
71
+ Model3d/Map/VirtualWalk tabs its template no longer references). Masonry grid items must not set
72
+ `width` after change detection (NG0100).
73
+ - **A11y:** exactly one `h1` (detail = object title); cards exposed as `role="link"` must activate
74
+ on Enter/Space; focused/hovered cards change label colours together with the rest of the content.
75
+
48
76
  ## Notes
49
77
 
50
78
  - This skill builds only the objects module + its shared components + its route. It never adds other
@@ -100,16 +100,33 @@ the static pages (`home`, `page`, `contact`, `search`, `not-found`) + their reso
100
100
  catch-all). Each module skill adds its own feature route to the `appRoutes` array when it runs
101
101
  (`article-preview` is articles-coupled — `mobilems-articles` adds it, not the foundation).
102
102
 
103
- ### 9. .claude extension
103
+ ### 9. QA hardening — SEO, i18n, WCAG, branding
104
+
105
+ Apply `references/qa-hardening.md` — a checklist distilled from real QA bug reports on live museum
106
+ deployments. The copied foundation carries the _reference museum's_ identity, so this step removes
107
+ leftovers and closes the recurring gaps QA always finds:
108
+
109
+ - dynamic `/sitemap.xml` + `/robots.txt` SSR endpoints driven by `SITE_URL` (never static copies —
110
+ they shipped another museum's domain twice),
111
+ - og/twitter images + URLs on the target domain, favicon set, skip links rewritten for this app,
112
+ - meta description + OpenGraph on home in both languages; `og:locale` follows the active language,
113
+ - default language = Polish, `<html lang>` synced on switch, zero hardcoded UI strings,
114
+ - WCAG: one `h1`, no skipped heading levels, contrast ≥4.5:1 (reference greys fail), heading font
115
+ with full Polish diacritics,
116
+ - warning-clean build & console (NG8107/NG8112/NG8113/NG0100/NG0913, Swiper loop, duplicate API
117
+ calls).
118
+
119
+ ### 10. .claude extension
104
120
 
105
121
  Apply `references/claude-config.md`: add `flow@mobilems` to `enabledPlugins`, grow the
106
122
  MobilEMS-specific permissions / MCP allow-lists, and seed `tools/specs` + `tools/prompts` pointers.
107
123
 
108
- ### 10. Verify
124
+ ### 11. Verify
109
125
 
110
126
  Run `scripts/verify-init.sh <workspace-root> <PREFIX>`. It asserts the delta landed (Tailwind,
111
- multimediaUrl, MobilEMS providers, populated shared-lib barrel) AND the foundation is self-contained
112
- (no `libs/museum-*`, no feature routes, and the build-safety invariant — no file under
127
+ multimediaUrl, MobilEMS providers, populated shared-lib barrel), the QA-hardening basics (sitemap +
128
+ robots endpoints, favicon, non-empty heading font), AND the foundation is self-contained (no
129
+ `libs/museum-*`, no feature routes, and the build-safety invariant — no file under
113
130
  `libs/shared/angular/src` imports a feature package). Fix and re-run until it passes. Gate
114
131
  `npm install` behind an explicit ask — skip it during self-validation/eval.
115
132
 
@@ -10,7 +10,11 @@ Copy verbatim and thread `<PREFIX>`. The root component imports the `COMPONENTS`
10
10
  `SkipLinksComponent` from `@<PREFIX>/angular`, injects `ConfigsFacade` + `SeoService` (from
11
11
  `@smartsoft001-mobilems/angular`), `LocalStorageService` (shared lib) and `Router`; it inits the
12
12
  language, loads the global `Config` in `ngOnInit`, and focuses `#skip-links` on each `NavigationEnd`.
13
- `app.html`:
13
+
14
+ > **QA gotcha (skip links):** the copied skip-link labels/targets describe the _reference_ museum's
15
+ > sections — a project shipped skip links that still said "Światowid". Rewrite them for this app's
16
+ > actual sections and localize them (PL/EN keys). See `references/qa-hardening.md` §1.
17
+ > `app.html`:
14
18
 
15
19
  ```html
16
20
  <app-skip-links></app-skip-links>
@@ -29,7 +33,11 @@ Copy `pages/{home,page,contact,search,not-found}` + the `home` and `page` resolv
29
33
  `NewsCarouselComponent`, `PartnersCarouselComponent`) and the `IObject`/`IArticle`/`IPartner` types
30
34
  from the shared `models/` dir, plus the `DataService` home loaders — all foundation code (see
31
35
  `references/shared-lib.md`). The home page renders fully; its carousels populate from the API at
32
- runtime.
36
+ runtime. **QA gotchas:** home sections must reuse the SAME shared card components as the module
37
+ list pages (an ad-hoc home card was a High bug); Swiper `loop` only when there are enough slides
38
+ for `slidesPerView` (else a console warning on every load); don't fire the same home API call
39
+ multiple times on load (`object-of-the-week` went out 3× — share/replay it). See
40
+ `references/qa-hardening.md` §6–7.
33
41
  - `search.component` renders results through a `<router-outlet>`; it has no feature import.
34
42
 
35
43
  Run a feature-package grep over the pages you copy as a guard. **Do not copy `article-preview`** — the
@@ -0,0 +1,124 @@
1
+ # QA hardening — lessons from live museum deployments
2
+
3
+ Every item below is a **real defect reported in QA on production museum projects** (Linear: Muzeum
4
+ Ostrowiec MOB-938…MOB-995, Muzeum Tomaszów Lubelski MOB-948…MOB-996). The foundation is copied from
5
+ a reference repo, so the default failure mode is **reference-project leftovers** plus a handful of
6
+ recurring SEO/i18n/WCAG gaps. Work through this checklist when applying the delta — each item is
7
+ cheap to do during init and expensive to chase later in QA.
8
+
9
+ ## 1. No reference-project leftovers (branding)
10
+
11
+ The reference repo belongs to a _different museum_. Everything carrying that museum's identity must
12
+ be replaced or generated for the target project:
13
+
14
+ - **`sitemap.xml` + `robots.txt`** — never copy static files (QA found `zbiory.muzeum-orkana.pl`
15
+ URLs shipped to another museum). Implement them as **dynamic SSR endpoints** driven by the
16
+ `SITE_URL` env var — see section 2.
17
+ - **`og:image` / `og:image:secure_url` / `twitter:image` / `og:url`** — must point at the target
18
+ museum's domain, not the reference deploy (QA found `mobilems-muzeum-swiatowid-qa.smartsoft.biz.pl/gfx/social-logo.jpg`).
19
+ Use `SITE_URL`/`siteUrl` to build absolute URLs; ship a local `gfx/social-logo.jpg` for this app.
20
+ - **Favicon** — the base often has none. Ship the full set: `favicon.ico`, 32×32 + 192×192 PNG,
21
+ `apple-touch-icon` (two projects opened QA bugs "Brak ikony").
22
+ - **Skip links** — keep them (they are required), but rewrite their labels/targets for THIS app's
23
+ sections; copied skip links still said "Światowid". Localize them (PL/EN translation keys).
24
+ - **Footer / contact data** — target museum's phone, e-mail, socials; links come from the CMS API,
25
+ not hardcode.
26
+ - Local static assets (funding logos etc.) must be served near their rendered size — oversized
27
+ copies trigger Angular `NG0913` warnings and hurt LCP.
28
+
29
+ ## 2. Dynamic sitemap + robots endpoints (server.ts)
30
+
31
+ Pattern proven in MOB-948 — put the logic in **pure, testable functions** in
32
+ `apps/web/src/seo/seo-endpoints.ts` (`resolveSiteUrl`, `buildSitemapXml`, `buildRobotsTxt`), and
33
+ wire thin routes in `server.ts`:
34
+
35
+ - Register `GET /sitemap.xml` and `GET /robots.txt` **after** `/api/translations` but **before**
36
+ `express.static` and the `'/**'` SSR catch-all — otherwise SSR answers with HTML and status 200
37
+ and the endpoints "work" only apparently.
38
+ - Normalize `SITE_URL`: strip trailing `/`, prepend `https://` when the schema is missing; fall back
39
+ to the request host when the env var is unset.
40
+ - Headers: `application/xml; charset=utf-8` / `text/plain; charset=utf-8`,
41
+ `Cache-Control: public, max-age=3600`.
42
+ - Sitemap content: home + the module list pages (`/strona-glowna`, `/obiekty`, `/kolekcje`, …). Do
43
+ **not** include CMS static pages (`/strona/:id`) — their ids are CMS-assigned and go stale.
44
+ - `robots.txt` points `Sitemap:` at the same `SITE_URL`.
45
+
46
+ ## 3. SEO meta (home + every routed page)
47
+
48
+ - `<meta name="description">` + complete OpenGraph (`og:title`, `og:description`, `og:image`,
49
+ `og:url`) + Twitter Card on the home page, in **both** languages, re-set on language switch.
50
+ - `og:locale` must follow the active language — a hardcoded `pl_PL` was a QA bug. In
51
+ `museum-seo.service.ts` use a locale map (`pl → pl_PL`, `en → en_GB`) read from
52
+ `translateService.currentLang` (fallback via `defaultLang` — `currentLang` can be `undefined`
53
+ during SSR), and emit one `og:locale:alternate`.
54
+
55
+ ## 4. i18n invariants
56
+
57
+ - **Default language is Polish.** A Polish museum site rendering English on first visit while
58
+ `<html lang="pl">` is a WCAG 3.1.1 violation and was a reported bug.
59
+ - `<html lang>` must track the actually rendered language after every switch; `<title>`, meta
60
+ description and `og:title` retranslate with it.
61
+ - **No hardcoded UI strings in templates.** QA repeatedly caught literals like `Zbiory {{ title }}`
62
+ untranslated in EN. Every static label goes through a translation key with both PL and EN values
63
+ in `translations.json`.
64
+ - Dictionary keys for filter dictionaries are **lowercase** conventions of the framework:
65
+ `dictionaryKey="authors"` routes to `api/article/authors/...`; `"Author"` silently falls through
66
+ to `api/dictionary/Author/...` → HTTP 409 and an empty options list.
67
+
68
+ ## 5. WCAG invariants
69
+
70
+ - **Contrast:** the reference grey `#9ca3af` on white is 2.53:1 — fails AA and was flagged on menus
71
+ and breadcrumbs in two projects. Use ≥4.5:1 for text: `#6f6f6f` ≈ 5.0:1, `#4b5563` ≈ 7.6:1.
72
+ Figma mockups themselves sometimes specify failing colours — match the design _intent_ with the
73
+ nearest compliant shade instead of copying the hex blindly, and note the deviation.
74
+ - **Headings:** exactly one `h1` per page; no skipped levels (an `h1 → h3` jump was a recurring
75
+ axe-core finding). Detail pages get an `h1` (the item title), image captions are NOT headings.
76
+ - **alt text:** when the alt comes from a CMS title, strip HTML first (`smartRemoveHtml`) —
77
+ `alt="<p>Tytuł</p>"` shipped to QA.
78
+ - **Keyboard:** any card implemented as `role="link"`/`tabindex=0` must activate on Enter/Space
79
+ (or better, be a real `<a href>`). Focus must be visible; labels should react to focus/hover the
80
+ same way the rest of the card content does.
81
+ - **Fonts must cover Polish diacritics.** Bacasime Antique, Prata and Italiana lack 16/18 of
82
+ `ą ć ę ł ń ś ź ż` (+ uppercase) — titles rendered in a fallback font and QA rejected it. Verified
83
+ full-latin-ext replacements: **Gilda Display** (closest to Bacasime) or **Playfair Display**.
84
+ Never leave a `tailwind.config.js` `fontFamily` entry as an empty array — it silently falls back
85
+ to system sans.
86
+
87
+ ## 6. Console hygiene (build + runtime must be warning-clean)
88
+
89
+ QA files bugs for compiler warnings, so treat them as errors after copying code:
90
+
91
+ - `NG8107` — remove `?.` on receivers whose type excludes null/undefined (typical after copying
92
+ templates: `list()?.length`, `filter()?.searchText`).
93
+ - `NG8112` — remove unused `@let` declarations.
94
+ - `NG8113` — remove components from `imports: []` that the template doesn't use (copied tab
95
+ containers often import Model3d/Map/VirtualWalk tabs conditionally rendered away).
96
+ - `NG0100` (`ExpressionChangedAfterItHasBeenCheckedError`) — masonry/grid items must not set their
97
+ `width` binding after change detection completes.
98
+ - **Never log API payloads to the console** — QA flagged leaked responses.
99
+ - Swiper: enable `loop` only when there are enough slides for the configured
100
+ `slidesPerView`/`slidesPerGroup`, otherwise Swiper logs a loop warning on every load.
101
+ - Avoid duplicate API calls on page load (home fired `object-of-the-week` 3×) — share/replay the
102
+ request in the service.
103
+
104
+ ## 7. Cross-module UI consistency
105
+
106
+ These conventions were enforced by QA across modules — bake them in from the start:
107
+
108
+ - **One card component per entity, reused everywhere.** The home page, module lists and search
109
+ results must render the same shared `*-grid-item` / `*-list-item` components — a second ad-hoc
110
+ card implementation on the home page was a High-priority bug (MOB-994).
111
+ - **Full-text search inputs debounce 250 ms** (`@debounce(250)` from `lodash-decorators`) — in every
112
+ module's filters (objects, collections, articles/news).
113
+ - **Every list page shows the total result count** (`facade.totalCount()`), independent of
114
+ pagination (MOB-995/996).
115
+ - **"Next page" button** at the bottom of each list follows one shared pattern
116
+ (`NextPageComponent`: `selectedPage`/`lastPageIndex` from `totalCount()` + limit, scroll-to-top on
117
+ advance, hidden on the last page).
118
+ - **Breadcrumbs:** everything except the active (last) item greyed — separators `#8e8e8e`,
119
+ intermediate links a WCAG-compliant grey (`#4b5563`/`#6f6f6f`), active item dark/semibold.
120
+ - **Card hover:** underline the card **title** only (not the "Zobacz więcej" link) — pattern
121
+ aligned across objects/collections.
122
+ - **Image fallback:** cards show the app's `no-image` placeholder when a thumbnail fails to load.
123
+ - Keyword/tag chips must wrap on 360 px wide screens — no page-level horizontal scroll at
124
+ 360/768/1024/1440.
@@ -87,3 +87,11 @@ app.get('/api/translations', (req, res) => {
87
87
  The `app.config.ts` `provideAppInitializer` hits `/api/translations` in dev and `apiUrl + 'setting/theme'`
88
88
  in prod (see `references/config-providers.md`). Optionally seed an empty
89
89
  `apps/web/public/translations.json` (`{"en":{},"pl":{}}`) so dev SSR doesn't 404.
90
+
91
+ ## 4. `/sitemap.xml` + `/robots.txt` — dynamic endpoints (required)
92
+
93
+ Never ship static `sitemap.xml`/`robots.txt` copied from the reference — two projects went to QA
94
+ with another museum's domain in them. Build them as SSR endpoints from `SITE_URL`, with the logic in
95
+ pure functions (`apps/web/src/seo/seo-endpoints.ts`) and thin routes in `server.ts`, registered
96
+ after `/api/translations` and **before** `express.static` + the `'/**'` catch-all. Full pattern
97
+ (normalization, headers, content rules): `references/qa-hardening.md` §2.
@@ -62,6 +62,12 @@ module.exports = {
62
62
  > The `content` globs already cover `libs/**`, so feature libs added later are picked up — no edit
63
63
  > needed when modules arrive. `darkMode` keys off `contrast-high` for the WCAG high-contrast toggle.
64
64
 
65
+ > **QA gotcha (fonts):** when the project's design swaps in its own heading font, verify it covers
66
+ > Polish diacritics (`ą ć ę ł ń ś ź ż` + uppercase). Bacasime Antique, Prata and Italiana lack them
67
+ > — titles silently render in a fallback font and QA rejects it (verified replacements: Gilda
68
+ > Display, Playfair Display). Never leave a `fontFamily` entry as an empty array — it falls back to
69
+ > system sans. See `references/qa-hardening.md` §5.
70
+
65
71
  ## 2. postcss
66
72
 
67
73
  Add Tailwind + autoprefixer to postcss. If the base has a `postcss.config.js`/`.json`, add the
@@ -24,6 +24,14 @@ grep -q "SharedModule.forRoot" apps/web/src/app/app.config.ts && pass "SharedMod
24
24
  grep -q "SEARCH_CONFIG_TOKEN" apps/web/src/app/app.config.ts && pass "SEARCH_CONFIG_TOKEN" || bad "SEARCH_CONFIG_TOKEN in app.config.ts"
25
25
  grep -q "multimediaUrl" apps/web/src/server.ts && pass "multimediaUrl in server.ts" || bad "multimediaUrl in server.ts"
26
26
  grep -q "/api/translations" apps/web/src/server.ts && pass "/api/translations endpoint" || bad "/api/translations in server.ts"
27
+
28
+ echo "== QA hardening (references/qa-hardening.md) =="
29
+ # dynamic sitemap/robots endpoints (static copies shipped another museum's domain in past projects)
30
+ grep -q "sitemap.xml" apps/web/src/server.ts && pass "dynamic /sitemap.xml endpoint" || bad "/sitemap.xml endpoint in server.ts (SITE_URL-driven, never a static copy)"
31
+ grep -q "robots.txt" apps/web/src/server.ts && pass "dynamic /robots.txt endpoint" || bad "/robots.txt endpoint in server.ts (SITE_URL-driven, never a static copy)"
32
+ if ls apps/web/public/favicon.ico apps/web/src/favicon.ico >/dev/null 2>&1; then pass "favicon present"; else bad "favicon.ico missing (QA bug 'Brak ikony' in two projects)"; fi
33
+ # an empty fontFamily array silently falls back to system sans (QA-rejected typography)
34
+ if grep -qE ":\s*\[\s*\]" tailwind.config.js 2>/dev/null; then bad "empty fontFamily array in tailwind.config.js (heading font falls back to system sans)"; else pass "no empty fontFamily entries"; fi
27
35
  grep -q "./lib/components" libs/shared/angular/src/index.ts && pass "shared-lib components barrel populated" || bad "components barrel in shared-lib index.ts"
28
36
  [ -f .claude/settings.json ] && grep -q "flow@mobilems" .claude/settings.json && pass "flow@mobilems enabled" || bad "flow@mobilems in .claude/settings.json"
29
37