@smartsoft001-mobilems/claude-plugins 2.88.0 → 2.90.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.
Files changed (44) hide show
  1. package/README.md +27 -2
  2. package/package.json +1 -1
  3. package/plugins/mobilems/.claude-plugin/README.md +21 -2
  4. package/plugins/mobilems/.claude-plugin/plugin.json +1 -1
  5. package/plugins/mobilems/skills/mobilems-articles/SKILL.md +52 -20
  6. package/plugins/mobilems/skills/mobilems-articles/references/module-and-routes.md +24 -0
  7. package/plugins/mobilems/skills/mobilems-articles/scripts/check-translations.js +90 -0
  8. package/plugins/mobilems/skills/mobilems-articles/scripts/verify-module.sh +53 -0
  9. package/plugins/mobilems/skills/mobilems-devops-config/SKILL.md +141 -0
  10. package/plugins/mobilems/skills/mobilems-devops-config/references/docker.md +58 -0
  11. package/plugins/mobilems/skills/mobilems-devops-config/references/env-and-lint.md +59 -0
  12. package/plugins/mobilems/skills/mobilems-devops-config/references/github-hygiene.md +54 -0
  13. package/plugins/mobilems/skills/mobilems-devops-config/references/tooling.md +105 -0
  14. package/plugins/mobilems/skills/mobilems-devops-config/references/workflows.md +410 -0
  15. package/plugins/mobilems/skills/mobilems-devops-config/scripts/detect-foundation.sh +47 -0
  16. package/plugins/mobilems/skills/mobilems-devops-config/scripts/verify-devops.sh +96 -0
  17. package/plugins/mobilems/skills/mobilems-events/SKILL.md +27 -1
  18. package/plugins/mobilems/skills/mobilems-events/references/module-and-routes.md +24 -0
  19. package/plugins/mobilems/skills/mobilems-events/scripts/verify-module.sh +43 -0
  20. package/plugins/mobilems/skills/mobilems-object-collections/SKILL.md +46 -14
  21. package/plugins/mobilems/skills/mobilems-object-collections/references/lib-contents.md +11 -0
  22. package/plugins/mobilems/skills/mobilems-object-collections/references/module-and-routes.md +26 -0
  23. package/plugins/mobilems/skills/mobilems-object-collections/scripts/check-translations.js +90 -0
  24. package/plugins/mobilems/skills/mobilems-object-collections/scripts/verify-module.sh +54 -0
  25. package/plugins/mobilems/skills/mobilems-objects/SKILL.md +42 -10
  26. package/plugins/mobilems/skills/mobilems-objects/references/module-and-routes.md +24 -0
  27. package/plugins/mobilems/skills/mobilems-objects/scripts/check-translations.js +90 -0
  28. package/plugins/mobilems/skills/mobilems-objects/scripts/verify-module.sh +53 -0
  29. package/plugins/mobilems/skills/mobilems-partners/SKILL.md +30 -2
  30. package/plugins/mobilems/skills/mobilems-partners/references/module-and-routes.md +24 -0
  31. package/plugins/mobilems/skills/mobilems-partners/scripts/check-translations.js +90 -0
  32. package/plugins/mobilems/skills/mobilems-partners/scripts/verify-module.sh +53 -0
  33. package/plugins/mobilems/skills/mobilems-project-init/SKILL.md +68 -8
  34. package/plugins/mobilems/skills/mobilems-project-init/references/branding-reset.md +112 -0
  35. package/plugins/mobilems/skills/mobilems-project-init/references/claude-config.md +7 -0
  36. package/plugins/mobilems/skills/mobilems-project-init/references/config-providers.md +1 -1
  37. package/plugins/mobilems/skills/mobilems-project-init/references/qa-hardening.md +30 -30
  38. package/plugins/mobilems/skills/mobilems-project-init/references/shared-lib.md +42 -5
  39. package/plugins/mobilems/skills/mobilems-project-init/references/tailwind-styles.md +33 -23
  40. package/plugins/mobilems/skills/mobilems-project-init/scripts/verify-init.sh +62 -9
  41. package/plugins/mobilems/skills/mobilems-thematic-publications/SKILL.md +25 -1
  42. package/plugins/mobilems/skills/mobilems-thematic-publications/references/lib-contents.md +11 -0
  43. package/plugins/mobilems/skills/mobilems-thematic-publications/references/module-and-routes.md +26 -0
  44. package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/verify-module.sh +43 -0
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # verify-devops.sh <workspace-root> <NODE_VERSION>
4
+ #
5
+ # Post-condition check for mobilems-devops-config. Asserts the ops/deployment layer landed and that
6
+ # the node version is consistent across .nvmrc, the Dockerfile, and the workflows.
7
+ # Portable to macOS system bash (3.2). Exit 0 = all pass; exit 1 = one or more checks failed.
8
+ set -eu
9
+
10
+ ROOT="${1:?usage: verify-devops.sh <workspace-root> <NODE_VERSION> [SITE_HOST]}"
11
+ NODE_VERSION="${2:?usage: verify-devops.sh <workspace-root> <NODE_VERSION> [SITE_HOST]}"
12
+ # Optional: this project's expected site host, so a carried-over SITE_URL is caught. No project
13
+ # host is hardcoded in this script.
14
+ SITE_HOST="${3:-}"
15
+ cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
16
+
17
+ fail=0
18
+ pass() { echo " PASS: $1"; }
19
+ bad() { echo " FAIL: $1" >&2; fail=1; }
20
+ exists() { [ -e "$1" ] && pass "$1" || bad "$1 missing"; }
21
+
22
+ echo "== Docker =="
23
+ exists apps/web/Dockerfile
24
+ [ -f apps/web/Dockerfile ] && grep -q "server/server.mjs" apps/web/Dockerfile && pass "Dockerfile starts SSR server" || bad "Dockerfile CMD node server/server.mjs"
25
+
26
+ echo "== GitHub Actions =="
27
+ exists .github/actions/cached-deps/action.yml
28
+ exists .github/workflows/development.yml
29
+ exists .github/workflows/e2e.yml
30
+ exists .github/workflows/push-to-client.yml
31
+ exists .github/workflows/ai-plan.yml
32
+ exists .github/workflows/ai-impl.yml
33
+ exists .github/workflows/ci.yml.disabled
34
+ # secrets are referenced, not hardcoded
35
+ [ -f .github/workflows/development.yml ] && grep -q "secrets.GITLAB_TOKEN" .github/workflows/development.yml && pass "development.yml uses secrets" || bad "development.yml secrets.GITLAB_TOKEN"
36
+
37
+ echo "== .github hygiene (references/github-hygiene.md) =="
38
+ # agent config belongs in .claude/, not .github/ (Nx's Copilot-format scaffolding leftovers)
39
+ STRAY=""
40
+ for d in skills agents prompts instructions chatmodes copilot-instructions.md; do
41
+ [ -e ".github/$d" ] && STRAY="$STRAY .github/$d"
42
+ done
43
+ if [ -n "$STRAY" ]; then
44
+ bad "agent config left in .github (delete plugin duplicates, git mv bespoke ones to .claude/):$STRAY"
45
+ else
46
+ pass ".github holds only workflows/actions/GitHub metadata"
47
+ fi
48
+
49
+ echo "== Update tooling =="
50
+ exists tools/scripts/update.js
51
+ grep -q '"update"' package.json && pass "npm update script" || bad 'package.json "update" script'
52
+ # husky install via postinstall OR prepare
53
+ if grep -qE '"(postinstall|prepare)"[[:space:]]*:[[:space:]]*"husky install"' package.json; then
54
+ pass "husky install hook"
55
+ else
56
+ bad 'package.json postinstall/prepare = "husky install"'
57
+ fi
58
+
59
+ echo "== Env / lint / node =="
60
+ exists .env.dev
61
+ [ -f .env.dev ] && grep -q "MULTIMEDIA_URL" .env.dev && pass ".env.dev has MULTIMEDIA_URL" || bad ".env.dev MULTIMEDIA_URL"
62
+ # SITE_URL drives og:url / sitemap.xml / robots.txt, so a value carried over from another project
63
+ # ships that project's domain in this app's SEO. Pass this project's expected host as $3 to check it.
64
+ if [ -n "${SITE_HOST:-}" ] && [ -f .env.dev ]; then
65
+ if grep -qE "^SITE_URL=https?://$SITE_HOST" .env.dev; then
66
+ pass "SITE_URL points at $SITE_HOST"
67
+ else
68
+ bad "SITE_URL does not point at the expected host ($SITE_HOST)"
69
+ fi
70
+ fi
71
+ # unreplaced template placeholders are worse than wrong values: the build silently ships '{{...}}'
72
+ if [ -f .env.dev ] && grep -q "{{" .env.dev; then
73
+ bad ".env.dev still contains {{...}} placeholders"
74
+ else
75
+ pass ".env.dev placeholders replaced"
76
+ fi
77
+ exists .eslintignore
78
+ exists .nvmrc
79
+
80
+ echo "== Node version consistency (expected: $NODE_VERSION) =="
81
+ NVMRC="$(tr -d '[:space:]' < .nvmrc 2>/dev/null || true)"
82
+ [ "$NVMRC" = "$NODE_VERSION" ] && pass ".nvmrc = $NODE_VERSION" || bad ".nvmrc is '$NVMRC', expected '$NODE_VERSION'"
83
+ [ -f apps/web/Dockerfile ] && grep -q "node:$NODE_VERSION-alpine" apps/web/Dockerfile && pass "Dockerfile base image node:$NODE_VERSION" || bad "Dockerfile base image not node:$NODE_VERSION-alpine"
84
+ # every workflow that pins a node-version should pin THIS version
85
+ DRIFT=""
86
+ for wf in .github/workflows/development.yml .github/workflows/e2e.yml; do
87
+ [ -f "$wf" ] || continue
88
+ if grep -qE "node-version:" "$wf" && ! grep -qE "node-version:[[:space:]]*'?${NODE_VERSION}'?" "$wf"; then
89
+ DRIFT="$DRIFT $wf"
90
+ fi
91
+ done
92
+ if [ -n "$DRIFT" ]; then bad "workflow node-version drift:$DRIFT"; else pass "workflow node-version matches $NODE_VERSION"; fi
93
+
94
+ echo
95
+ if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
96
+ exit "$fail"
@@ -23,7 +23,33 @@ its own `shell/angular`.
23
23
  Run `scripts/detect-init.sh <workspace-root>`. It confirms the MobilEMS foundation is present
24
24
  (`SharedModule.forRoot` in `app.config.ts`, a populated shared-lib barrel). If not, tell the user to
25
25
  run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`; the reference
26
- MobilEMS app uses `@msr`) and the reference repo path (same one init used).
26
+ MobilEMS app defines its own) and the reference repo path (same one init used).
27
+
28
+ ## Framework first — MANDATORY
29
+
30
+ Read these before writing ANY component, service, pipe, directive or helper, and reuse what they
31
+ list — re-implementing framework code is a defect, not a shortcut:
32
+
33
+ 0. [**The Framework API index**](https://github.com/smartsoft001/mobilems-framework/blob/main/README.md#-framework-api) — the map of every package to
34
+ its catalog. Start here when you do not already know which package owns what.
35
+ 1. The framework API catalog — `packages/shared/angular/README.md` in this repo, installed as
36
+ `node_modules/@smartsoft001-mobilems/angular/README.md` in a museum app ([browse](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md)).
37
+ This is the path `scripts/verify-module.sh` names when it fails.
38
+ 2. `libs/shared/angular/README.md` — the app's own shared lib (built by `mobilems-project-init`)
39
+
40
+ Events is a standalone booking module (no feature package of its own), so it leans on the framework
41
+ hardest:
42
+
43
+ | Do not hand-roll | Use |
44
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | a masonry grid (`new Masonry(...)`, per-page masonry JS/CSS) | `<smart-mobilems-masonry-grid>` (`MasonryGridComponent`) with a `#masonryContentTpl`; the `Masonry` provider on the list route only feeds it |
46
+ | filter/search state and `query` building | `FiltersContext` via `FiltersBaseComponent` (or `QueryFilterService` for plain query strings) |
47
+ | confirm/cancel dialogs | `ModalService.open()` + `ModalRef` (and the shared `confirmation-modal` service) |
48
+ | media URLs, meta/OG tags, dictionaries, WCAG, contrast | `FileUrlService`, `SeoService`/`MetaService`, `DictionaryService`, `WcagService`, `ContrastService` |
49
+ | a calendar/date-picker helper duplicated per page | the shared lib's `calendar` component — built once, reused |
50
+
51
+ Anything the framework genuinely lacks goes into `libs/shared/angular` **once** (and into its
52
+ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on hand-rolled masonry.
27
53
 
28
54
  ## Steps
29
55
 
@@ -89,6 +89,30 @@ export const eventsRoutes: Route[] = [
89
89
  ];
90
90
  ```
91
91
 
92
+ > **The `Masonry` provider exists only to feed the framework's `MasonryGridComponent`.** It is DI
93
+ > plumbing, not a grid — never call `new Masonry(...)`, keep a `MasonryInstance`, or re-implement the
94
+ > layout/`reloadItems` dance in a page component. A hand-rolled CSS-grid or column layout is the same
95
+ > defect. `scripts/verify-module.sh` fails the run on both.
96
+
97
+ The list page renders the grid like this — copy this shape, it is the required one:
98
+
99
+ ```html
100
+ <smart-mobilems-masonry-grid [list]="list()" [options]="{ gutter: 24 }">
101
+ <ng-template #masonryContentTpl>
102
+ <div class="masonry-grid-sizer"></div>
103
+ @for (item of list(); track item.id) {
104
+ <div class="masonry-item"><app-events-grid-item [item]="item" /></div>
105
+ }
106
+ </ng-template>
107
+ </smart-mobilems-masonry-grid>
108
+ ```
109
+
110
+ `.masonry-grid-sizer` and `.masonry-item` are required — they are the component's default
111
+ `columnWidth` / `itemSelector`. The card (lib-contents.md) is the only module-specific part. Inputs,
112
+ public methods and the SSR provider are documented in the framework catalog,
113
+ [`packages/shared/angular/README.md`](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md#masonrygridcomponent)
114
+ (installed as `node_modules/@smartsoft001-mobilems/angular/README.md`).
115
+
92
116
  > Route order matters: the specific reservation sub-routes (`:id/potwierdzenie`, `:id/anulowanie`,
93
117
  > `:id/booking`) come before the generic `:id/:slug` and `:id` detail routes, and the bare list route
94
118
  > `''` comes last. Keep this order.
@@ -54,6 +54,49 @@ echo "== Foundation deps =="
54
54
  grep -q "@smartsoft001-mobilems/angular" package.json && pass "@smartsoft001-mobilems/angular present" || bad "@smartsoft001-mobilems/angular missing"
55
55
  grep -q "@smartsoft001-mobilems/models" package.json && pass "@smartsoft001-mobilems/models present" || bad "@smartsoft001-mobilems/models missing"
56
56
 
57
+ echo "== Framework first (packages/shared/angular/README.md) =="
58
+ LIBDIR="libs/museum-events/shell/angular/src/lib"
59
+ # 1. masonry belongs to the framework's MasonryGridComponent — no module-level instances
60
+ MAS="$(grep -rlnE "new Masonry\(|MasonryInstance" libs apps/web/src 2>/dev/null || true)"
61
+ if [ -n "$MAS" ]; then
62
+ bad "hand-rolled masonry — render grids with <smart-mobilems-masonry-grid> (MasonryGridComponent):"
63
+ printf ' %s\n' $MAS >&2
64
+ else
65
+ pass "no hand-rolled masonry instances"
66
+ fi
67
+ # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
68
+ # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
69
+ # which the `new Masonry(` grep above cannot see.
70
+ if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
71
+ if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
72
+ pass "grids render via <smart-mobilems-masonry-grid>"
73
+ else
74
+ bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
75
+ fi
76
+ fi
77
+ # 3. a list page must render through the framework grid
78
+ LISTP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'list*' 2>/dev/null || true)"
79
+ if [ -n "$LISTP" ]; then
80
+ if grep -rq "smart-mobilems-masonry-grid" $LISTP 2>/dev/null; then
81
+ pass "list page renders via <smart-mobilems-masonry-grid>"
82
+ else
83
+ bad "list page does not render <smart-mobilems-masonry-grid> (README.md#masonrygridcomponent)"
84
+ fi
85
+ fi
86
+ # 4. filters must EXTEND the framework base class — `context.` alone proves nothing, and a
87
+ # differently-named dir must not silently skip the check
88
+ FILT="$(find "$LIBDIR/components" -maxdepth 1 -type d \( -name 'filters*' -o -name 'search*' -o -name 'facet*' \) 2>/dev/null || true)"
89
+ if [ -n "$FILT" ]; then
90
+ if grep -rqE "FiltersBaseComponent|QueryFilterService" $FILT 2>/dev/null; then
91
+ pass "filters use the framework filter API"
92
+ else
93
+ bad "filters use neither FiltersBaseComponent nor QueryFilterService — no ad-hoc query building:"
94
+ printf ' %s\n' $FILT >&2
95
+ fi
96
+ else
97
+ pass "no filters component in this module"
98
+ fi
99
+
57
100
  echo
58
101
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
59
102
  exit "$fail"
@@ -19,7 +19,30 @@ Collections is a list/detail view backed by `PublicCollectionsSharedModule.forFe
19
19
  Run `scripts/detect-init.sh <workspace-root>`. It confirms the MobilEMS foundation is present
20
20
  (`SharedModule.forRoot` in `app.config.ts`, a populated shared-lib barrel). If not, tell the user to
21
21
  run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`; reference uses
22
- `@msr`) and the reference repo path (same one init used).
22
+ own) and the reference repo path (same one init used).
23
+
24
+ ## Framework first — MANDATORY
25
+
26
+ Read these before writing ANY component, service, pipe, directive or helper, and reuse what they
27
+ list — re-implementing framework code is a defect, not a shortcut:
28
+
29
+ 0. [**The Framework API index**](https://github.com/smartsoft001/mobilems-framework/blob/main/README.md#-framework-api) — the map of every package to
30
+ its catalog. Start here when you do not already know which package owns what.
31
+ 1. The framework API catalog — `packages/shared/angular/README.md` in this repo, installed as
32
+ `node_modules/@smartsoft001-mobilems/angular/README.md` in a museum app ([browse](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md)).
33
+ This is the path `scripts/verify-module.sh` names when it fails.
34
+ 2. [`packages/public-collections/README.md`](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/public-collections/README.md) — this module's package catalog
35
+ 3. `libs/shared/angular/README.md` — the app's own shared lib (built by `mobilems-project-init`)
36
+
37
+ | Do not hand-roll | Use |
38
+ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
39
+ | a masonry grid (`new Masonry(...)`, per-page masonry JS/CSS) | `<smart-mobilems-masonry-grid>` (`MasonryGridComponent`) with a `#masonryContentTpl`; the `Masonry` provider only feeds it |
40
+ | filter state / hand-built `query` arrays | `FiltersBaseComponent<PublicCollection>` + its `FiltersContext` accessors (`searchText`, `keywords`, `cycles`, `page`, `sort`, …) |
41
+ | list/detail plumbing (facade wiring, id-from-route) | the `PublicCollectionsListComponent` / `PublicCollectionsItemComponent` base classes |
42
+ | media URLs, meta/OG tags, filter dictionaries, modals, WCAG | `FileUrlService`, `SeoService`/`MetaService`, `DictionaryService`, `ModalService`, `WcagService` |
43
+
44
+ Anything the framework genuinely lacks goes into `libs/shared/angular` **once** (and into its
45
+ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on hand-rolled masonry.
23
46
 
24
47
  ## Steps
25
48
 
@@ -27,10 +50,16 @@ run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`;
27
50
  `@smartsoft001-mobilems/public-collections-shell-angular`,
28
51
  `@smartsoft001-mobilems/public-collections-shell-dtos` (match the installed mobilems line).
29
52
  Resolve versions at run time; do not `npm install` unless asked. No extra third-party viewer deps.
30
- 2. **Library** — create `libs/museum-collections/{domain,shell/angular}` per `references/lib-contents.md`:
31
- copy the domain entity (`MuseumCollection extends PublicCollection`) and the shell
32
- (`MuseumCollectionsModule`, routes, `components/filters`, `pages/{list,item}`, `services`), thread
33
- `<PREFIX>`. Add the two `tsconfig.base.json` paths and the `museum-collections` commit scope.
53
+ 2. **Library** — scaffold `libs/museum-collections` with this package's Nx generator (deterministic — do
54
+ NOT copy files by hand when the generator is available):
55
+ ```bash
56
+ npx nx g @smartsoft001-mobilems/public-collections-shell-angular:museum-module \
57
+ --referenceRoot=<reference-repo> --prefix=<PREFIX>
58
+ ```
59
+ It copies the lib from the reference, threads `@<PREFIX>/`, adds both `tsconfig.base.json`
60
+ paths, normalizes `project.json` paths and copies the module's translation groups (pl+en).
61
+ Fallback (older framework line without the generator): do the same by hand per
62
+ `references/lib-contents.md`. The `museum-collections` commit scope registers automatically from the `libs/` dir name.
34
63
  3. **Module + routes** — see `references/module-and-routes.md`: `MuseumCollectionsModule` wraps
35
64
  `PublicCollectionsSharedModule.forFeature({ routing: false, config: { type: MuseumCollection, apiUrl, entity: 'publicCollections' } })`
36
65
  with the `Masonry` SSR factory and the `CrudService → CollectionsService` provider. Add the lazy
@@ -40,20 +69,23 @@ run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`;
40
69
  `components/collections-grid-item` (it imports `public-collections-domain`) in the shared lib and
41
70
  export it from `components/index.ts`. Collections contributes no widget-map entry and no model type
42
71
  (those are objects-only).
43
- 5. **Verify** — run `scripts/verify-module.sh <workspace-root> <PREFIX>`: asserts the lib +
72
+ 5. **Translations** — copy the module's translation groups (`COLLECTIONS`) from the reference
73
+ repo's `apps/web/public/translations.json` into the app's file, for BOTH `pl` and `en`.
74
+ Complete group = every reference key present; every key referenced in the module's code
75
+ must exist in both languages — a raw `GROUP.key` rendered in the UI is a defect.
76
+ 6. **Verify** — run `REF_ROOT=<reference-repo> scripts/verify-module.sh <workspace-root> <PREFIX>`: asserts the lib +
44
77
  `MuseumCollection` entity exist, the `forFeature` is wired, the `kolekcje` route is registered
45
78
  (top-level + search child), the `collections-grid-item` component is built and exported, and the
46
79
  three public-collections deps are present. Fix and re-run.
47
80
 
48
- ## Known QA pitfalls (from live deployments — fix while copying, not after)
81
+ ## Known pitfalls (fix while copying, not after)
49
82
 
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`
83
+ The cross-cutting rules live in `mobilems-project-init`
52
84
  `references/qa-hardening.md`; these are the collections-specific ones:
53
85
 
54
86
  - **One card component, reused everywhere.** The home page and the collection detail must render the
55
87
  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.
88
+ card on the home page is a defect. Never fork a card per page.
57
89
  - **No hardcoded UI strings.** The detail heading was shipped as a literal `Zbiory {{ title }}` and
58
90
  stayed Polish in EN — every static label goes through a translation key (PL + EN in
59
91
  `translations.json`). The list page needs a VISIBLE translated heading (not only `sr-only`), and
@@ -64,10 +96,10 @@ Tomaszów MOB-956). The cross-cutting rules live in `mobilems-project-init`
64
96
  objects.
65
97
  - **Search debounce 250 ms** in the filters (`@debounce(250)`), total count + shared
66
98
  `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).
99
+ - **Keyword/cycle chips must wrap at 360 px** — unwrapped chips cause page-level horizontal scroll.
100
+ - **Heading font must cover Polish diacritics** — a serif lacking `ą ć ę ł ń ś ź ż` renders card
101
+ titles in a silent fallback; verify the Tailwind heading `fontFamily` is non-empty and latin-ext
102
+ complete (see qa-hardening).
71
103
  - **Warning-clean templates:** remove needless `?.` on non-nullable signals (NG8107) after copying.
72
104
 
73
105
  ## Notes
@@ -74,3 +74,14 @@ export class MuseumCollection extends PublicCollection {
74
74
  The `components/filters` component renders the keyword/cycle facets exposed by the entity's
75
75
  `connections` block; the list/detail rendering itself comes from `PublicCollectionsSharedModule`.
76
76
  No third-party viewer deps are needed (unlike the objects 3D tab).
77
+
78
+ It **must** extend the framework base class rather than build `query` arrays by hand — that base is
79
+ what wires the debounced writes back through `CrudFacade`:
80
+
81
+ ```ts
82
+ export class FiltersComponent extends FiltersBaseComponent<PublicCollection> {}
83
+ ```
84
+
85
+ Read filter state through the inherited `context` accessors (`searchText`, `keywords`, `cycles`,
86
+ `page`, `sort`), never by assembling a `query` yourself. `scripts/verify-module.sh` fails the run if
87
+ this component does not extend `FiltersBaseComponent`.
@@ -68,6 +68,32 @@ import { CollectionsService } from './services';
68
68
  export class MuseumCollectionsModule {}
69
69
  ```
70
70
 
71
+ > **The `Masonry` provider exists only to feed the framework's `MasonryGridComponent`.** It is DI
72
+ > plumbing, not a grid — never call `new Masonry(...)`, keep a `MasonryInstance`, or re-implement the
73
+ > layout/`reloadItems` dance in a page component. A hand-rolled CSS-grid or column layout is the same
74
+ > defect. `scripts/verify-module.sh` fails the run on both.
75
+
76
+ The list page renders the grid like this — copy this shape, it is the required one:
77
+
78
+ ```html
79
+ <smart-mobilems-masonry-grid [list]="list()" [options]="{ gutter: 24 }">
80
+ <ng-template #masonryContentTpl>
81
+ <div class="masonry-grid-sizer"></div>
82
+ @for (item of list(); track item.id) {
83
+ <div class="masonry-item">
84
+ <smart-collections-grid-item [item]="item" />
85
+ </div>
86
+ }
87
+ </ng-template>
88
+ </smart-mobilems-masonry-grid>
89
+ ```
90
+
91
+ `.masonry-grid-sizer` and `.masonry-item` are required — they are the component's default
92
+ `columnWidth` / `itemSelector`. The card (shared-components.md) is the only module-specific part. Inputs,
93
+ public methods and the SSR provider are documented in the framework catalog,
94
+ [`packages/shared/angular/README.md`](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md#masonrygridcomponent)
95
+ (installed as `node_modules/@smartsoft001-mobilems/angular/README.md`).
96
+
71
97
  ## `collections.routes.ts`
72
98
 
73
99
  Identical shape to the objects routes (`:id/:slug` → Item, `:id` → Item, `''` → List).
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-translations.js "<GROUP GROUP …>" [reference-root]
4
+ *
5
+ * Run from the workspace root (verify-module.sh cd's there). Verifies the
6
+ * module's translation groups in apps/web/public/translations.json:
7
+ * 1. each group exists and is non-empty in BOTH pl and en,
8
+ * 2. every literal GROUP.* key referenced in libs/ or apps/web/src exists
9
+ * in both languages (dead references = defect),
10
+ * 3. with a reference root: every reference key of the group exists in the
11
+ * generated file — missing keys are listed (the module is incomplete).
12
+ * Prints PASS/FAIL lines in the verify-module.sh idiom; exit 1 on any FAIL.
13
+ */
14
+ const fs = require("fs");
15
+
16
+ const [groupsArg, refRoot] = process.argv.slice(2);
17
+ const groups = (groupsArg || "").split(/\s+/).filter(Boolean);
18
+ if (!groups.length) { console.log(" FAIL: no translation groups given"); process.exit(1); }
19
+
20
+ const TRANS = "apps/web/public/translations.json";
21
+ let fail = 0;
22
+ const pass = (m) => console.log(` PASS: ${m}`);
23
+ const bad = (m) => { console.log(` FAIL: ${m}`); fail = 1; };
24
+
25
+ const flat = (obj, prefix) => Object.entries(obj || {}).flatMap(([k, v]) =>
26
+ v && typeof v === "object" ? flat(v, `${prefix}.${k}`) : [`${prefix}.${k}`]);
27
+
28
+ const readTrans = (p) => { try { return JSON.parse(fs.readFileSync(p, "utf8")).data.translations; } catch { return null; } };
29
+
30
+ const gen = readTrans(TRANS);
31
+ if (!gen) { bad(`cannot read ${TRANS}`); process.exit(1); }
32
+
33
+ // 1. groups present + non-empty in both languages
34
+ const genKeys = { pl: new Set(), en: new Set() };
35
+ for (const lang of ["pl", "en"]) {
36
+ for (const g of groups) {
37
+ const keys = flat(gen[lang]?.[g], g);
38
+ keys.forEach((k) => genKeys[lang].add(k));
39
+ if (keys.length) pass(`${g} present in ${lang} (${keys.length} keys)`);
40
+ else bad(`${g} missing or empty in ${lang}`);
41
+ }
42
+ }
43
+
44
+ // 2. dead references — literal GROUP.* keys used in sources must exist.
45
+ // Dynamic tails (`GROUP.${x}`) don't match the literal regex, and a matched
46
+ // PREFIX that is a parent of existing keys counts as present.
47
+ const walk = (dir) => {
48
+ let out = [];
49
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
50
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
51
+ const p = `${dir}/${e.name}`;
52
+ if (e.isDirectory()) out = out.concat(walk(p));
53
+ else if (/\.(ts|html)$/.test(e.name)) out.push(p);
54
+ }
55
+ return out;
56
+ };
57
+ let src = "";
58
+ for (const d of ["libs", "apps/web/src"]) {
59
+ if (fs.existsSync(d)) src += walk(d).map((f) => { try { return fs.readFileSync(f, "utf8"); } catch { return ""; } }).join("\n");
60
+ }
61
+ const re = new RegExp(`\\b(?:${groups.join("|")})(?:\\.[A-Za-z0-9_]+)+`, "g");
62
+ const used = new Set(src.match(re) || []);
63
+ const dead = { pl: [], en: [] };
64
+ for (const key of used) {
65
+ for (const lang of ["pl", "en"]) {
66
+ const ks = genKeys[lang];
67
+ if (!ks.has(key) && ![...ks].some((k) => k.startsWith(`${key}.`))) dead[lang].push(key);
68
+ }
69
+ }
70
+ for (const lang of ["pl", "en"]) {
71
+ if (dead[lang].length) bad(`${lang}: keys referenced in code but missing in ${TRANS}: ${dead[lang].slice(0, 8).join(", ")}${dead[lang].length > 8 ? ` (+${dead[lang].length - 8})` : ""}`);
72
+ else pass(`no dead ${lang} references (${used.size} literal keys checked)`);
73
+ }
74
+
75
+ // 3. completeness vs reference
76
+ if (refRoot) {
77
+ const ref = readTrans(`${refRoot}/apps/web/public/translations.json`);
78
+ if (!ref) bad(`cannot read reference translations under ${refRoot}`);
79
+ else {
80
+ for (const lang of ["pl", "en"]) {
81
+ const missing = groups.flatMap((g) => flat(ref[lang]?.[g], g)).filter((k) => !genKeys[lang].has(k));
82
+ if (missing.length) bad(`${lang}: ${missing.length} reference keys missing: ${missing.slice(0, 8).join(", ")}${missing.length > 8 ? " …" : ""}`);
83
+ else pass(`${lang}: all reference keys covered`);
84
+ }
85
+ }
86
+ } else {
87
+ console.log(" SKIP: no reference root given — completeness vs reference not checked");
88
+ }
89
+
90
+ process.exit(fail);
@@ -9,6 +9,7 @@ set -eu
9
9
 
10
10
  ROOT="${1:?usage: verify-module.sh <workspace-root> <PREFIX>}"
11
11
  PREFIX="${2:?usage: verify-module.sh <workspace-root> <PREFIX>}"
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
12
13
  cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
13
14
 
14
15
  fail=0
@@ -39,6 +40,59 @@ grep -q "@smartsoft001-mobilems/public-collections-domain" package.json && pass
39
40
  grep -q "@smartsoft001-mobilems/public-collections-shell-angular" package.json && pass "public-collections-shell-angular dep" || bad "public-collections-shell-angular dep missing"
40
41
  grep -q "@smartsoft001-mobilems/public-collections-shell-dtos" package.json && pass "public-collections-shell-dtos dep" || bad "public-collections-shell-dtos dep missing"
41
42
 
43
+ echo "== Framework first (packages/shared/angular/README.md) =="
44
+ LIBDIR="libs/museum-collections/shell/angular/src/lib"
45
+ # 1. masonry belongs to the framework's MasonryGridComponent — no module-level instances
46
+ MAS="$(grep -rlnE "new Masonry\(|MasonryInstance" libs apps/web/src 2>/dev/null || true)"
47
+ if [ -n "$MAS" ]; then
48
+ bad "hand-rolled masonry — render grids with <smart-mobilems-masonry-grid> (MasonryGridComponent):"
49
+ printf ' %s\n' $MAS >&2
50
+ else
51
+ pass "no hand-rolled masonry instances"
52
+ fi
53
+ # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
54
+ # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
55
+ # which the `new Masonry(` grep above cannot see.
56
+ if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
57
+ if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
58
+ pass "grids render via <smart-mobilems-masonry-grid>"
59
+ else
60
+ bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
61
+ fi
62
+ fi
63
+ # 3. the ITEM page renders its objects grid through the framework grid — in the
64
+ # reference the collections LIST is a plain grid; masonry lives on the item page
65
+ ITEMP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'item*' 2>/dev/null || true)"
66
+ if [ -n "$ITEMP" ]; then
67
+ if grep -rq "smart-mobilems-masonry-grid" $ITEMP 2>/dev/null; then
68
+ pass "item page renders via <smart-mobilems-masonry-grid>"
69
+ else
70
+ bad "item page does not render <smart-mobilems-masonry-grid> (README.md#masonrygridcomponent)"
71
+ fi
72
+ fi
73
+ # 4. filters must EXTEND the framework base class — `context.` alone proves nothing, and a
74
+ # differently-named dir must not silently skip the check
75
+ FILT="$(find "$LIBDIR/components" -maxdepth 1 -type d \( -name 'filters*' -o -name 'search*' -o -name 'facet*' \) 2>/dev/null || true)"
76
+ if [ -n "$FILT" ]; then
77
+ if grep -rqE "FiltersBaseComponent" $FILT 2>/dev/null; then
78
+ pass "filters use the framework filter API"
79
+ else
80
+ bad "filters do not extend FiltersBaseComponent — no ad-hoc query building:"
81
+ printf ' %s\n' $FILT >&2
82
+ fi
83
+ else
84
+ pass "no filters component in this module"
85
+ fi
86
+
87
+ echo "== Translations (REF_ROOT=${REF_ROOT:-unset}) =="
88
+ # Groups: present in pl+en, no dead references, complete vs the reference repo
89
+ # (export REF_ROOT=<reference-repo-root> to enable the completeness diff).
90
+ if node "$SCRIPT_DIR/check-translations.js" "COLLECTIONS" "${REF_ROOT:-}"; then
91
+ pass "translation groups complete"
92
+ else
93
+ bad "translation gaps — fix the FAIL lines above"
94
+ fi
95
+
42
96
  echo
43
97
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
44
98
  exit "$fail"
@@ -20,7 +20,30 @@ map, audio, 3D model, animation, virtual walk) backed by `ObjectsSharedModule.fo
20
20
  Run `scripts/detect-init.sh <workspace-root>`. It confirms the MobilEMS foundation is present
21
21
  (`SharedModule.forRoot` in `app.config.ts`, a populated shared-lib barrel). If not, tell the user to
22
22
  run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`; reference uses
23
- `@msr`) and the reference repo path (same one init used).
23
+ own) and the reference repo path (same one init used).
24
+
25
+ ## Framework first — MANDATORY
26
+
27
+ Read these before writing ANY component, service, pipe, directive or helper, and reuse what they
28
+ list — re-implementing framework code is a defect, not a shortcut:
29
+
30
+ 0. [**The Framework API index**](https://github.com/smartsoft001/mobilems-framework/blob/main/README.md#-framework-api) — the map of every package to
31
+ its catalog. Start here when you do not already know which package owns what.
32
+ 1. The framework API catalog — `packages/shared/angular/README.md` in this repo, installed as
33
+ `node_modules/@smartsoft001-mobilems/angular/README.md` in a museum app ([browse](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/shared/angular/README.md)).
34
+ This is the path `scripts/verify-module.sh` names when it fails.
35
+ 2. [`packages/objects/README.md`](https://github.com/smartsoft001/mobilems-framework/blob/main/packages/objects/README.md) — this module's package catalog
36
+ 3. `libs/shared/angular/README.md` — the app's own shared lib (built by `mobilems-project-init`)
37
+
38
+ | Do not hand-roll | Use |
39
+ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
40
+ | a masonry grid (`new Masonry(...)`, per-page masonry JS/CSS) | `<smart-mobilems-masonry-grid>` (`MasonryGridComponent`) with a `#masonryContentTpl`; the `Masonry` provider in step 3 only feeds it |
41
+ | filter state / hand-built `query` arrays | `FiltersBaseComponent<MsObject>` + its `FiltersContext` accessors (`searchText`, `keywords`, `page`, `sort`, `onlyImage`, …) |
42
+ | list/detail plumbing (facade wiring, id-from-route, PDF) | the `ObjectsListComponent` / `ObjectsItemComponent` base classes |
43
+ | media URLs, meta/OG tags, filter dictionaries, modals, WCAG | `FileUrlService`, `SeoService`/`MetaService`, `DictionaryService`, `ModalService`, `WcagService` |
44
+
45
+ Anything the framework genuinely lacks goes into `libs/shared/angular` **once** (and into its
46
+ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on hand-rolled masonry.
24
47
 
25
48
  ## Steps
26
49
 
@@ -28,10 +51,16 @@ run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`;
28
51
  `@smartsoft001-mobilems/objects-shell-dtos` (match the installed mobilems line) and
29
52
  `online-3d-viewer` (the 3D tab; `leaflet`/`openseadragon`/`swiper` are already foundation deps).
30
53
  Resolve versions at run time; do not `npm install` unless asked.
31
- 2. **Library** — create `libs/museum-objects/{domain,shell/angular}` per `references/lib-contents.md`:
32
- copy the domain entity/enums and the shell (`MuseumObjectsModule`, routes, `components/*`,
33
- `pages/{list,item}`, `services`), thread `<PREFIX>`. Add the two `tsconfig.base.json` paths and the
34
- `museum-objects` commit scope.
54
+ 2. **Library** — scaffold `libs/museum-objects` with this package's Nx generator (deterministic — do
55
+ NOT copy files by hand when the generator is available):
56
+ ```bash
57
+ npx nx g @smartsoft001-mobilems/objects-shell-angular:museum-module \
58
+ --referenceRoot=<reference-repo> --prefix=<PREFIX>
59
+ ```
60
+ It copies the lib from the reference, threads `@<PREFIX>/`, adds both `tsconfig.base.json`
61
+ paths, normalizes `project.json` paths and copies the module's translation groups (pl+en).
62
+ Fallback (older framework line without the generator): do the same by hand per
63
+ `references/lib-contents.md`. The `museum-objects` commit scope registers automatically from the `libs/` dir name.
35
64
  3. **Module + routes** — see `references/module-and-routes.md`: `MuseumObjectsModule` wraps
36
65
  `ObjectsSharedModule.forFeature({ routing: false, config: { type: MsObject, apiUrl, entity: 'objects' } })`
37
66
  with the `Masonry` SSR factory. Add the lazy `obiekty` route to `appRoutes` in
@@ -40,15 +69,18 @@ run `/mobilems:project-init` first. Hold `<PREFIX>` (from `tsconfig.base.json`;
40
69
  `objects-list-item`, and the object gallery widget (`widget/gallery-object`) in the shared lib +
41
70
  export them; register the object gallery widget in the widget content-type map (`GALLERY_OBJECT`);
42
71
  and add `ISpecificObject` (extends `MsObject`) to `models/objects.model.ts`.
43
- 5. **Verify** — run `scripts/verify-module.sh <workspace-root> <PREFIX>`: asserts the lib exists, the
72
+ 5. **Translations** — copy the module's translation groups (`OBJECTS`, `DETAILS_PAGE`, `GALLERY`) from the reference
73
+ repo's `apps/web/public/translations.json` into the app's file, for BOTH `pl` and `en`.
74
+ Complete group = every reference key present; every key referenced in the module's code
75
+ must exist in both languages — a raw `GROUP.key` rendered in the UI is a defect.
76
+ 6. **Verify** — run `REF_ROOT=<reference-repo> scripts/verify-module.sh <workspace-root> <PREFIX>`: asserts the lib exists, the
44
77
  `obiekty` route is registered (top-level + search child), the objects shared components are built
45
78
  and exported, the widget map registers the object gallery widget, and `ISpecificObject` is present.
46
79
  Fix and re-run.
47
80
 
48
- ## Known QA pitfalls (from live deployments — fix while copying, not after)
81
+ ## Known pitfalls (fix while copying, not after)
49
82
 
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`
83
+ The cross-cutting rules live in `mobilems-project-init`
52
84
  `references/qa-hardening.md`; these are the objects-specific ones:
53
85
 
54
86
  - **Search debounce.** The panel full-text search (`app-objects-searchbar variant="panel"`) must
@@ -63,7 +95,7 @@ Tomaszów MOB-949/993). The cross-cutting rules live in `mobilems-project-init`
63
95
  is valid; `Number('') === 0` — reject empty strings explicitly). Duplicating the predicate in two
64
96
  components caused a "map never renders" bug.
65
97
  - **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").
98
+ the same names (e.g. do not render "Materiał" where the mockup says "Tworzywo").
67
99
  - **Keywords section** maps the API `tags[]` field (not `keywords[]`) and renders clickable links
68
100
  that filter the list.
69
101
  - **Warning-clean templates:** after copying, remove needless `?.` on non-nullable signals (NG8107)