@smartsoft001-mobilems/claude-plugins 2.89.0 → 2.91.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/mobilems/.claude-plugin/plugin.json +1 -1
- package/plugins/mobilems/skills/mobilems-articles/SKILL.md +15 -6
- package/plugins/mobilems/skills/mobilems-articles/scripts/check-translations.js +90 -0
- package/plugins/mobilems/skills/mobilems-articles/scripts/verify-module.sh +10 -0
- package/plugins/mobilems/skills/mobilems-object-collections/SKILL.md +15 -5
- package/plugins/mobilems/skills/mobilems-object-collections/scripts/check-translations.js +90 -0
- package/plugins/mobilems/skills/mobilems-object-collections/scripts/verify-module.sh +17 -6
- package/plugins/mobilems/skills/mobilems-objects/SKILL.md +15 -5
- package/plugins/mobilems/skills/mobilems-objects/scripts/check-translations.js +90 -0
- package/plugins/mobilems/skills/mobilems-objects/scripts/verify-module.sh +10 -0
- package/plugins/mobilems/skills/mobilems-partners/SKILL.md +5 -1
- package/plugins/mobilems/skills/mobilems-partners/scripts/check-translations.js +90 -0
- package/plugins/mobilems/skills/mobilems-partners/scripts/verify-module.sh +10 -0
- package/plugins/mobilems/skills/mobilems-search/SKILL.md +84 -0
- package/plugins/mobilems/skills/mobilems-search/scripts/detect-search.sh +14 -0
- package/plugins/mobilems/skills/mobilems-search/scripts/verify-search.sh +61 -0
- package/plugins/mobilems/skills/mobilems-static-pages/SKILL.md +81 -0
- package/plugins/mobilems/skills/mobilems-static-pages/scripts/detect-pages.sh +16 -0
- package/plugins/mobilems/skills/mobilems-static-pages/scripts/verify-pages.sh +58 -0
package/package.json
CHANGED
|
@@ -78,11 +78,16 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
|
|
|
78
78
|
Also add any **extra third-party packages the copied pages import** that aren't already in the
|
|
79
79
|
workspace — e.g. the reference list page uses `lodash-decorators` (`@debounce`); grep the pages you
|
|
80
80
|
copy for non-`@angular`/`@smartsoft001` imports and add the missing ones.
|
|
81
|
-
3. **Library** —
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
3. **Library** — scaffold `libs/museum-news` with this package's Nx generator (deterministic — do
|
|
82
|
+
NOT copy files by hand when the generator is available):
|
|
83
|
+
```bash
|
|
84
|
+
npx nx g @smartsoft001-mobilems/articles-shell-angular:museum-module \
|
|
85
|
+
--referenceRoot=<reference-repo> --prefix=<PREFIX>
|
|
86
|
+
```
|
|
87
|
+
It copies the lib from the reference, threads `@<PREFIX>/`, adds both `tsconfig.base.json`
|
|
88
|
+
paths, normalizes `project.json` paths and copies the module's translation groups (pl+en).
|
|
89
|
+
Fallback (older framework line without the generator): do the same by hand per
|
|
90
|
+
`references/lib-contents.md`. For a NON-news article module (different `<name>`/entity), the generator's `news` output is the starting point — rename per `references/lib-contents.md` after generating, or fall back to the manual path entirely.
|
|
86
91
|
4. **Module + route** — see `references/module-and-routes.md`: `Museum<Name>Module` wraps
|
|
87
92
|
`ArticlesSharedModule.forFeature` with `type: Article`, the chosen `entity`, and
|
|
88
93
|
`baseQuery: [{ key: 'template', value: '<template>', type: '=', hidden: true }]`, plus the `Masonry`
|
|
@@ -95,7 +100,11 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
|
|
|
95
100
|
and list cards (`<name>-grid-item`, `<name>-list-item`) in `libs/shared/angular` and export them; if
|
|
96
101
|
the module renders article-gallery content blocks, also build the article gallery widget and register
|
|
97
102
|
it in the widget content-type map (build once, idempotent across article modules).
|
|
98
|
-
6. **
|
|
103
|
+
6. **Translations** — copy the module's translation groups (`NEWS` + `ARTICLES` for the aktualności instance (override with `TRANS_GROUPS` when the module uses different groups)) from the reference
|
|
104
|
+
repo's `apps/web/public/translations.json` into the app's file, for BOTH `pl` and `en`.
|
|
105
|
+
Complete group = every reference key present; every key referenced in the module's code
|
|
106
|
+
must exist in both languages — a raw `GROUP.key` rendered in the UI is a defect.
|
|
107
|
+
7. **Verify** — run `REF_ROOT=<reference-repo> scripts/verify-module.sh <workspace-root> <PREFIX> <name> [template]`: asserts the
|
|
99
108
|
library exists, the `forFeature` carries the chosen `entity` (and template, if given), the route is
|
|
100
109
|
registered, and the article card shared components are built and exported. Fix and re-run.
|
|
101
110
|
|
|
@@ -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);
|
|
@@ -11,6 +11,7 @@ ROOT="${1:?usage: verify-module.sh <workspace-root> <PREFIX> <name> [template]}"
|
|
|
11
11
|
PREFIX="${2:?usage: verify-module.sh <workspace-root> <PREFIX> <name> [template]}"
|
|
12
12
|
NAME="${3:?usage: verify-module.sh <workspace-root> <PREFIX> <name> [template]}"
|
|
13
13
|
TEMPLATE="${4:-}"
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
14
15
|
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
15
16
|
|
|
16
17
|
LIBF="museum-$NAME"
|
|
@@ -99,6 +100,15 @@ else
|
|
|
99
100
|
pass "no filters component in this module"
|
|
100
101
|
fi
|
|
101
102
|
|
|
103
|
+
echo "== Translations (REF_ROOT=${REF_ROOT:-unset}) =="
|
|
104
|
+
# Groups: present in pl+en, no dead references, complete vs the reference repo
|
|
105
|
+
# (export REF_ROOT=<reference-repo-root> to enable the completeness diff).
|
|
106
|
+
if node "$SCRIPT_DIR/check-translations.js" "${TRANS_GROUPS:-NEWS ARTICLES}" "${REF_ROOT:-}"; then
|
|
107
|
+
pass "translation groups complete"
|
|
108
|
+
else
|
|
109
|
+
bad "translation gaps — fix the FAIL lines above"
|
|
110
|
+
fi
|
|
111
|
+
|
|
102
112
|
echo
|
|
103
113
|
if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
|
|
104
114
|
exit "$fail"
|
|
@@ -50,10 +50,16 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
|
|
|
50
50
|
`@smartsoft001-mobilems/public-collections-shell-angular`,
|
|
51
51
|
`@smartsoft001-mobilems/public-collections-shell-dtos` (match the installed mobilems line).
|
|
52
52
|
Resolve versions at run time; do not `npm install` unless asked. No extra third-party viewer deps.
|
|
53
|
-
2. **Library** —
|
|
54
|
-
copy
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
57
63
|
3. **Module + routes** — see `references/module-and-routes.md`: `MuseumCollectionsModule` wraps
|
|
58
64
|
`PublicCollectionsSharedModule.forFeature({ routing: false, config: { type: MuseumCollection, apiUrl, entity: 'publicCollections' } })`
|
|
59
65
|
with the `Masonry` SSR factory and the `CrudService → CollectionsService` provider. Add the lazy
|
|
@@ -63,7 +69,11 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
|
|
|
63
69
|
`components/collections-grid-item` (it imports `public-collections-domain`) in the shared lib and
|
|
64
70
|
export it from `components/index.ts`. Collections contributes no widget-map entry and no model type
|
|
65
71
|
(those are objects-only).
|
|
66
|
-
5. **
|
|
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 +
|
|
67
77
|
`MuseumCollection` entity exist, the `forFeature` is wired, the `kolekcje` route is registered
|
|
68
78
|
(top-level + search child), the `collections-grid-item` component is built and exported, and the
|
|
69
79
|
three public-collections deps are present. Fix and re-run.
|
|
@@ -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
|
|
@@ -59,13 +60,14 @@ if grep -rq "provide: Masonry" "$LIBDIR" 2>/dev/null; then
|
|
|
59
60
|
bad "module provides Masonry but never renders <smart-mobilems-masonry-grid> — hand-rolled grid"
|
|
60
61
|
fi
|
|
61
62
|
fi
|
|
62
|
-
# 3.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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>"
|
|
67
69
|
else
|
|
68
|
-
bad "
|
|
70
|
+
bad "item page does not render <smart-mobilems-masonry-grid> (README.md#masonrygridcomponent)"
|
|
69
71
|
fi
|
|
70
72
|
fi
|
|
71
73
|
# 4. filters must EXTEND the framework base class — `context.` alone proves nothing, and a
|
|
@@ -82,6 +84,15 @@ else
|
|
|
82
84
|
pass "no filters component in this module"
|
|
83
85
|
fi
|
|
84
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
|
+
|
|
85
96
|
echo
|
|
86
97
|
if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
|
|
87
98
|
exit "$fail"
|
|
@@ -51,10 +51,16 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
|
|
|
51
51
|
`@smartsoft001-mobilems/objects-shell-dtos` (match the installed mobilems line) and
|
|
52
52
|
`online-3d-viewer` (the 3D tab; `leaflet`/`openseadragon`/`swiper` are already foundation deps).
|
|
53
53
|
Resolve versions at run time; do not `npm install` unless asked.
|
|
54
|
-
2. **Library** —
|
|
55
|
-
copy
|
|
56
|
-
|
|
57
|
-
|
|
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.
|
|
58
64
|
3. **Module + routes** — see `references/module-and-routes.md`: `MuseumObjectsModule` wraps
|
|
59
65
|
`ObjectsSharedModule.forFeature({ routing: false, config: { type: MsObject, apiUrl, entity: 'objects' } })`
|
|
60
66
|
with the `Masonry` SSR factory. Add the lazy `obiekty` route to `appRoutes` in
|
|
@@ -63,7 +69,11 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
|
|
|
63
69
|
`objects-list-item`, and the object gallery widget (`widget/gallery-object`) in the shared lib +
|
|
64
70
|
export them; register the object gallery widget in the widget content-type map (`GALLERY_OBJECT`);
|
|
65
71
|
and add `ISpecificObject` (extends `MsObject`) to `models/objects.model.ts`.
|
|
66
|
-
5. **
|
|
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
|
|
67
77
|
`obiekty` route is registered (top-level + search child), the objects shared components are built
|
|
68
78
|
and exported, the widget map registers the object gallery widget, and `ISpecificObject` is present.
|
|
69
79
|
Fix and re-run.
|
|
@@ -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);
|
|
@@ -8,6 +8,7 @@ set -eu
|
|
|
8
8
|
|
|
9
9
|
ROOT="${1:?usage: verify-module.sh <workspace-root> <PREFIX>}"
|
|
10
10
|
PREFIX="${2:?usage: verify-module.sh <workspace-root> <PREFIX>}"
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
12
|
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
12
13
|
|
|
13
14
|
fail=0
|
|
@@ -89,6 +90,15 @@ else
|
|
|
89
90
|
pass "no filters component in this module"
|
|
90
91
|
fi
|
|
91
92
|
|
|
93
|
+
echo "== Translations (REF_ROOT=${REF_ROOT:-unset}) =="
|
|
94
|
+
# Groups: present in pl+en, no dead references, complete vs the reference repo
|
|
95
|
+
# (export REF_ROOT=<reference-repo-root> to enable the completeness diff).
|
|
96
|
+
if node "$SCRIPT_DIR/check-translations.js" "OBJECTS DETAILS_PAGE GALLERY" "${REF_ROOT:-}"; then
|
|
97
|
+
pass "translation groups complete"
|
|
98
|
+
else
|
|
99
|
+
bad "translation gaps — fix the FAIL lines above"
|
|
100
|
+
fi
|
|
101
|
+
|
|
92
102
|
echo
|
|
93
103
|
if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
|
|
94
104
|
exit "$fail"
|
|
@@ -72,7 +72,11 @@ README) — never a per-module copy. `scripts/verify-module.sh` fails the run on
|
|
|
72
72
|
`partnerzy` route to the `appRoutes` array in `apps/web/src/app/app.routes.ts` as a top-level
|
|
73
73
|
route, and a matching child under the `wyszukaj/:text` search route (partners appears in site
|
|
74
74
|
search).
|
|
75
|
-
4. **
|
|
75
|
+
4. **Translations** — copy the module's translation groups (`PARTNERS`) from the reference
|
|
76
|
+
repo's `apps/web/public/translations.json` into the app's file, for BOTH `pl` and `en`.
|
|
77
|
+
Complete group = every reference key present; every key referenced in the module's code
|
|
78
|
+
must exist in both languages — a raw `GROUP.key` rendered in the UI is a defect.
|
|
79
|
+
5. **Verify** — run `REF_ROOT=<reference-repo> scripts/verify-module.sh <workspace-root> <PREFIX>`: asserts the lib + entity
|
|
76
80
|
exist, both tsconfig paths are added, `ArticlesSharedModule.forFeature` is wired with
|
|
77
81
|
`entity: 'partners'` + `reducerFactory: getReducer`, the `+state/partners.reducer.ts` exports
|
|
78
82
|
`getReducer`, the `components/{grid-item,list-item}` cards are present in the partners lib, the
|
|
@@ -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);
|
|
@@ -10,6 +10,7 @@ set -eu
|
|
|
10
10
|
|
|
11
11
|
ROOT="${1:?usage: verify-module.sh <workspace-root> <PREFIX>}"
|
|
12
12
|
PREFIX="${2:?usage: verify-module.sh <workspace-root> <PREFIX>}"
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
14
|
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
14
15
|
|
|
15
16
|
fail=0
|
|
@@ -97,6 +98,15 @@ else
|
|
|
97
98
|
pass "no filters component in this module"
|
|
98
99
|
fi
|
|
99
100
|
|
|
101
|
+
echo "== Translations (REF_ROOT=${REF_ROOT:-unset}) =="
|
|
102
|
+
# Groups: present in pl+en, no dead references, complete vs the reference repo
|
|
103
|
+
# (export REF_ROOT=<reference-repo-root> to enable the completeness diff).
|
|
104
|
+
if node "$SCRIPT_DIR/check-translations.js" "PARTNERS" "${REF_ROOT:-}"; then
|
|
105
|
+
pass "translation groups complete"
|
|
106
|
+
else
|
|
107
|
+
bad "translation gaps — fix the FAIL lines above"
|
|
108
|
+
fi
|
|
109
|
+
|
|
100
110
|
echo
|
|
101
111
|
if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
|
|
102
112
|
exit "$fail"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobilems-search
|
|
3
|
+
description: Style and wire the MobilEMS global search results page (`/wyszukaj/:text`) to a museum's approved mockup — the tabbed results view over objects, collections and news, plus the searchbar that feeds it. Use whenever the user wants to build, restyle or fix search, the results tabs, the searchbar, result cards or pagination, or asks to "zrób wyszukiwarkę", "ostyluj wyniki wyszukiwania", "popraw wyszukiwanie globalne", "dodaj zakładkę wyników". The results page ships with the project skeleton, so this skill is about the design, the per-module tabs and the interaction details — not about creating a feature library.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MobilEMS global search
|
|
9
|
+
|
|
10
|
+
Bring the search results page up to the museum's mockup and make its tabs
|
|
11
|
+
actually work. The page (`apps/web/src/app/pages/search`) and its route come
|
|
12
|
+
with the skeleton; what varies per museum is the styling and which feature
|
|
13
|
+
modules contribute tabs.
|
|
14
|
+
|
|
15
|
+
Search is the page where cross-module wiring goes wrong most often, because a
|
|
16
|
+
tab is owned by the module that renders it while the frame is owned by the app.
|
|
17
|
+
Both sides have to hold up their end.
|
|
18
|
+
|
|
19
|
+
## Precondition
|
|
20
|
+
|
|
21
|
+
Run `scripts/detect-search.sh <workspace-root>` — confirms the results page and
|
|
22
|
+
the `wyszukaj/:text` route exist. If they do not, the project predates the
|
|
23
|
+
skeleton; build the foundation with `mobilems-project-init` first.
|
|
24
|
+
|
|
25
|
+
## Framework first — read before writing components
|
|
26
|
+
|
|
27
|
+
1. `packages/shared/angular/README.md` (installed as
|
|
28
|
+
`node_modules/@smartsoft001-mobilems/angular/README.md`)
|
|
29
|
+
2. `libs/shared/angular/README.md` — the app's own shared lib
|
|
30
|
+
|
|
31
|
+
| Do not hand-roll | Use |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| a tab strip | `TabsComponent` / `Tab` from the shared lib (the page already imports it) |
|
|
34
|
+
| result cards | the module's own list-item components (`objects-list-item`, news/collection cards) |
|
|
35
|
+
| "next page" behaviour | the shared `NextPageComponent`, same pattern as the module lists |
|
|
36
|
+
| a search input | `app-objects-searchbar` / the shared searchbar variants |
|
|
37
|
+
|
|
38
|
+
Forking a result card for search is the classic mistake: the card then drifts
|
|
39
|
+
from the module's list and the museum ends up with two visually different
|
|
40
|
+
representations of the same object.
|
|
41
|
+
|
|
42
|
+
## Steps
|
|
43
|
+
|
|
44
|
+
1. **Read the mockup** in `docs/mockups/` — the searchbar and the result list
|
|
45
|
+
item are usually exported as separate design-system components rather than a
|
|
46
|
+
full screen, so match the component and keep the page layout consistent with
|
|
47
|
+
the rest of the portal.
|
|
48
|
+
2. **Tabs come from modules.** Each feature module registers a child route under
|
|
49
|
+
`wyszukaj/:text` (its search module) and the page builds its tab list from
|
|
50
|
+
what is registered. When a tab is missing, fix it in that module's skill
|
|
51
|
+
scope, not by hardcoding the tab here.
|
|
52
|
+
3. **Direct navigation to a tab must work.** Opening `/wyszukaj/wazy/aktualnosci`
|
|
53
|
+
straight from a link — not by clicking through — has to land on that tab with
|
|
54
|
+
results loaded. This broke repeatedly (MOB-1052…1056) because the active tab
|
|
55
|
+
was derived from click state instead of the URL.
|
|
56
|
+
4. **Interaction details that the audit checks:** debounce the full-text input
|
|
57
|
+
by 250 ms (`@debounce(250)` from `lodash-decorators`) so every keystroke does
|
|
58
|
+
not hit the API; give the results page its own `title`; keep pagination
|
|
59
|
+
targets at the WCAG 2.5.8 minimum size.
|
|
60
|
+
5. **Translations.** Labels, tab names, the result counter and the empty state
|
|
61
|
+
all go through translation keys in both `pl` and `en`; run
|
|
62
|
+
`npm run check:translations` (or `npx mobilems-check-translations`).
|
|
63
|
+
The results page invents new keys more often than other pages — it is the one
|
|
64
|
+
place where a phrase like "found N results" has no counterpart elsewhere.
|
|
65
|
+
6. **Verify** — `scripts/verify-search.sh <workspace-root>`: route, tab wiring,
|
|
66
|
+
debounce, framework components, translation coverage.
|
|
67
|
+
|
|
68
|
+
## Known pitfalls
|
|
69
|
+
|
|
70
|
+
- **Empty query and no-results states.** Both must render something deliberate;
|
|
71
|
+
a blank page reads as a broken site.
|
|
72
|
+
- **Result counts per tab.** If the mockup shows counts, they have to come from
|
|
73
|
+
the facade's total, not from the length of the loaded page.
|
|
74
|
+
- **Keyboard path.** Type, press Enter, arrow through tabs, reach a result and
|
|
75
|
+
open it — all without a mouse. Search is the most keyboard-driven page in the
|
|
76
|
+
portal.
|
|
77
|
+
- **Phrase in the heading.** Show the searched phrase, escaped — museum
|
|
78
|
+
collections contain quotes and angle brackets in titles.
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- This skill owns the search frame and its styling. A missing or broken tab
|
|
83
|
+
belongs to the feature module that should register it.
|
|
84
|
+
- Bundled `scripts/` ship automatically. Do not hand-edit `plugin.json`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# detect-search.sh <workspace-root>
|
|
4
|
+
#
|
|
5
|
+
# Precondition for mobilems-search: the skeleton's results page and route exist.
|
|
6
|
+
set -eu
|
|
7
|
+
ROOT="${1:?usage: detect-search.sh <workspace-root>}"
|
|
8
|
+
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
9
|
+
|
|
10
|
+
miss() { echo "BRAK FUNDAMENTU: $1 — uruchom najpierw /mobilems:project-init" >&2; exit 3; }
|
|
11
|
+
|
|
12
|
+
[ -f apps/web/src/app/pages/search/search.component.ts ] || miss "pages/search/search.component.ts"
|
|
13
|
+
grep -qE "path: *['\"]wyszukaj" apps/web/src/app/app.routes.ts || miss "trasa wyszukaj w app.routes.ts"
|
|
14
|
+
echo "OK: szkielet wyszukiwarki obecny"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# verify-search.sh <workspace-root>
|
|
4
|
+
#
|
|
5
|
+
# Post-condition for mobilems-search: route + tabs from modules, URL-driven
|
|
6
|
+
# active tab, debounce, framework components, translations. bash 3.2 safe.
|
|
7
|
+
# Env: REF_ROOT=<reference-repo> enables the completeness diff.
|
|
8
|
+
set -eu
|
|
9
|
+
ROOT="${1:?usage: verify-search.sh <workspace-root>}"
|
|
10
|
+
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
11
|
+
|
|
12
|
+
fail=0
|
|
13
|
+
pass() { echo " PASS: $1"; }
|
|
14
|
+
bad() { echo " FAIL: $1" >&2; fail=1; }
|
|
15
|
+
|
|
16
|
+
ROUTES=apps/web/src/app/app.routes.ts
|
|
17
|
+
SEARCH=apps/web/src/app/pages/search
|
|
18
|
+
|
|
19
|
+
echo "== Trasa i zakładki =="
|
|
20
|
+
grep -qE "path: *['\"]wyszukaj" "$ROUTES" && pass "trasa wyszukaj" || bad "brak trasy wyszukaj"
|
|
21
|
+
# Zakładki rejestrują moduły dziedzinowe jako dzieci wyszukaj/:text.
|
|
22
|
+
# Moduły wnoszą zakładki jako *SearchModule — liczymy je bez zgadywania,
|
|
23
|
+
# gdzie dokładnie kończy się blok trasy (formatowanie bywa różne).
|
|
24
|
+
TABS="$(grep -cE "[A-Za-z]+SearchModule" "$ROUTES" || true)"
|
|
25
|
+
[ "${TABS:-0}" -ge 1 ] && pass "moduły rejestrują zakładki wyników ($TABS)" || bad "żaden moduł nie rejestruje zakładki pod wyszukaj/:text"
|
|
26
|
+
|
|
27
|
+
echo "== Aktywna zakładka z URL, nie ze stanu kliknięcia =="
|
|
28
|
+
# Bug MOB-1052…1056: wejście linkiem prosto na zakładkę nie działało.
|
|
29
|
+
if grep -rqE "ActivatedRoute|router\.events|url" "$SEARCH"/search.component.ts 2>/dev/null; then
|
|
30
|
+
pass "komponent czyta zakładkę z routingu"
|
|
31
|
+
else
|
|
32
|
+
bad "zakładka nie jest wyprowadzana z URL — wejście bezpośrednie się zepsuje"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo "== Komponenty frameworka =="
|
|
36
|
+
grep -rq "TabsComponent" "$SEARCH" 2>/dev/null && pass "zakładki przez TabsComponent" || bad "własny pasek zakładek zamiast TabsComponent"
|
|
37
|
+
FORK="$(grep -rlE "class .*(SearchResultItem|SearchCard)Component" apps/web/src libs 2>/dev/null || true)"
|
|
38
|
+
if [ -n "$FORK" ]; then
|
|
39
|
+
bad "własny kafel wyników zamiast komponentu modułu (rozjedzie się z listą):"
|
|
40
|
+
printf ' %s\n' $FORK >&2
|
|
41
|
+
else
|
|
42
|
+
pass "wyniki renderowane komponentami modułów"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo "== Interakcja =="
|
|
46
|
+
grep -rq "debounce" apps/web/src libs 2>/dev/null && pass "debounce wyszukiwania obecny" || bad "brak debounce — każde naciśnięcie klawisza uderza w API"
|
|
47
|
+
|
|
48
|
+
echo "== Translacje =="
|
|
49
|
+
if npx --no-install mobilems-check-translations --help >/dev/null 2>&1 || [ -x node_modules/.bin/mobilems-check-translations ]; then
|
|
50
|
+
if npx --no-install mobilems-check-translations ${REF_ROOT:+--reference "$REF_ROOT"}; then
|
|
51
|
+
pass "translacje bez martwych referencji"
|
|
52
|
+
else
|
|
53
|
+
bad "braki w translacjach — popraw wypisane wyżej"
|
|
54
|
+
fi
|
|
55
|
+
else
|
|
56
|
+
echo " SKIP: mobilems-check-translations niedostępny w tym projekcie (starsza linia frameworka)"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
echo
|
|
60
|
+
if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
|
|
61
|
+
exit "$fail"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mobilems-static-pages
|
|
3
|
+
description: Style and wire the MobilEMS static CMS pages (`/strona/:id`, `/strona/:id/:slug`, `/kontakt`) to a museum's approved mockup — the About / Accessibility statement / Privacy policy / Contact pages every portal has. Use whenever the user wants to build, restyle or fix the static pages, the CMS page template, the contact page, or asks to "zrób strony statyczne", "podłącz strony z CMS", "ostyluj stronę Kontakt", "strony O nas / deklaracja dostępności / polityka prywatności". The page scaffolding ships with the project skeleton, so this skill is about making it match the design and render real CMS content — not about creating a feature library.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# MobilEMS static pages
|
|
9
|
+
|
|
10
|
+
Bring the museum's static pages up to its approved mockup. Unlike the feature
|
|
11
|
+
modules, there is nothing to scaffold: `apps/web/src/app/pages/page` (CMS page
|
|
12
|
+
template + resolver) and `pages/contact` come with the skeleton, and their
|
|
13
|
+
routes are already registered. What differs per museum is the design and the
|
|
14
|
+
CMS wiring — which is exactly what tends to be missing when these pages are
|
|
15
|
+
built ad hoc.
|
|
16
|
+
|
|
17
|
+
## Precondition
|
|
18
|
+
|
|
19
|
+
Run `scripts/detect-pages.sh <workspace-root>`. It confirms the skeleton pages
|
|
20
|
+
and their routes exist. If they do not, the project predates the skeleton that
|
|
21
|
+
ships them — build them from `mobilems-project-init` first rather than
|
|
22
|
+
hand-rolling here, so every museum keeps the same structure.
|
|
23
|
+
|
|
24
|
+
## Framework first — read before writing components
|
|
25
|
+
|
|
26
|
+
Static pages are mostly composition, so almost everything you need already
|
|
27
|
+
exists. Re-implementing it is a defect, not a shortcut:
|
|
28
|
+
|
|
29
|
+
1. `packages/shared/angular/README.md` (installed as
|
|
30
|
+
`node_modules/@smartsoft001-mobilems/angular/README.md`) — the framework API catalog
|
|
31
|
+
2. `libs/shared/angular/README.md` — the app's own shared lib
|
|
32
|
+
|
|
33
|
+
| Do not hand-roll | Use |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| a rich-text renderer for CMS body HTML | the page template's existing content block + widget map |
|
|
36
|
+
| per-page galleries / sliders | the widget content-type components (`WIDGET` group) |
|
|
37
|
+
| menu and footer links | the API menu configuration (see pitfalls — hardcoding them was a reported bug) |
|
|
38
|
+
| meta/OG tags | `SeoService` / `MetaService` |
|
|
39
|
+
|
|
40
|
+
## Steps
|
|
41
|
+
|
|
42
|
+
1. **Read the mockup.** Exports live in `docs/mockups/`; read them (Read) rather
|
|
43
|
+
than guessing from the Figma link. Note the container width, heading scale,
|
|
44
|
+
spacing rhythm and how the page header/hero differs from the module pages.
|
|
45
|
+
2. **CMS data — render everything the API sends.** The page payload carries more
|
|
46
|
+
than a title and body: `image`, `backgroundImage`, `links`, `slides`.
|
|
47
|
+
A field that arrives and is not rendered reads to the client as a missing
|
|
48
|
+
feature, so walk the resolver's payload and account for each field.
|
|
49
|
+
3. **Contact page.** The contact page's CMS id is per-instance, so the `kontakt`
|
|
50
|
+
route resolves it through the menu rather than a constant. Keep it that way —
|
|
51
|
+
a hardcoded id works on one museum and breaks on the next.
|
|
52
|
+
4. **Translations.** Every static label goes through a key in the `CONTACT`
|
|
53
|
+
group (plus `COMMON` where it is genuinely shared); no Polish literals in
|
|
54
|
+
templates, and both `pl` and `en` filled. Run `npm run check:translations`
|
|
55
|
+
(or `npx mobilems-check-translations`) — a raw `CONTACT.something` on screen
|
|
56
|
+
is one of the most visible defects a user can hit.
|
|
57
|
+
5. **Verify** — `scripts/verify-pages.sh <workspace-root>`: asserts the routes,
|
|
58
|
+
the resolver wiring, translation coverage and that no menu/footer link is
|
|
59
|
+
hardcoded. Fix and re-run.
|
|
60
|
+
|
|
61
|
+
## Known pitfalls (fix while styling, not after)
|
|
62
|
+
|
|
63
|
+
- **Hardcoded navigation.** Footer and menu entries come from the API config.
|
|
64
|
+
Two separate bugs (MOB-1044, MOB-1071) traced back to links pasted into
|
|
65
|
+
templates — they render fine on the museum you are looking at and 404 on the
|
|
66
|
+
next one.
|
|
67
|
+
- **One `h1` per page.** The CMS title is the `h1`; a second one in the hero or
|
|
68
|
+
breadcrumb is a WCAG 1.3.1 violation that the audit will catch.
|
|
69
|
+
- **360 px without horizontal scroll.** Long CMS words and wide embedded media
|
|
70
|
+
are the usual culprits; test the narrow viewport before calling it done.
|
|
71
|
+
- **Dark / high-contrast mode.** The page renders client HTML, so contrast
|
|
72
|
+
overrides must reach nested CMS markup (`[&_p]`, `[&_a]`, headings) — checking
|
|
73
|
+
only the page chrome misses the body text.
|
|
74
|
+
- **Empty states.** A CMS page with no body, no image or no links must still
|
|
75
|
+
render a sane layout; museums publish half-filled pages more often than not.
|
|
76
|
+
|
|
77
|
+
## Notes
|
|
78
|
+
|
|
79
|
+
- This skill touches the static pages only. Feature modules (objects,
|
|
80
|
+
collections, news) have their own skills and their own libraries.
|
|
81
|
+
- Bundled `scripts/` ship automatically. Do not hand-edit `plugin.json`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# detect-pages.sh <workspace-root>
|
|
4
|
+
#
|
|
5
|
+
# Precondition for mobilems-static-pages: the skeleton's static pages and their
|
|
6
|
+
# routes exist. bash 3.2 safe.
|
|
7
|
+
set -eu
|
|
8
|
+
ROOT="${1:?usage: detect-pages.sh <workspace-root>}"
|
|
9
|
+
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
10
|
+
|
|
11
|
+
miss() { echo "BRAK FUNDAMENTU: $1 — uruchom najpierw /mobilems:project-init" >&2; exit 3; }
|
|
12
|
+
|
|
13
|
+
[ -f apps/web/src/app/pages/page/page.component.ts ] || miss "pages/page/page.component.ts"
|
|
14
|
+
[ -f apps/web/src/app/pages/page/page.resolver.ts ] || miss "pages/page/page.resolver.ts"
|
|
15
|
+
grep -q "strona/:id" apps/web/src/app/app.routes.ts || miss "trasa strona/:id w app.routes.ts"
|
|
16
|
+
echo "OK: szkielet stron statycznych obecny"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# verify-pages.sh <workspace-root>
|
|
4
|
+
#
|
|
5
|
+
# Post-condition for mobilems-static-pages: routes wired, CMS payload rendered,
|
|
6
|
+
# navigation not hardcoded, translations complete. bash 3.2 safe.
|
|
7
|
+
# Env: REF_ROOT=<reference-repo> enables the completeness diff.
|
|
8
|
+
set -eu
|
|
9
|
+
ROOT="${1:?usage: verify-pages.sh <workspace-root>}"
|
|
10
|
+
cd "$ROOT" 2>/dev/null || { echo "ERROR: no such dir: $ROOT" >&2; exit 2; }
|
|
11
|
+
|
|
12
|
+
fail=0
|
|
13
|
+
pass() { echo " PASS: $1"; }
|
|
14
|
+
bad() { echo " FAIL: $1" >&2; fail=1; }
|
|
15
|
+
|
|
16
|
+
ROUTES=apps/web/src/app/app.routes.ts
|
|
17
|
+
PAGE=apps/web/src/app/pages/page
|
|
18
|
+
|
|
19
|
+
echo "== Trasy =="
|
|
20
|
+
grep -q "strona/:id" "$ROUTES" && pass "trasa strona/:id" || bad "brak trasy strona/:id"
|
|
21
|
+
grep -q "strona/:id/:slug" "$ROUTES" && pass "trasa strona/:id/:slug (SEO slug)" || bad "brak trasy ze slugiem"
|
|
22
|
+
grep -qE "path: *'kontakt'" "$ROUTES" && pass "trasa kontakt" || bad "brak trasy kontakt"
|
|
23
|
+
grep -q "pageResolver" "$ROUTES" && pass "resolver podpięty do trasy" || bad "trasa bez pageResolver"
|
|
24
|
+
|
|
25
|
+
echo "== Treść z CMS =="
|
|
26
|
+
# Pola, których pominięcie klient odbiera jako brak funkcji.
|
|
27
|
+
for field in image links; do
|
|
28
|
+
grep -rq "$field" "$PAGE" 2>/dev/null && pass "szablon używa pola $field" || bad "pole $field z CMS nigdzie nierenderowane"
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
echo "== Nawigacja z konfiguracji, nie z kodu =="
|
|
32
|
+
# Twarde ścieżki do stron CMS w szablonach = bug MOB-1044/MOB-1071.
|
|
33
|
+
HARD="$(grep -rlE "routerLink=\"/strona/[0-9]" apps/web/src libs 2>/dev/null || true)"
|
|
34
|
+
if [ -n "$HARD" ]; then
|
|
35
|
+
bad "twarde linki do stron CMS (id jest per-instancja):"
|
|
36
|
+
printf ' %s\n' $HARD >&2
|
|
37
|
+
else
|
|
38
|
+
pass "brak twardych linków do stron CMS"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "== Dostępność =="
|
|
42
|
+
H1="$(grep -rc "<h1" "$PAGE"/*.html 2>/dev/null | awk -F: '{s+=$2} END {print s+0}')"
|
|
43
|
+
[ "${H1:-0}" -le 1 ] && pass "co najwyżej jeden h1 w szablonie strony" || bad "kilka h1 w szablonie strony ($H1) — WCAG 1.3.1"
|
|
44
|
+
|
|
45
|
+
echo "== Translacje =="
|
|
46
|
+
if npx --no-install mobilems-check-translations --help >/dev/null 2>&1 || [ -x node_modules/.bin/mobilems-check-translations ]; then
|
|
47
|
+
if npx --no-install mobilems-check-translations --groups "CONTACT" ${REF_ROOT:+--reference "$REF_ROOT"}; then
|
|
48
|
+
pass "grupa CONTACT kompletna"
|
|
49
|
+
else
|
|
50
|
+
bad "braki w translacjach — popraw wypisane wyżej"
|
|
51
|
+
fi
|
|
52
|
+
else
|
|
53
|
+
echo " SKIP: mobilems-check-translations niedostępny w tym projekcie (starsza linia frameworka)"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
echo
|
|
57
|
+
if [ "$fail" = 0 ]; then echo "VERIFY: all checks passed."; else echo "VERIFY: FAILED — fix the items above and re-run." >&2; fi
|
|
58
|
+
exit "$fail"
|