@smartsoft001-mobilems/claude-plugins 2.115.0 → 2.117.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 (28) hide show
  1. package/package.json +1 -1
  2. package/plugins/mobilems/.claude-plugin/plugin.json +1 -1
  3. package/plugins/mobilems/skills/mobilems-articles/references/module-and-routes.md +4 -22
  4. package/plugins/mobilems/skills/mobilems-articles/scripts/verify-module.sh +22 -1
  5. package/plugins/mobilems/skills/mobilems-devops-config/references/workflows.md +13 -0
  6. package/plugins/mobilems/skills/mobilems-devops-config/scripts/verify-devops.sh +10 -0
  7. package/plugins/mobilems/skills/mobilems-events/references/module-and-routes.md +4 -24
  8. package/plugins/mobilems/skills/mobilems-events/scripts/verify-module.sh +22 -1
  9. package/plugins/mobilems/skills/mobilems-object-collections/SKILL.md +1 -1
  10. package/plugins/mobilems/skills/mobilems-object-collections/references/module-and-routes.md +4 -26
  11. package/plugins/mobilems/skills/mobilems-object-collections/references/shared-components.md +5 -0
  12. package/plugins/mobilems/skills/mobilems-object-collections/scripts/verify-module.sh +22 -1
  13. package/plugins/mobilems/skills/mobilems-objects/SKILL.md +1 -1
  14. package/plugins/mobilems/skills/mobilems-objects/references/module-and-routes.md +4 -28
  15. package/plugins/mobilems/skills/mobilems-objects/references/shared-components.md +10 -1
  16. package/plugins/mobilems/skills/mobilems-objects/scripts/verify-module.sh +22 -1
  17. package/plugins/mobilems/skills/mobilems-partners/references/module-and-routes.md +4 -29
  18. package/plugins/mobilems/skills/mobilems-partners/scripts/verify-module.sh +22 -1
  19. package/plugins/mobilems/skills/mobilems-project-init/references/app-shell.md +7 -2
  20. package/plugins/mobilems/skills/mobilems-project-init/references/config-providers.md +14 -3
  21. package/plugins/mobilems/skills/mobilems-project-init/references/qa-hardening.md +39 -0
  22. package/plugins/mobilems/skills/mobilems-project-init/references/shared-lib.md +27 -10
  23. package/plugins/mobilems/skills/mobilems-project-init/references/tailwind-styles.md +9 -4
  24. package/plugins/mobilems/skills/mobilems-project-init/scripts/verify-init.sh +12 -0
  25. package/plugins/mobilems/skills/mobilems-search/scripts/verify-search.sh +12 -0
  26. package/plugins/mobilems/skills/mobilems-static-pages/scripts/verify-pages.sh +12 -0
  27. package/plugins/mobilems/skills/mobilems-thematic-publications/references/module-and-routes.md +4 -30
  28. package/plugins/mobilems/skills/mobilems-thematic-publications/scripts/verify-module.sh +22 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartsoft001-mobilems/claude-plugins",
3
- "version": "2.115.0",
3
+ "version": "2.117.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.115.0"
4
+ "version": "2.117.0"
5
5
  }
@@ -2,8 +2,8 @@
2
2
 
3
3
  An article module wraps `ArticlesSharedModule.forFeature` over the npm `Article` entity and filters
4
4
  its API requests with a `template` value. Thread `<PREFIX>` (the `environment` import) and the chosen
5
- `<name>` / `<entity>` / `<template>`. The `Masonry` factory returns the real library in the browser (`window.Masonry`, set by `bootstrapApplication` from `@smartsoft001-mobilems/angular/bootstrap` in `main.ts`)
6
- and a no-op mock during SSR. Copy the file structure from an existing article module in the reference
5
+ `<name>` / `<entity>` / `<template>`. `provideBrowserMasonry()` supplies the real library in the browser (`window.Masonry`, set by `bootstrapApplication` from `@smartsoft001-mobilems/angular/bootstrap` in `main.ts`)
6
+ and `null` during SSR (the grid skips layout). Copy the file structure from an existing article module in the reference
7
7
  MobilEMS app and substitute the parameters.
8
8
 
9
9
  ## `libs/museum-<name>/shell/angular/src/lib/<name>.module.ts`
@@ -15,32 +15,14 @@ import { RouterModule } from '@angular/router';
15
15
  import { SharedModule as SmartSharedModule } from '@smartsoft001/angular';
16
16
  import { Article } from '@smartsoft001-mobilems/articles-domain';
17
17
  import { ArticlesSharedModule } from '@smartsoft001-mobilems/articles-shell-angular';
18
- import { Masonry } from '@thisissoon/angular-masonry';
18
+ import { provideBrowserMasonry } from '@smartsoft001-mobilems/angular';
19
19
 
20
20
  import { environment } from '@<PREFIX>/angular';
21
21
 
22
22
  import { routes } from './<name>.routes';
23
23
 
24
24
  @NgModule({
25
- providers: [
26
- {
27
- provide: Masonry,
28
- useFactory: () => {
29
- if (typeof window !== 'undefined' && (window as any)['Masonry']) {
30
- return (window as any)['Masonry'];
31
- }
32
- // Return a mock for SSR
33
- return class {
34
- constructor() {
35
- // no-op
36
- }
37
- layout() { return; }
38
- reloadItems() { return; }
39
- destroy() { return; }
40
- };
41
- },
42
- },
43
- ],
25
+ providers: [provideBrowserMasonry()],
44
26
  imports: [
45
27
  FormsModule,
46
28
  SmartSharedModule,
@@ -70,13 +70,22 @@ fi
70
70
  # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
71
71
  # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
72
72
  # which the `new Masonry(` grep above cannot see.
73
- if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
73
+ if grep -rqE "provide: Masonry|provideBrowserMasonry" "$LIBDIR" 2>/dev/null; then
74
74
  if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
75
75
  pass "grids render via <smart-mobilems-masonry-grid>"
76
76
  else
77
77
  bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
78
78
  fi
79
79
  fi
80
+ # 2b. the provider is provideBrowserMasonry() — an anonymous stand-in class next to
81
+ # `provide: Masonry` reaches for window during SSR and is the bug the framework removed
82
+ STUB="$(grep -rlE "provide: Masonry" "$LIBDIR" 2>/dev/null | xargs grep -lE "return class|: class \{" 2>/dev/null || true)"
83
+ if [ -n "$STUB" ]; then
84
+ bad "hand-written Masonry stub (return class / : class {) — use provideBrowserMasonry() from @smartsoft001-mobilems/angular:"
85
+ printf ' %s\n' $STUB >&2
86
+ else
87
+ pass "no hand-written Masonry stub"
88
+ fi
80
89
  # 3. a list page must render through the framework grid
81
90
  LISTP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'list*' 2>/dev/null || true)"
82
91
  if [ -n "$LISTP" ]; then
@@ -118,6 +127,18 @@ else
118
127
  bad "translation gaps — fix the FAIL lines above"
119
128
  fi
120
129
 
130
+ echo "== Portal guard =="
131
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
132
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
133
+ if npx --no-install mobilems-check-portal; then
134
+ pass "mobilems-check-portal clean"
135
+ else
136
+ bad "mobilems-check-portal found recurring portal bugs — fix the FAIL lines above"
137
+ fi
138
+ else
139
+ echo " SKIP: mobilems-check-portal unavailable in this project (older framework line)"
140
+ fi
141
+
121
142
  echo
122
143
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
123
144
  exit "$fail"
@@ -374,6 +374,10 @@ jobs:
374
374
  - name: Install Playwright browsers
375
375
  run: npx playwright install --with-deps chromium
376
376
 
377
+ # Static guard for the recurring portal bugs (MOB-1889..1895); it reads sources only, so it
378
+ # runs before the build. Needs @smartsoft001-mobilems/angular with the `mobilems-check-portal` bin.
379
+ - run: npx mobilems-check-portal
380
+
377
381
  # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
378
382
  # - run: npx nx-cloud record -- echo Hello World
379
383
  - run: npx nx run-many -t lint test build e2e
@@ -382,6 +386,15 @@ jobs:
382
386
  if: always()
383
387
  ```
384
388
 
389
+ > **`mobilems-check-portal` is part of the CI contract.** Whoever enables the workflow keeps the
390
+ > step before the build. A project that builds in `development.yml` instead adds the same
391
+ > `- run: npx mobilems-check-portal` step ahead of its `nx build` step. `scripts/verify-devops.sh`
392
+ > fails when `ci.yml` / `ci.yml.disabled` lacks it.
393
+ >
394
+ > **Existing portal?** It will report dozens of `innerhtml-external-links` findings until it opts
395
+ > into `contentLinks: true`. Use `- run: npx mobilems-check-portal --skip innerhtml-external-links`
396
+ > (or `check-portal-ignore` comments) first, then remove the `--skip` once `contentLinks: true` is set.
397
+
385
398
  > Note: the reference's `ci.yml.disabled` pins Node 20. We thread `{{NODE_VERSION}}` here so the
386
399
  > disabled template matches the rest of the project. If a project has a live `ci.yml` on a stale
387
400
  > node version, report the drift and ask before changing it.
@@ -31,6 +31,16 @@ exists .github/workflows/push-to-client.yml
31
31
  exists .github/workflows/ai-plan.yml
32
32
  exists .github/workflows/ai-impl.yml
33
33
  exists .github/workflows/ci.yml.disabled
34
+ # CI runs the portal guard before the build (references/workflows.md)
35
+ CI_FILE=""
36
+ for f in .github/workflows/ci.yml .github/workflows/ci.yml.disabled; do
37
+ [ -f "$f" ] && { CI_FILE="$f"; break; }
38
+ done
39
+ if [ -n "$CI_FILE" ] && grep -q "mobilems-check-portal" "$CI_FILE"; then
40
+ pass "$CI_FILE runs mobilems-check-portal"
41
+ else
42
+ bad "ci.yml (or ci.yml.disabled) does not run mobilems-check-portal"
43
+ fi
34
44
  # secrets are referenced, not hardcoded
35
45
  [ -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
46
 
@@ -22,13 +22,13 @@ export class MuseumEventsModule {}
22
22
 
23
23
  ## `events.routes.ts`
24
24
 
25
- Every page is loaded lazily with `loadComponent`. The list route additionally provides the `Masonry`
26
- SSR factory (the real library in the browser — `window.Masonry`, set by `bootstrapApplication` from `@smartsoft001-mobilems/angular/bootstrap` in `main.ts` — a no-op mock during SSR). Copy verbatim — there is no
25
+ Every page is loaded lazily with `loadComponent`. The list route additionally provides
26
+ `provideBrowserMasonry()` (the real library in the browser — `window.Masonry`, set by `bootstrapApplication` from `@smartsoft001-mobilems/angular/bootstrap` in `main.ts` — and `null` during SSR, where the grid skips layout). Copy verbatim — there is no
27
27
  `<PREFIX>` to thread here either.
28
28
 
29
29
  ```ts
30
30
  import { Route } from '@angular/router';
31
- import { Masonry } from '@thisissoon/angular-masonry';
31
+ import { provideBrowserMasonry } from '@smartsoft001-mobilems/angular';
32
32
 
33
33
  export const eventsRoutes: Route[] = [
34
34
  {
@@ -64,27 +64,7 @@ export const eventsRoutes: Route[] = [
64
64
  path: '',
65
65
  loadComponent: () =>
66
66
  import('./pages/list/list.component').then((m) => m.EventsListComponent),
67
- providers: [
68
- {
69
- provide: Masonry,
70
- useFactory: () => {
71
- if (typeof window !== 'undefined' && (window as any)['Masonry']) {
72
- return (window as any)['Masonry'];
73
- }
74
- return class {
75
- layout() {
76
- return;
77
- }
78
- reloadItems() {
79
- return;
80
- }
81
- destroy() {
82
- return;
83
- }
84
- };
85
- },
86
- },
87
- ],
67
+ providers: [provideBrowserMasonry()],
88
68
  },
89
69
  ];
90
70
  ```
@@ -67,13 +67,22 @@ fi
67
67
  # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
68
68
  # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
69
69
  # which the `new Masonry(` grep above cannot see.
70
- if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
70
+ if grep -rqE "provide: Masonry|provideBrowserMasonry" "$LIBDIR" 2>/dev/null; then
71
71
  if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
72
72
  pass "grids render via <smart-mobilems-masonry-grid>"
73
73
  else
74
74
  bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
75
75
  fi
76
76
  fi
77
+ # 2b. the provider is provideBrowserMasonry() — an anonymous stand-in class next to
78
+ # `provide: Masonry` reaches for window during SSR and is the bug the framework removed
79
+ STUB="$(grep -rlE "provide: Masonry" "$LIBDIR" 2>/dev/null | xargs grep -lE "return class|: class \{" 2>/dev/null || true)"
80
+ if [ -n "$STUB" ]; then
81
+ bad "hand-written Masonry stub (return class / : class {) — use provideBrowserMasonry() from @smartsoft001-mobilems/angular:"
82
+ printf ' %s\n' $STUB >&2
83
+ else
84
+ pass "no hand-written Masonry stub"
85
+ fi
77
86
  # 3. a list page must render through the framework grid
78
87
  LISTP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'list*' 2>/dev/null || true)"
79
88
  if [ -n "$LISTP" ]; then
@@ -106,6 +115,18 @@ else
106
115
  pass "no filters component in this module"
107
116
  fi
108
117
 
118
+ echo "== Portal guard =="
119
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
120
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
121
+ if npx --no-install mobilems-check-portal; then
122
+ pass "mobilems-check-portal clean"
123
+ else
124
+ bad "mobilems-check-portal found recurring portal bugs — fix the FAIL lines above"
125
+ fi
126
+ else
127
+ echo " SKIP: mobilems-check-portal unavailable in this project (older framework line)"
128
+ fi
129
+
109
130
  echo
110
131
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
111
132
  exit "$fail"
@@ -66,7 +66,7 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
66
66
  `kolekcje` route to `appRoutes` in `apps/web/src/app/app.routes.ts` and a matching child under the
67
67
  `wyszukaj/:text` search route.
68
68
  4. **Collections shared component** — see `references/shared-components.md`: build
69
- `components/collections-grid-item` (it imports `public-collections-domain`) in the shared lib and
69
+ `components/collections-grid-item` (it imports `public-collections-domain`) in the shared lib (a component that `extends CollectionsGridItemBaseComponent` with the app's own markup) and
70
70
  export it from `components/index.ts`. Collections contributes no widget-map entry and no model type
71
71
  (those are objects-only).
72
72
  5. **Translations** — copy the module's translation groups (`COLLECTIONS`) from the reference
@@ -4,8 +4,8 @@
4
4
 
5
5
  Thread `<PREFIX>` (the `environment` + domain imports). `PublicCollectionsSharedModule.forFeature`
6
6
  provides the list/detail behaviour over `MuseumCollection`; `CollectionsService` is provided as the
7
- `CrudService`; the `Masonry` factory returns the real library in the browser and a no-op mock during
8
- SSR. Note: unlike the objects module this one does NOT import `SmartSharedModule`.
7
+ `CrudService`; `provideBrowserMasonry()` supplies the real library in the browser and `null` during
8
+ SSR (the grid then skips layout). Note: unlike the objects module this one does NOT import `SmartSharedModule`.
9
9
 
10
10
  ```ts
11
11
  import { NgModule } from '@angular/core';
@@ -13,7 +13,7 @@ import { FormsModule } from '@angular/forms';
13
13
  import { RouterModule } from '@angular/router';
14
14
  import { CrudService } from '@smartsoft001/crud-shell-angular';
15
15
  import { PublicCollectionsSharedModule } from '@smartsoft001-mobilems/public-collections-shell-angular';
16
- import { Masonry } from '@thisissoon/angular-masonry';
16
+ import { provideBrowserMasonry } from '@smartsoft001-mobilems/angular';
17
17
 
18
18
  import { environment } from '@<PREFIX>/angular';
19
19
  import { MuseumCollection } from '@<PREFIX>/museum-collections/domain';
@@ -28,29 +28,7 @@ import { CollectionsService } from './services';
28
28
  provide: CrudService,
29
29
  useClass: CollectionsService,
30
30
  },
31
- {
32
- provide: Masonry,
33
- useFactory: () => {
34
- if (typeof window !== 'undefined' && (window as any)['Masonry']) {
35
- return (window as any)['Masonry'];
36
- }
37
- // Return a mock for SSR
38
- return class {
39
- constructor() {
40
- // no-op
41
- }
42
- layout() {
43
- return;
44
- }
45
- reloadItems() {
46
- return;
47
- }
48
- destroy() {
49
- return;
50
- }
51
- };
52
- },
53
- },
31
+ provideBrowserMasonry(),
54
32
  ],
55
33
  imports: [
56
34
  FormsModule,
@@ -21,6 +21,11 @@ Build this component directory and export it from `components/index.ts`:
21
21
  export * from './collections-grid-item';
22
22
  ```
23
23
 
24
+ `CollectionsGridItemComponent` extends `CollectionsGridItemBaseComponent` (the list row, where the
25
+ museum has one, extends `CollectionCardBaseComponent`): the framework holds the logic, the museum
26
+ keeps its own template and styles, and the domain stores stay in the museum, reached through hooks.
27
+ Do not copy the reference's card logic.
28
+
24
29
  `collections-grid-item` carries its own `index.ts`
25
30
  (`export * from './collections-grid-item.component';`) plus the `.component.ts` + `.component.html`.
26
31
  Build the whole dir from the reference and thread `<PREFIX>`.
@@ -53,13 +53,22 @@ fi
53
53
  # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
54
54
  # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
55
55
  # which the `new Masonry(` grep above cannot see.
56
- if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
56
+ if grep -rqE "provide: Masonry|provideBrowserMasonry" "$LIBDIR" 2>/dev/null; then
57
57
  if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
58
58
  pass "grids render via <smart-mobilems-masonry-grid>"
59
59
  else
60
60
  bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
61
61
  fi
62
62
  fi
63
+ # 2b. the provider is provideBrowserMasonry() — an anonymous stand-in class next to
64
+ # `provide: Masonry` reaches for window during SSR and is the bug the framework removed
65
+ STUB="$(grep -rlE "provide: Masonry" "$LIBDIR" 2>/dev/null | xargs grep -lE "return class|: class \{" 2>/dev/null || true)"
66
+ if [ -n "$STUB" ]; then
67
+ bad "hand-written Masonry stub (return class / : class {) — use provideBrowserMasonry() from @smartsoft001-mobilems/angular:"
68
+ printf ' %s\n' $STUB >&2
69
+ else
70
+ pass "no hand-written Masonry stub"
71
+ fi
63
72
  # 3. the ITEM page renders its objects grid through the framework grid — in the
64
73
  # reference the collections LIST is a plain grid; masonry lives on the item page
65
74
  ITEMP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'item*' 2>/dev/null || true)"
@@ -102,6 +111,18 @@ else
102
111
  bad "translation gaps — fix the FAIL lines above"
103
112
  fi
104
113
 
114
+ echo "== Portal guard =="
115
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
116
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
117
+ if npx --no-install mobilems-check-portal; then
118
+ pass "mobilems-check-portal clean"
119
+ else
120
+ bad "mobilems-check-portal found recurring portal bugs — fix the FAIL lines above"
121
+ fi
122
+ else
123
+ echo " SKIP: mobilems-check-portal unavailable in this project (older framework line)"
124
+ fi
125
+
105
126
  echo
106
127
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
107
128
  exit "$fail"
@@ -66,7 +66,7 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
66
66
  with the `Masonry` SSR factory. Add the lazy `obiekty` route to `appRoutes` in
67
67
  `apps/web/src/app/app.routes.ts` and a matching child under the `wyszukaj/:text` search route.
68
68
  4. **Objects shared components** — see `references/shared-components.md`: build `objects-grid-item`,
69
- `objects-list-item`, and the object gallery widget (`widget/gallery-object`) in the shared lib +
69
+ `objects-list-item` (each `extends` the framework `Objects…ItemBaseComponent`, with the app's own markup), and the object gallery widget (`widget/gallery-object`, `extends` the framework `WidgetGalleryObjectBaseComponent`) in the shared lib +
70
70
  export them; and register the object gallery widget in the widget content-type map (`GALLERY_OBJECT`).
71
71
  `ISpecificObject` and the filter models come from `@smartsoft001-mobilems/objects-shell-angular`.
72
72
  5. **Translations** — copy the module's translation groups (`OBJECTS`, `DETAILS_PAGE`, `GALLERY`) from the reference
@@ -3,8 +3,8 @@
3
3
  ## `libs/museum-objects/shell/angular/src/lib/objects.module.ts`
4
4
 
5
5
  Thread `<PREFIX>` (the `environment` import). `ObjectsSharedModule.forFeature` provides the list/detail
6
- behaviour over `MsObject`; the `Masonry` factory returns the real library in the browser and a no-op
7
- mock during SSR.
6
+ behaviour over `MsObject`; `provideBrowserMasonry()` supplies the real library in the browser and `null`
7
+ during SSR (the grid then skips layout).
8
8
 
9
9
  ```ts
10
10
  import { NgModule } from '@angular/core';
@@ -13,38 +13,14 @@ import { RouterModule } from '@angular/router';
13
13
  import { SharedModule as SmartSharedModule } from '@smartsoft001/angular';
14
14
  import { MsObject } from '@smartsoft001-mobilems/objects-domain';
15
15
  import { ObjectsSharedModule } from '@smartsoft001-mobilems/objects-shell-angular';
16
- import { Masonry } from '@thisissoon/angular-masonry';
16
+ import { provideBrowserMasonry } from '@smartsoft001-mobilems/angular';
17
17
 
18
18
  import { environment } from '@<PREFIX>/angular';
19
19
 
20
20
  import { routes } from './objects.routes';
21
21
 
22
22
  @NgModule({
23
- providers: [
24
- {
25
- provide: Masonry,
26
- useFactory: () => {
27
- if (typeof window !== 'undefined' && (window as any)['Masonry']) {
28
- return (window as any)['Masonry'];
29
- }
30
- // Return a mock for SSR
31
- return class {
32
- constructor() {
33
- // no-op
34
- }
35
- layout() {
36
- return;
37
- }
38
- reloadItems() {
39
- return;
40
- }
41
- destroy() {
42
- return;
43
- }
44
- };
45
- },
46
- },
47
- ],
23
+ providers: [provideBrowserMasonry()],
48
24
  imports: [
49
25
  FormsModule,
50
26
  SmartSharedModule,
@@ -16,6 +16,14 @@ Build these component directories and export them from `components/index.ts`:
16
16
  | `components/objects-grid-item` | an object card in the grid view |
17
17
  | `components/objects-list-item` | an object row in the list view |
18
18
 
19
+ Both are **logic-free shells over framework bases**: `ObjectsGridItemComponent extends
20
+ ObjectsGridItemBaseComponent` and `ObjectsListItemComponent extends ObjectsListItemBaseComponent`
21
+ (both build on `ObjectCardBaseComponent`; field helpers `objectCardMeta`/`objectCardPlace`/
22
+ `objectCardExhibition` come from the framework). Do not copy the reference's card logic. The museum
23
+ keeps its own template and styles; the favorites and selection stores stay in the museum and reach
24
+ the base through its hooks (`FavoriteButtonBaseComponent`/`IFavoriteHooks`,
25
+ `SelectionActionsBaseComponent`).
26
+
19
27
  ```ts
20
28
  // components/index.ts — these exports belong to the objects module:
21
29
  export * from './objects-grid-item';
@@ -24,7 +32,8 @@ export * from './objects-list-item';
24
32
 
25
33
  ## 2. Object gallery widget
26
34
 
27
- Build `components/widget/gallery-object` and export it from the widget barrel:
35
+ Build `components/widget/gallery-object` as `extends WidgetGalleryObjectBaseComponent` (from
36
+ `@smartsoft001-mobilems/angular`) with the app's own markup and styles, and export it from the widget barrel:
28
37
 
29
38
  ```ts
30
39
  // components/widget/index.ts:
@@ -59,13 +59,22 @@ fi
59
59
  # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
60
60
  # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
61
61
  # which the `new Masonry(` grep above cannot see.
62
- if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
62
+ if grep -rqE "provide: Masonry|provideBrowserMasonry" "$LIBDIR" 2>/dev/null; then
63
63
  if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
64
64
  pass "grids render via <smart-mobilems-masonry-grid>"
65
65
  else
66
66
  bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
67
67
  fi
68
68
  fi
69
+ # 2b. the provider is provideBrowserMasonry() — an anonymous stand-in class next to
70
+ # `provide: Masonry` reaches for window during SSR and is the bug the framework removed
71
+ STUB="$(grep -rlE "provide: Masonry" "$LIBDIR" 2>/dev/null | xargs grep -lE "return class|: class \{" 2>/dev/null || true)"
72
+ if [ -n "$STUB" ]; then
73
+ bad "hand-written Masonry stub (return class / : class {) — use provideBrowserMasonry() from @smartsoft001-mobilems/angular:"
74
+ printf ' %s\n' $STUB >&2
75
+ else
76
+ pass "no hand-written Masonry stub"
77
+ fi
69
78
  # 3. a list page must render through the framework grid
70
79
  LISTP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'list*' 2>/dev/null || true)"
71
80
  if [ -n "$LISTP" ]; then
@@ -107,6 +116,18 @@ else
107
116
  bad "translation gaps — fix the FAIL lines above"
108
117
  fi
109
118
 
119
+ echo "== Portal guard =="
120
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
121
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
122
+ if npx --no-install mobilems-check-portal; then
123
+ pass "mobilems-check-portal clean"
124
+ else
125
+ bad "mobilems-check-portal found recurring portal bugs — fix the FAIL lines above"
126
+ fi
127
+ else
128
+ echo " SKIP: mobilems-check-portal unavailable in this project (older framework line)"
129
+ fi
130
+
110
131
  echo
111
132
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
112
133
  exit "$fail"
@@ -4,8 +4,8 @@
4
4
 
5
5
  Thread `<PREFIX>` (the `environment` import). This module REUSES `ArticlesSharedModule` (the same
6
6
  shared module the news/articles/thematic-paths modules use) but configures it over the Articles
7
- `Article` type with its OWN `getReducer` and **no `baseQuery`**. The `Masonry` factory returns the
8
- real library in the browser and a no-op mock during SSR (identical to objects). It provides the
7
+ `Article` type with its OWN `getReducer` and **no `baseQuery`**. `provideBrowserMasonry()` supplies the
8
+ real library in the browser and `null` during SSR (identical to objects). It provides the
9
9
  `SERVICES` array (which binds both `CrudService` and `ArticlesService` to `PartnersService`).
10
10
 
11
11
  ```ts
@@ -14,7 +14,7 @@ import { FormsModule } from '@angular/forms';
14
14
  import { RouterModule } from '@angular/router';
15
15
  import { Article } from '@smartsoft001-mobilems/articles-domain';
16
16
  import { ArticlesSharedModule } from '@smartsoft001-mobilems/articles-shell-angular';
17
- import { Masonry } from '@thisissoon/angular-masonry';
17
+ import { provideBrowserMasonry } from '@smartsoft001-mobilems/angular';
18
18
 
19
19
  import { environment } from '@<PREFIX>/angular';
20
20
 
@@ -23,32 +23,7 @@ import { routes } from './partners.routes';
23
23
  import { SERVICES } from './services';
24
24
 
25
25
  @NgModule({
26
- providers: [
27
- ...SERVICES,
28
- {
29
- provide: Masonry,
30
- useFactory: () => {
31
- if (typeof window !== 'undefined' && (window as any)['Masonry']) {
32
- return (window as any)['Masonry'];
33
- }
34
- // Return a mock for SSR
35
- return class {
36
- constructor() {
37
- // no-op
38
- }
39
- layout() {
40
- return;
41
- }
42
- reloadItems() {
43
- return;
44
- }
45
- destroy() {
46
- return;
47
- }
48
- };
49
- },
50
- },
51
- ],
26
+ providers: [...SERVICES, provideBrowserMasonry()],
52
27
  imports: [
53
28
  FormsModule,
54
29
  ArticlesSharedModule.forFeature({
@@ -68,13 +68,22 @@ fi
68
68
  # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
69
69
  # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
70
70
  # which the `new Masonry(` grep above cannot see.
71
- if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
71
+ if grep -rqE "provide: Masonry|provideBrowserMasonry" "$LIBDIR" 2>/dev/null; then
72
72
  if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
73
73
  pass "grids render via <smart-mobilems-masonry-grid>"
74
74
  else
75
75
  bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
76
76
  fi
77
77
  fi
78
+ # 2b. the provider is provideBrowserMasonry() — an anonymous stand-in class next to
79
+ # `provide: Masonry` reaches for window during SSR and is the bug the framework removed
80
+ STUB="$(grep -rlE "provide: Masonry" "$LIBDIR" 2>/dev/null | xargs grep -lE "return class|: class \{" 2>/dev/null || true)"
81
+ if [ -n "$STUB" ]; then
82
+ bad "hand-written Masonry stub (return class / : class {) — use provideBrowserMasonry() from @smartsoft001-mobilems/angular:"
83
+ printf ' %s\n' $STUB >&2
84
+ else
85
+ pass "no hand-written Masonry stub"
86
+ fi
78
87
  # 3. a list page must render through the framework grid
79
88
  LISTP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'list*' 2>/dev/null || true)"
80
89
  if [ -n "$LISTP" ]; then
@@ -116,6 +125,18 @@ else
116
125
  bad "translation gaps — fix the FAIL lines above"
117
126
  fi
118
127
 
128
+ echo "== Portal guard =="
129
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
130
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
131
+ if npx --no-install mobilems-check-portal; then
132
+ pass "mobilems-check-portal clean"
133
+ else
134
+ bad "mobilems-check-portal found recurring portal bugs — fix the FAIL lines above"
135
+ fi
136
+ else
137
+ echo " SKIP: mobilems-check-portal unavailable in this project (older framework line)"
138
+ fi
139
+
119
140
  echo
120
141
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
121
142
  exit "$fail"
@@ -69,14 +69,19 @@ The framework ships the bases; the app keeps the templates (design) and the data
69
69
  `PageHeadingOwner`; the template renders `loading()`, `loadFailed()` (→ `app-error-retry (retry)="retry()"`),
70
70
  the breadcrumb declaration (`pageRoute ?? '/strona/' + id + '/' + (title | smartSlug)`), a hidden `h1`
71
71
  unless `sectionsHaveH1()`, and `<smart-mobilems-page-sections [sections]="pageData.sections ?? null">`
72
- with `[innerHTML]="contentHtml()"` in the slot. Routes use `pageResolver` (`strona/:id`) and
72
+ with `[innerHTML]="contentHtml()"` in the slot. CMS `[innerHTML]` links (leave-site modal, `rel`,
73
+ new-tab note, `#id` scroll without a route change) are handled by `AppComponent`'s content-link
74
+ directives, but only when `provideMobilemsApp({ contentLinks: true })` is set — it is **opt-in, off by
75
+ default**, so the foundation sets it (`references/config-providers.md`). With `contentLinks: false`
76
+ (or unset), every `[innerHTML]` carries `smartExternalLinks smartInPageAnchors` itself, otherwise
77
+ `mobilems-check-portal` fails `innerhtml-external-links`. Routes use `pageResolver` (`strona/:id`) and
73
78
  `createSlugPageResolver('kontakt', { emptyTitleKey: 'CONTACT.pageTitle' })` with
74
79
  `runGuardsAndResolvers: 'always'` and `data: { pageRoute: '/kontakt' }` for `/kontakt`.
75
80
  - `search.component` extends `SearchBaseComponent<'obiekty' | …>` and declares only `tabDefinitions`
76
81
  (`{ id, labelKey: 'SEARCH.tabs.*', count: (d) => d.objects }` per module the app ships); the template
77
82
  renders the heading, `app-tabs` with `[(activeTabId)]="activeTab"` and a `<router-outlet>`.
78
83
  - `not-found.component` extends `NotFoundComponent` and renders `<smart-mobilems-not-found-content
79
- [fullHeight]="true" />` (plus the breadcrumb declaration with `url`).
84
+ [fullHeight]="true" />` (plus the breadcrumb declaration with `url`).
80
85
 
81
86
  Run a feature-package grep over the pages you copy as a guard. **Do not copy `article-preview`** — the
82
87
  reference's `article-preview` page imports `@smartsoft001-mobilems/articles-domain` (it previews an
@@ -45,12 +45,19 @@ export const appConfig: ApplicationConfig = {
45
45
  provideMobilemsApp({
46
46
  environment, // provides MOBILEMS_ENVIRONMENT; `version` defaults to environment.version
47
47
  languages: { available: ['pl', 'en'], default: 'pl' },
48
+ contentLinks: true, // opt-in (off by default): the shell handles external and #id links in CMS content
48
49
  skipLinks: {
49
50
  home: [
50
- { targetId: 'main-new-objects', translationKey: 'HOME.accessibility.skipToNewObjects' },
51
+ {
52
+ targetId: 'main-new-objects',
53
+ translationKey: 'HOME.accessibility.skipToNewObjects',
54
+ },
51
55
  ],
52
56
  subpage: [
53
- { targetId: 'main-content', translationKey: 'HOME.accessibility.skipToContent' },
57
+ {
58
+ targetId: 'main-content',
59
+ translationKey: 'HOME.accessibility.skipToContent',
60
+ },
54
61
  ],
55
62
  },
56
63
  // Produkcyjnie translacje są własnością CMS-a klienta (setting/theme),
@@ -65,7 +72,11 @@ export const appConfig: ApplicationConfig = {
65
72
  },
66
73
  seo: {
67
74
  siteNameFallback: { pl: '<NAZWA MUZEUM>', en: '<MUSEUM NAME>' },
68
- defaultOgImage: { url: '/gfx/og-default.jpg', width: 1200, height: 630 },
75
+ defaultOgImage: {
76
+ url: '/gfx/og-default.jpg',
77
+ width: 1200,
78
+ height: 630,
79
+ },
69
80
  siteUrl: () => environment.siteUrl,
70
81
  },
71
82
  }),
@@ -121,3 +121,42 @@ These conventions were enforced by QA across modules — bake them in from the s
121
121
  - **Image fallback:** cards show the app's `no-image` placeholder when a thumbnail fails to load.
122
122
  - Keyword/tag chips must wrap on 360 px wide screens — no page-level horizontal scroll at
123
123
  360/768/1024/1440.
124
+
125
+ ## 8. Recurring portal bugs (MOB-1889..1895)
126
+
127
+ Run `npx mobilems-check-portal` (it is part of `scripts/verify-init.sh` and of CI, see
128
+ `mobilems-devops-config`). It reads sources only, so the items marked "manual" still need a check in
129
+ the browser or against the SSR response. Each item says what enforces it.
130
+
131
+ - [ ] **CLS < 0.1 with SSR.** No `[width]`/`[height]` bound to `naturalWidth`/`naturalHeight`, images
132
+ reserve their space. _Enforced in part_: `natural-size-binding`. Measure the rest (Lighthouse).
133
+ - [ ] **`Content-Encoding` on the HTML.** `createServer` compresses by default (`compression` peer
134
+ installed, `ICompressionOptions` to tune); do not add a second `compression()`. _Manual_:
135
+ `curl -sI -H 'Accept-Encoding: gzip' <url>`.
136
+ - [ ] **No `window is not defined` in SSR logs.** Masonry via `provideBrowserMasonry()`, browser-only work
137
+ behind `isPlatformBrowser`. _Enforced in part_: `masonry-stub`. Read the server log on the first load.
138
+ - [ ] **Modal for a link in content, menu and footer.** Set `provideMobilemsApp({ contentLinks: true })`
139
+ (opt-in, off by default) or put `smartExternalLinks` on each `[innerHTML]` host and on the menu and
140
+ footer. _Enforced_: `innerhtml-external-links` (skipped only with the opt-in). Menu/footer: manual.
141
+ - [ ] **Footnote without route change.** A `#id` link scrolls inside the page (`contentLinks` covers it
142
+ in content). _Manual_: click a footnote, the URL path must not change.
143
+ - [ ] **Photos uncropped and ≤ 50svh.** No `object-cover` on museum photos; cap the height at `50svh`.
144
+ _Enforced in part_: `object-cover` (the height cap is manual).
145
+ - [ ] **No placeholders in Contact/footer.** _Enforced_: `contact-placeholder` (`Przykładowa`,
146
+ `00 000 00 00`).
147
+ - [ ] **SVG icons and a safe download filename.** An icon component, not an emoji, arrow or cross glyph;
148
+ the download name comes from `photoFileName`. _Enforced in part_: `template-emoji` (the filename
149
+ is manual).
150
+ - [ ] **Year debounce 500 ms.** The year range filter extends the framework date-input base
151
+ (`FILTER_DATE_INPUT_DEBOUNCE_MS = 500`). _Manual_.
152
+ - [ ] **Filter pill ≥ 24 px.** WCAG 2.2 target size for the active-filter pills. _Manual_ (`ui-a11y-validator`).
153
+ - [ ] **Hover only with a pointer.** `presets: [mobilemsPreset]` (`hoverOnlyWhenSupported`), see
154
+ `tailwind-styles.md`. _Manual_: no sticky hover on a touch emulation.
155
+ - [ ] **List chunk without `three`.** `online-3d-viewer` is loaded with `createLazyLoader(() => import(…))`,
156
+ never a static import. _Enforced_: `static-online-3d-viewer`. Confirm in the build stats.
157
+ - [ ] **No `width="0"` in SSR HTML.** _Enforced in part_: `natural-size-binding`. Grep the SSR
158
+ response: `curl -s <url> | grep -c 'width="0"'`.
159
+ - [ ] **`loading="lazy"` below the first screen.** The LCP image stays eager. _Manual_.
160
+
161
+ To accept a finding on purpose, add `check-portal-ignore <rule>: reason` on the line (see the framework
162
+ README, `mobilems-check-portal`).
@@ -50,6 +50,18 @@ FEATURE-OWNED. A typical FEATURE-OWNED set (confirm with the classifier on your
50
50
  | `news-grid-item`, `news-list-item` | `mobilems-articles` (news) |
51
51
  | `widget/gallery-article` | `mobilems-articles` |
52
52
 
53
+ The feature-owned cards are still generated by their module skill, but as `extends`
54
+ `ObjectsGridItemBaseComponent` / `ObjectsListItemBaseComponent` (objects) and
55
+ `CollectionsGridItemBaseComponent` (collections; the list row extends `CollectionCardBaseComponent`),
56
+ not as copies of the reference logic. The museum keeps its own template and styles, and the domain
57
+ stores (favorites, selection, downloads, recommended items) stay in the museum and reach the base
58
+ through its hooks (`IFavoriteHooks`, `ISelectionActionsFavorites`, `IRecommendedSources`, the abstract `downloadCardsExport`).
59
+
60
+ The same applies to the foundation shared components `favorite-button`, `favorites-notice`,
61
+ `selection-actions`, `download-cards` and `recommended`: extend `FavoriteButtonBaseComponent`,
62
+ `FavoritesNoticeBaseComponent`, `SelectionActionsBaseComponent`, `DownloadCardsBaseComponent` and
63
+ `RecommendedBaseComponent`, write the markup yourself and wire the museum store through the hooks.
64
+
53
65
  Build everything else — `header`, `footer`, `page`, the `filter*` family, `breadcrumbs`,
54
66
  `pagination`, `searchbar`, `loader`, `error-retry`, `scroll-top`, `wcag`, `gallery`,
55
67
  `map`, `slider`, `carousel`, the three carousels above, `category-dropdown`, `dropdown`, `menu-item`,
@@ -88,6 +100,11 @@ Build it registering only the **foundation widgets** — `text`, `image`, `video
88
100
  and type-map entries when they build those widgets. Build the widget barrel
89
101
  (`components/widget/index.ts`) exporting the foundation widgets only.
90
102
 
103
+ The foundation `image`, `audio`, `media` and `gallery-image` widgets are generated as `extends
104
+ WidgetImageBaseComponent` / `WidgetAudioBaseComponent` / `WidgetMediaBaseComponent` /
105
+ `WidgetGalleryImageBaseComponent`, with the app's own markup and styles; `text` extends
106
+ `WidgetBaseComponent` and `video` has no framework base. The framework ships no templates.
107
+
91
108
  ## Models — framework, not the lib
92
109
 
93
110
  The museum API models (`IObject`, `IImage`, `IDictionary`, `IDictionaryEntry`, `IArticleListItem`,
@@ -117,16 +134,16 @@ files duplicate exports that now exist; copying those is the single most common
117
134
 
118
135
  Framework now — do **not** generate (import from `@smartsoft001-mobilems/angular`):
119
136
 
120
- | Kind | Framework exports |
121
- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
122
- | environment | `createMobilemsEnvironment`, `MOBILEMS_ENVIRONMENT`, `EnvironmentService` |
123
- | pipes | `altText`, `fileUrl`, `backgroundUrl`, `plainText`, `stripAnchors`, `capitalize` |
124
- | tools | `classifyLink`, `isInternalLink`, `normalizeUrl`, `stripHtml`, `formatNamedEntries`, `normalizeSearchText`, `newTabText`, `pageToOffset`, `decodeHtmlEntities`, `ElementIdSequence`, `createSafeTimeouts`, `photoFileName`/`urlExtension`, `widgetImageUrl`, `overflowingTags`, `themeTagKey`, `themeTagsLayout`, `THEME_TAGS_VISIBLE_LIMIT`, `resolveMenuItemTarget`, `isNavigableMenuItem` (+ `MenuItemTarget`) |
125
- | directives | `a[smartExternalLink]`, `[smartExternalLinks]`, `[smartFocusTrap]`, `[smartTruncatedTitle]`, `[smartInPageAnchors]` (`InPageAnchorsDirective`; references use `app…` — rename the attributes) |
126
- | services | `ConfirmationModalService`, `ExternalLinkConfirmationService`, `FilterPersistenceService`, `OrientationService`, `WcagChangesService`, `LineHeightService`, `BreadcrumbsService`, `ContactService`, `DataService`, `MuseumSeoService` |
127
- | interceptors | `LanguageInterceptor`, `PreviewAuthInterceptor`, `SsrTimeoutInterceptor`, `DedupeRequestInterceptor` |
128
- | bases | `CalendarBaseComponent`, `PaginationDotsBaseComponent`, `PaginationBaseComponent`, `CarouselBaseComponent`, `PartnersCarouselBaseComponent`, `NewObjectsCarouselBaseComponent`, `ArticlesCarouselBaseComponent`, `SliderBaseComponent` (+ `ISliderOptions`; `ScrollTopComponent` is server-guarded), `GalleryBaseComponent`, `GalleryFullscreenDialogBaseComponent` (`GalleryFullscreenBaseComponent` is `@deprecated`), `ConfirmationModalBaseComponent`, `NewsletterModalBaseComponent`, `MapBaseComponent`, `FilterInputBaseComponent`, `FilterDateInputBaseComponent`, `FilterCustomDictionaryInputBaseComponent`, `FilterDictionaryInputBaseComponent` (inline tree, `source`, paging), `ThemeTagsFilterBaseComponent`, `CatalogFilterBarBaseComponent`, `CatalogToolbarBaseComponent`, `SearchbarBaseComponent`, `SelectCheckboxBaseComponent` (+ `ISelectionHooks`), `DropdownBaseComponent`, `TabsBaseComponent`, `WcagBaseComponent` (+ `WCAG_OPTION_GROUPS`), `BreadcrumbsBaseComponent`, `BreadcrumbsBarBaseComponent`, `MenuItemBaseComponent` |
129
- | SSR tools | `injectServerResponseInit`, `appendPreloadLinks` (image preload `Link` headers) |
137
+ | Kind | Framework exports |
138
+ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
139
+ | environment | `createMobilemsEnvironment`, `MOBILEMS_ENVIRONMENT`, `EnvironmentService` |
140
+ | pipes | `altText`, `fileUrl`, `backgroundUrl`, `plainText`, `stripAnchors`, `capitalize` |
141
+ | tools | `classifyLink`, `isInternalLink`, `normalizeUrl`, `stripHtml`, `formatNamedEntries`, `normalizeSearchText`, `newTabText`, `pageToOffset`, `decodeHtmlEntities`, `ElementIdSequence`, `createSafeTimeouts`, `createLazyLoader` (single-flight dynamic `import()`, e.g. `online-3d-viewer`), `photoFileName`/`urlExtension`, `widgetImageUrl`, `overflowingTags`, `themeTagKey`, `themeTagsLayout`, `THEME_TAGS_VISIBLE_LIMIT`, `resolveMenuItemTarget`, `isNavigableMenuItem` (+ `MenuItemTarget`), `objectCardMeta`/`objectCardPlace`/`objectCardExhibition`/`exhibitionText` (+ `IObjectCardSource`), `SERIES_TITLES_FILTER`, `catalogEntryFilterParams` (the named-entries filter query params), `recommendedPerPage`, `widgetDomId`, `cleanWidgetText`, `formatFileSize`, `safeDecode` |
142
+ | directives | `a[smartExternalLink]`, `[smartExternalLinks]`, `[smartFocusTrap]`, `[smartTruncatedTitle]`, `[smartInPageAnchors]` (`InPageAnchorsDirective`; references use `app…` — rename the attributes) |
143
+ | services | `ConfirmationModalService`, `ExternalLinkConfirmationService`, `FilterPersistenceService`, `OrientationService`, `WcagChangesService`, `LineHeightService`, `BreadcrumbsService`, `ContactService`, `DataService`, `MuseumSeoService` |
144
+ | interceptors | `LanguageInterceptor`, `PreviewAuthInterceptor`, `SsrTimeoutInterceptor`, `DedupeRequestInterceptor` |
145
+ | bases | `CalendarBaseComponent`, `PaginationDotsBaseComponent`, `PaginationBaseComponent`, `CarouselBaseComponent`, `PartnersCarouselBaseComponent`, `NewObjectsCarouselBaseComponent`, `ArticlesCarouselBaseComponent`, `SliderBaseComponent` (+ `ISliderOptions`; `ScrollTopComponent` is server-guarded), `GalleryBaseComponent`, `GalleryFullscreenDialogBaseComponent` (`GalleryFullscreenBaseComponent` is `@deprecated`), `ConfirmationModalBaseComponent`, `NewsletterModalBaseComponent`, `MapBaseComponent`, `FilterInputBaseComponent`, `FilterDateInputBaseComponent`, `FilterCustomDictionaryInputBaseComponent`, `FilterDictionaryInputBaseComponent` (inline tree, `source`, paging), `ThemeTagsFilterBaseComponent`, `CatalogFilterBarBaseComponent`, `CatalogToolbarBaseComponent`, `SearchbarBaseComponent`, `SelectCheckboxBaseComponent` (+ `ISelectionHooks`), `DropdownBaseComponent`, `TabsBaseComponent`, `WcagBaseComponent` (+ `WCAG_OPTION_GROUPS`), `BreadcrumbsBaseComponent`, `BreadcrumbsBarBaseComponent`, `MenuItemBaseComponent`, `ObjectCardBaseComponent`, `ObjectsGridItemBaseComponent`, `ObjectsListItemBaseComponent`, `CollectionCardBaseComponent`, `CollectionsGridItemBaseComponent`, `FavoriteButtonBaseComponent` (+ `IFavoriteHooks`, `IFavoriteRef`), `FavoritesNoticeBaseComponent`, `SelectionActionsBaseComponent`, `DownloadCardsBaseComponent`, `RecommendedBaseComponent`, `WidgetImageBaseComponent`, `WidgetMediaBaseComponent`, `WidgetGalleryImageBaseComponent`, `WidgetAudioBaseComponent`, `WidgetGalleryObjectBaseComponent` (all extend `WidgetBaseComponent`) |
146
+ | SSR tools | `injectServerResponseInit`, `appendPreloadLinks` (image preload `Link` headers) |
130
147
 
131
148
  What stays in the app lib: the **template and styles** of every base above — the museum keeps its
132
149
  own HTML and Tailwind classes (the filter inputs, searchbar, catalog toolbar and filter bar, theme tags and select
@@ -11,7 +11,10 @@ WCAG-clean skin. The token **keys** are the structural contract the templates us
11
11
  as written. Step 9 swaps the values for the client's brand spec if there is one.
12
12
 
13
13
  ```js
14
- const { mobilemsContent } = require('@smartsoft001-mobilems/angular/tailwind');
14
+ const {
15
+ mobilemsContent,
16
+ mobilemsPreset,
17
+ } = require('@smartsoft001-mobilems/angular/tailwind');
15
18
 
16
19
  /** @type {import('tailwindcss').Config} */
17
20
  module.exports = {
@@ -20,8 +23,7 @@ module.exports = {
20
23
  './libs/**/*.{html,ts}',
21
24
  ...mobilemsContent,
22
25
  ],
23
- prefix: 'smart-',
24
- darkMode: ['class', '[class~="contrast-high"]'],
26
+ presets: [mobilemsPreset],
25
27
  theme: {
26
28
  extend: {
27
29
  colors: {
@@ -76,7 +78,10 @@ module.exports = {
76
78
  > needed when modules arrive. `mobilemsContent` adds the framework component templates (app shell,
77
79
  > 404, page sections, module pages): their `smart-*` classes live in `node_modules`, so without it
78
80
  > they are missing from the app CSS — the framework skip links carry their own inline styles, the
79
- > rest does not. `darkMode` keys off `contrast-high` for the WCAG high-contrast toggle.
81
+ > rest does not. `mobilemsPreset` (opt-in, no `content`, no `theme`) carries what every portal shares:
82
+ > the `smart-` `prefix`, `darkMode` keyed off `contrast-high` for the WCAG high-contrast toggle, and
83
+ > `future.hoverOnlyWhenSupported`, which wraps `hover:` in `@media (hover: hover)` so touch devices no
84
+ > longer get sticky hover states. Brand tokens stay in this file's `theme.extend`.
80
85
 
81
86
  > **Font tokens are named by role** (`heading`, `body`, `ui`), never after a typeface or a project.
82
87
  > In reference mode the copied templates may use the source project's own token names — remap those
@@ -143,6 +143,18 @@ else
143
143
  pass "workspace aliases use @$PREFIX/"
144
144
  fi
145
145
 
146
+ echo "== Portal guard =="
147
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
148
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
149
+ if npx --no-install mobilems-check-portal; then
150
+ pass "mobilems-check-portal clean"
151
+ else
152
+ bad "mobilems-check-portal found recurring portal bugs — fix the FAIL lines above"
153
+ fi
154
+ else
155
+ echo " SKIP: mobilems-check-portal unavailable in this project (older framework line)"
156
+ fi
157
+
146
158
  echo
147
159
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
148
160
  exit "$fail"
@@ -56,6 +56,18 @@ else
56
56
  echo " SKIP: mobilems-check-translations niedostępny w tym projekcie (starsza linia frameworka)"
57
57
  fi
58
58
 
59
+ echo "== Strażnik portalu =="
60
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
61
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
62
+ if npx --no-install mobilems-check-portal; then
63
+ pass "brak naruszeń strażnika portalu"
64
+ else
65
+ bad "strażnik portalu zgłosił błędy — popraw wypisane wyżej"
66
+ fi
67
+ else
68
+ echo " SKIP: mobilems-check-portal niedostępny w tym projekcie (starsza linia frameworka)"
69
+ fi
70
+
59
71
  echo
60
72
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
61
73
  exit "$fail"
@@ -53,6 +53,18 @@ else
53
53
  echo " SKIP: mobilems-check-translations niedostępny w tym projekcie (starsza linia frameworka)"
54
54
  fi
55
55
 
56
+ echo "== Strażnik portalu =="
57
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
58
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
59
+ if npx --no-install mobilems-check-portal; then
60
+ pass "brak naruszeń strażnika portalu"
61
+ else
62
+ bad "strażnik portalu zgłosił błędy — popraw wypisane wyżej"
63
+ fi
64
+ else
65
+ echo " SKIP: mobilems-check-portal niedostępny w tym projekcie (starsza linia frameworka)"
66
+ fi
67
+
56
68
  echo
57
69
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
58
70
  exit "$fail"
@@ -4,8 +4,8 @@
4
4
 
5
5
  Thread `<PREFIX>` (the `environment` + domain imports). This module REUSES `ArticlesSharedModule`
6
6
  (the same shared module the news/articles modules use) but configures it over its OWN
7
- `MuseumThematicPath` entity with its OWN `getReducer` and **no `baseQuery`**. The `Masonry` factory
8
- returns the real library in the browser (`window.Masonry`, set by `bootstrapApplication` from `@smartsoft001-mobilems/angular/bootstrap` in `main.ts`) and a no-op mock during SSR (identical to objects).
7
+ `MuseumThematicPath` entity with its OWN `getReducer` and **no `baseQuery`**. `provideBrowserMasonry()`
8
+ supplies the real library in the browser (`window.Masonry`, set by `bootstrapApplication` from `@smartsoft001-mobilems/angular/bootstrap` in `main.ts`) and `null` during SSR (identical to objects).
9
9
 
10
10
  ```ts
11
11
  import { NgModule } from '@angular/core';
@@ -13,7 +13,7 @@ import { FormsModule } from '@angular/forms';
13
13
  import { RouterModule } from '@angular/router';
14
14
  import { SharedModule as SmartSharedModule } from '@smartsoft001/angular';
15
15
  import { ArticlesSharedModule } from '@smartsoft001-mobilems/articles-shell-angular';
16
- import { Masonry } from '@thisissoon/angular-masonry';
16
+ import { provideBrowserMasonry } from '@smartsoft001-mobilems/angular';
17
17
 
18
18
  import { environment } from '@<PREFIX>/angular';
19
19
  import { MuseumThematicPath } from '@<PREFIX>/museum-thematic-paths/domain';
@@ -24,33 +24,7 @@ import { SERVICES } from './services';
24
24
  import { routes } from './thematic-paths.routes';
25
25
 
26
26
  @NgModule({
27
- providers: [
28
- ThematicPathsFacade,
29
- ...SERVICES,
30
- {
31
- provide: Masonry,
32
- useFactory: () => {
33
- if (typeof window !== 'undefined' && (window as any)['Masonry']) {
34
- return (window as any)['Masonry'];
35
- }
36
- // Return a mock for SSR
37
- return class {
38
- constructor() {
39
- // no-op
40
- }
41
- layout() {
42
- return;
43
- }
44
- reloadItems() {
45
- return;
46
- }
47
- destroy() {
48
- return;
49
- }
50
- };
51
- },
52
- },
53
- ],
27
+ providers: [ThematicPathsFacade, ...SERVICES, provideBrowserMasonry()],
54
28
  imports: [
55
29
  FormsModule,
56
30
  SmartSharedModule,
@@ -64,13 +64,22 @@ fi
64
64
  # 2. the Masonry provider exists ONLY to feed MasonryGridComponent. Providing it without ever
65
65
  # rendering the component means the grid was hand-rolled some other way (CSS columns, flex, …),
66
66
  # which the `new Masonry(` grep above cannot see.
67
- if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
67
+ if grep -rqE "provide: Masonry|provideBrowserMasonry" "$LIBDIR" 2>/dev/null; then
68
68
  if grep -rq "smart-mobilems-masonry-grid" "$LIBDIR" 2>/dev/null; then
69
69
  pass "grids render via <smart-mobilems-masonry-grid>"
70
70
  else
71
71
  bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
72
72
  fi
73
73
  fi
74
+ # 2b. the provider is provideBrowserMasonry() — an anonymous stand-in class next to
75
+ # `provide: Masonry` reaches for window during SSR and is the bug the framework removed
76
+ STUB="$(grep -rlE "provide: Masonry" "$LIBDIR" 2>/dev/null | xargs grep -lE "return class|: class \{" 2>/dev/null || true)"
77
+ if [ -n "$STUB" ]; then
78
+ bad "hand-written Masonry stub (return class / : class {) — use provideBrowserMasonry() from @smartsoft001-mobilems/angular:"
79
+ printf ' %s\n' $STUB >&2
80
+ else
81
+ pass "no hand-written Masonry stub"
82
+ fi
74
83
  # 3. a list page must render through the framework grid
75
84
  LISTP="$(find "$LIBDIR/pages" -maxdepth 1 -type d -name 'list*' 2>/dev/null || true)"
76
85
  if [ -n "$LISTP" ]; then
@@ -103,6 +112,18 @@ else
103
112
  pass "no filters component in this module"
104
113
  fi
105
114
 
115
+ echo "== Portal guard =="
116
+ # Static guard for the recurring portal bugs (MOB-1889..1895); needs @smartsoft001-mobilems/angular with the bin.
117
+ if [ -x node_modules/.bin/mobilems-check-portal ]; then
118
+ if npx --no-install mobilems-check-portal; then
119
+ pass "mobilems-check-portal clean"
120
+ else
121
+ bad "mobilems-check-portal found recurring portal bugs — fix the FAIL lines above"
122
+ fi
123
+ else
124
+ echo " SKIP: mobilems-check-portal unavailable in this project (older framework line)"
125
+ fi
126
+
106
127
  echo
107
128
  if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
108
129
  exit "$fail"