@salesforce/afv-skills 1.46.0 → 1.47.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 (159) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-observe/SKILL.md +32 -4
  3. package/skills/agentforce-observe/references/ahm-alerts.md +719 -0
  4. package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +275 -0
  5. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/README.md +32 -0
  6. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls +75 -0
  7. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/SetCommentValue.cls-meta.xml +5 -0
  8. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/interview-answers.json +13 -0
  9. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/copy.json +10 -0
  10. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/create.json +20 -0
  11. package/skills/consumer-goods-promotion-bo-api-deploy/assets/set-comment-value/payloads/update.json +16 -0
  12. package/skills/consumer-goods-promotion-bo-api-deploy/references/conventions-and-payload-rules.md +273 -0
  13. package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +236 -0
  14. package/skills/consumer-goods-promotion-bo-api-deploy/references/reference-example-set-comment-value.md +132 -0
  15. package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +211 -0
  16. package/skills/dx-devops-project-manage/SKILL.md +197 -0
  17. package/skills/dx-devops-project-manage/examples/common-workflows.md +197 -0
  18. package/skills/dx-devops-project-manage/references/cli-commands.md +295 -0
  19. package/skills/dx-devops-project-manage/scripts/create-project.sh +48 -0
  20. package/skills/dx-devops-project-manage/scripts/list-projects.sh +51 -0
  21. package/skills/dx-devops-project-manage/scripts/update-project.sh +96 -0
  22. package/skills/education-cloud-academic-calendar-generate/SKILL.md +225 -0
  23. package/skills/education-cloud-academic-calendar-generate/examples/quarter-calendar.json +47 -0
  24. package/skills/education-cloud-academic-calendar-generate/examples/sample-output.md +57 -0
  25. package/skills/education-cloud-academic-calendar-generate/examples/semester-calendar.json +54 -0
  26. package/skills/education-cloud-academic-calendar-generate/references/calendar-systems.md +127 -0
  27. package/skills/education-cloud-academic-calendar-generate/references/date-validation.md +222 -0
  28. package/skills/education-cloud-academic-calendar-generate/references/foundation_prerequisites.md +40 -0
  29. package/skills/education-cloud-academic-calendar-generate/scripts/validate_calendar_dates.py +143 -0
  30. package/skills/education-cloud-course-catalog-migrate/SKILL.md +321 -0
  31. package/skills/education-cloud-course-catalog-migrate/references/gotchas-detail.md +16 -0
  32. package/skills/education-cloud-course-catalog-migrate/references/gotchas.md +16 -0
  33. package/skills/education-cloud-course-catalog-migrate/references/large-catalog-handling.md +42 -0
  34. package/skills/education-cloud-course-catalog-migrate/scripts/batch_courses.py +36 -0
  35. package/skills/education-cloud-course-catalog-migrate/scripts/detect_linked_courses.py +51 -0
  36. package/skills/education-cloud-course-catalog-migrate/scripts/detect_modality_variants.py +48 -0
  37. package/skills/education-cloud-course-catalog-migrate/scripts/resolve_api_version.py +43 -0
  38. package/skills/education-cloud-course-catalog-migrate/scripts/split_course_code.py +39 -0
  39. package/skills/education-cloud-course-catalog-migrate/scripts/validate_completeness.py +54 -0
  40. package/skills/education-cloud-multi-campus-configure/references/foundation_prerequisites.md +3 -5
  41. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +177 -0
  42. package/skills/education-cloud-student-recruitment-agent-configure/references/agent-and-subagents.md +151 -0
  43. package/skills/education-cloud-student-recruitment-agent-configure/references/customer-narration.md +34 -0
  44. package/skills/education-cloud-student-recruitment-agent-configure/references/execution-model.md +54 -0
  45. package/skills/education-cloud-student-recruitment-agent-configure/references/flows.md +82 -0
  46. package/skills/education-cloud-student-recruitment-agent-configure/references/grounding.md +199 -0
  47. package/skills/education-cloud-student-recruitment-agent-configure/references/permissions.md +183 -0
  48. package/skills/education-cloud-student-recruitment-agent-configure/references/platform-enablement.md +82 -0
  49. package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +158 -0
  50. package/skills/education-cloud-student-recruitment-agent-configure/references/routing.md +141 -0
  51. package/skills/experience-cms-brand-apply/SKILL.md +5 -5
  52. package/skills/experience-cms-brand-create/SKILL.md +2 -2
  53. package/skills/experience-cms-content-generate/SKILL.md +1 -0
  54. package/skills/experience-cms-content-render/SKILL.md +173 -0
  55. package/skills/experience-cms-content-render/assets/angular/DetailPage.component.ts +25 -0
  56. package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +133 -0
  57. package/skills/experience-cms-content-render/assets/angular/TypeList.component.ts +38 -0
  58. package/skills/experience-cms-content-render/assets/angular/TypeRenderer.component.ts +90 -0
  59. package/skills/experience-cms-content-render/assets/angular/cms-content.component.ts +248 -0
  60. package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +100 -0
  61. package/skills/experience-cms-content-render/assets/react/DetailPage.tsx +20 -0
  62. package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +129 -0
  63. package/skills/experience-cms-content-render/assets/react/TypeList.tsx +40 -0
  64. package/skills/experience-cms-content-render/assets/react/TypeRenderer.tsx +64 -0
  65. package/skills/experience-cms-content-render/assets/react/heuristicRenderer.tsx +310 -0
  66. package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +129 -0
  67. package/skills/experience-cms-content-render/assets/shared/cmsContentType.ts +49 -0
  68. package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +96 -0
  69. package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +55 -0
  70. package/skills/experience-cms-content-render/references/bulk-loading.md +60 -0
  71. package/skills/experience-cms-content-render/references/codegen-guardrails.md +111 -0
  72. package/skills/experience-cms-content-render/references/detail-pages.md +87 -0
  73. package/skills/experience-cms-content-render/references/embed-recipes.md +127 -0
  74. package/skills/experience-cms-content-render/references/failure-modes.md +96 -0
  75. package/skills/experience-cms-content-render/references/heuristic-render-rules.md +131 -0
  76. package/skills/experience-cms-content-render/references/init-scaffold.md +122 -0
  77. package/skills/experience-cms-content-render/references/interaction-model.md +173 -0
  78. package/skills/experience-cms-content-render/references/package-api.md +106 -0
  79. package/skills/experience-cms-content-render/references/schema-sync.md +114 -0
  80. package/skills/experience-cms-content-render/references/styling-scopes.md +65 -0
  81. package/skills/experience-cms-content-render/references/verify.md +49 -0
  82. package/skills/experience-cms-content-type-generate/SKILL.md +2 -2
  83. package/skills/experience-content-media-stock-image-search/SKILL.md +5 -4
  84. package/skills/experience-search-coordinate/SKILL.md +198 -0
  85. package/skills/experience-search-coordinate/assets/search-payload-template.json +25 -0
  86. package/skills/experience-search-coordinate/references/content-route.md +313 -0
  87. package/skills/experience-search-coordinate/references/content-type-discovery.md +57 -0
  88. package/skills/experience-search-coordinate/references/media-route.md +172 -0
  89. package/skills/experience-search-coordinate/references/scope-resolution.md +14 -0
  90. package/skills/experience-ui-bundle-localize/SKILL.md +1 -1
  91. package/skills/experience-ui-bundle-localize/references/i18n-setup.md +5 -3
  92. package/skills/experience-ui-bundle-project-generate/SKILL.md +18 -14
  93. package/skills/experience-ui-bundle-project-generate/references/angular-project-generate.md +22 -0
  94. package/skills/experience-ui-bundle-project-generate/references/react-project-generate.md +20 -0
  95. package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +58 -54
  96. package/skills/experience-ui-bundle-salesforce-data-access/references/caching.md +6 -0
  97. package/skills/experience-ui-bundle-salesforce-data-access/references/graphiti-cli.md +2 -2
  98. package/skills/experience-ui-bundle-salesforce-data-access/references/migration.md +6 -0
  99. package/skills/experience-ui-bundle-salesforce-data-access/references/rest-and-integration.md +2 -1
  100. package/skills/experience-ui-bundle-salesforce-data-access/references/sdk-api.md +6 -0
  101. package/skills/experience-ui-bundle-site-generate/SKILL.md +59 -8
  102. package/skills/experience-ui-bundle-site-generate/references/configure-metadata-digital-experience.md +8 -3
  103. package/skills/experience-ui-bundle-site-generate/references/configure-metadata-language-settings.md +120 -0
  104. package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +336 -0
  105. package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +143 -0
  106. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +127 -0
  107. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-deploy-commands.md +116 -0
  108. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +111 -0
  109. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-overview.md +312 -0
  110. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +171 -0
  111. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-state-tracking.md +64 -0
  112. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-2-starter-config-trigger-handlers.md +122 -0
  113. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-overview.md +335 -0
  114. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-4-user-provisioning-user-provisioning-details.md +140 -0
  115. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +196 -0
  116. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-metadata-cache-generation.md +155 -0
  117. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +307 -0
  118. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +211 -0
  119. package/skills/life-sciences-fieldsalesrep-coordinate/references/state-machine-and-changes.md +108 -0
  120. package/skills/life-sciences-kam-coordinate/SKILL.md +241 -0
  121. package/skills/life-sciences-kam-coordinate/references/orchestration-flow.md +152 -0
  122. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-application-flexipage-mapping.md +79 -0
  123. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-deploy-commands.md +131 -0
  124. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-kam-config-records.md +85 -0
  125. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-lifesci-metadata-deploy.md +112 -0
  126. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-overview.md +202 -0
  127. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-profile-layout-assignments.md +67 -0
  128. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-state-tracking.md +65 -0
  129. package/skills/life-sciences-kam-coordinate/references/stage-2-starter-config-trigger-handlers.md +123 -0
  130. package/skills/life-sciences-kam-coordinate/references/stage-4-participant-role-and-sprint.md +89 -0
  131. package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +337 -0
  132. package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +248 -0
  133. package/skills/life-sciences-kam-coordinate/references/stage-6-ipad-validation-script.md +35 -0
  134. package/skills/life-sciences-kam-coordinate/references/stage-6-metadata-cache-generation.md +155 -0
  135. package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-details.md +146 -0
  136. package/skills/life-sciences-kam-coordinate/references/stage-6-user-provisioning-overview.md +89 -0
  137. package/skills/life-sciences-kam-coordinate/references/state-machine-and-changes.md +114 -0
  138. package/skills/life-sciences-prerequisites-validate/SKILL.md +138 -0
  139. package/skills/life-sciences-prerequisites-validate/references/checks-org-settings.md +190 -0
  140. package/skills/life-sciences-prerequisites-validate/references/checks-user-and-package.md +211 -0
  141. package/skills/life-sciences-territory-configure/SKILL.md +217 -0
  142. package/skills/life-sciences-territory-configure/references/territory-metadata.md +262 -0
  143. package/skills/platform-dsar-policy-manage/SKILL.md +272 -0
  144. package/skills/platform-dsar-policy-manage/references/configure.md +106 -0
  145. package/skills/platform-dsar-policy-manage/references/export-and-history.md +123 -0
  146. package/skills/platform-dsar-policy-manage/references/gap-analysis-guide.md +150 -0
  147. package/skills/platform-dsar-policy-manage/references/gap-scan.md +129 -0
  148. package/skills/platform-dsar-policy-manage/references/headless-sor.md +59 -0
  149. package/skills/platform-dsar-policy-manage/references/report-format.md +59 -0
  150. package/skills/platform-dsar-policy-manage/scripts/tests/__init__.py +0 -0
  151. package/skills/platform-dsar-policy-manage/scripts/tests/test_validate_policy_tree.py +76 -0
  152. package/skills/platform-dsar-policy-manage/scripts/validate-policy-tree.py +130 -0
  153. package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +359 -0
  154. package/skills/platform-salesforce-connect-adapter-generate/references/official-examples.md +69 -0
  155. package/skills/platform-salesforce-connect-adapter-generate/references/scenarios.md +187 -0
  156. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +20 -27
  157. package/skills/service-native-voice-recording-transcription-configure/SKILL.md +47 -27
  158. package/skills/service-native-voice-recording-transcription-configure/references/thunderbird-voice-settings.md +13 -9
  159. package/skills/service-native-voice-recording-transcription-configure/scripts/enable-recording-transcription.sh +104 -45
@@ -0,0 +1,248 @@
1
+ /**
2
+ * CmsContentComponent — schema-less body renderer for Angular (Init →
3
+ * src/cms/angular/cms-content.component.ts). Counterpart of React's heuristicRenderer.
4
+ * Picks fields by priority (title/image/excerpt) or a `fields` whitelist, then
5
+ * classifies each by shape. Holds the ONE RichText injection path (Rule 4): decode,
6
+ * then `[innerHTML]` — Angular's DomSanitizer sanitizes it, so NEVER
7
+ * bypassSecurityTrustHtml. Rules: references/heuristic-render-rules.md.
8
+ */
9
+ import { Component, Input, type OnChanges } from '@angular/core';
10
+ import { CommonModule } from '@angular/common';
11
+ import {
12
+ decodeRichHtmlEntities,
13
+ resolveCmsImageUrl,
14
+ } from '@salesforce/ui-bundle-template-feature-cms-toolkit';
15
+ import type { CmsImageField } from '../shared/cmsCore.types';
16
+
17
+ const IMAGE_PRIORITY = ['bannerImage', 'featuredImage', 'heroImage', 'coverImage', 'thumbnail', 'image'];
18
+ const EXCERPT_PRIORITY = ['excerpt', 'summary', 'description', 'subtitle'];
19
+
20
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}/;
21
+ const URL_RE = /^https?:\/\//;
22
+ // Match an HTML tag-open (`<tag`, `</`, `<!`) AND entity-encoded HTML (`&lt;`) — the
23
+ // encoded form has no literal `<`, so a `<`-only check would skip decode entirely, while
24
+ // a bare `<` in prose ("Q4 < projected") must NOT be treated as markup (Rule 4).
25
+ const RICH_TEXT_RE = /<[a-z!/]|&lt;|&#(?:60|x3c);/i;
26
+
27
+ // ISO_DATE_RE only checks the PREFIX, so unparseable strings ("2024-13-99") and SKUs
28
+ // ("1234-56-78") slip through; `Intl.DateTimeFormat.format` throws RangeError on an
29
+ // Invalid Date, which would break the whole render. Return null on invalid so the
30
+ // caller falls through to the next classifier / skips the field.
31
+ function formatCmsDate(value: string, options: Intl.DateTimeFormatOptions): string | null {
32
+ const date = new Date(value);
33
+ if (Number.isNaN(date.getTime())) return null;
34
+ return new Intl.DateTimeFormat('en-US', options).format(date);
35
+ }
36
+
37
+ /** One renderable cell — a discriminated union the template switches on. */
38
+ type Cell =
39
+ | { kind: 'title'; text: string }
40
+ | { kind: 'richText'; html: string }
41
+ | { kind: 'date'; iso: string; label: string }
42
+ | { kind: 'url'; href: string }
43
+ | { kind: 'image'; src: string; alt: string }
44
+ | { kind: 'number'; text: string }
45
+ | { kind: 'boolean'; label: string }
46
+ | { kind: 'text'; text: string };
47
+
48
+ @Component({
49
+ selector: 'cms-content',
50
+ standalone: true,
51
+ imports: [CommonModule],
52
+ template: `
53
+ @if (loading) {
54
+ <div role="status">Loading…</div>
55
+ } @else if (error) {
56
+ <div role="alert">{{ error.message }}</div>
57
+ } @else if (body) {
58
+ <article [class]="className">
59
+ @for (cell of cells; track $index) {
60
+ @switch (cell.kind) {
61
+ @case ('title') {
62
+ @if (layout === 'detail') { <h1>{{ cell.text }}</h1> }
63
+ @else { <h2>{{ cell.text }}</h2> }
64
+ }
65
+ <!-- Decoded, then Angular's DomSanitizer sanitizes the [innerHTML] binding. -->
66
+ @case ('richText') { <div [innerHTML]="cell.html"></div> }
67
+ @case ('date') { <time [attr.dateTime]="cell.iso">{{ cell.label }}</time> }
68
+ @case ('url') {
69
+ <a [href]="cell.href" target="_blank" rel="noopener noreferrer"
70
+ [attr.aria-label]="'External link: ' + cell.href">{{ cell.href }}</a>
71
+ }
72
+ @case ('image') { <img [src]="cell.src" [alt]="cell.alt" loading="lazy" /> }
73
+ @case ('number') { <span>{{ cell.text }}</span> }
74
+ @case ('boolean') { <span class="badge">{{ cell.label }}</span> }
75
+ @case ('text') { <p>{{ cell.text }}</p> }
76
+ }
77
+ }
78
+ </article>
79
+ }
80
+ `,
81
+ })
82
+ export class CmsContentComponent implements OnChanges {
83
+ @Input() body: Record<string, unknown> | undefined;
84
+ @Input() loading = false;
85
+ @Input() error: Error | undefined;
86
+ @Input() layout: 'list' | 'detail' = 'detail';
87
+ /** Optional whitelist — bypasses priority lists and renders only these fields in order. */
88
+ @Input() fields?: ReadonlyArray<string>;
89
+ @Input() className?: string;
90
+ /** Resolve a `references`-bag contentKey to its media field (rare payload shape). */
91
+ @Input() resolveRef?: (contentKey: string) => CmsImageField | undefined;
92
+
93
+ /** Recomputed on every input change — the template reads this. */
94
+ cells: Cell[] = [];
95
+
96
+ ngOnChanges(): void {
97
+ this.cells = this.body ? this.buildCells(this.body) : [];
98
+ }
99
+
100
+ private buildCells(body: Record<string, unknown>): Cell[] {
101
+ // Whitelist mode — render exactly these fields, in order, by shape.
102
+ if (this.fields && this.fields.length > 0) {
103
+ const out: Cell[] = [];
104
+ for (const name of this.fields) {
105
+ const cell = this.classify(name, body[name]);
106
+ if (cell) out.push(cell);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ // Heuristic mode — title, then first image, then first excerpt, then the rest.
112
+ const out: Cell[] = [];
113
+ const consumed = new Set<string>();
114
+
115
+ const title = this.pickTitle(body);
116
+ if (title) {
117
+ out.push({ kind: 'title', text: title.value });
118
+ consumed.add(title.name);
119
+ }
120
+
121
+ const imageField = this.pickFirstField(body, IMAGE_PRIORITY, isImageObject);
122
+ if (imageField) {
123
+ consumed.add(imageField.name);
124
+ const src = this.resolveImageSrc(imageField.value);
125
+ if (src) out.push({ kind: 'image', src, alt: resolveImageAlt(imageField.value) });
126
+ }
127
+
128
+ const excerpt = this.pickFirstField<string>(
129
+ body,
130
+ EXCERPT_PRIORITY,
131
+ (v) => typeof v === 'string' && v.length > 0,
132
+ );
133
+ if (excerpt) {
134
+ consumed.add(excerpt.name);
135
+ out.push({ kind: 'text', text: excerpt.value });
136
+ }
137
+
138
+ if (this.layout === 'detail') {
139
+ for (const [k, v] of Object.entries(body)) {
140
+ if (consumed.has(k)) continue;
141
+ const cell = this.classify(k, v);
142
+ if (cell) out.push(cell);
143
+ }
144
+ }
145
+
146
+ return out;
147
+ }
148
+
149
+ /** Value-shape dispatcher — the Angular parallel of RenderField. */
150
+ private classify(name: string, value: unknown): Cell | null {
151
+ if (value == null) return null;
152
+
153
+ if (typeof value === 'string') {
154
+ if (RICH_TEXT_RE.test(value)) {
155
+ // Decode first (else tags render as literal text); [innerHTML] is then
156
+ // sanitized by Angular's DomSanitizer. Rule 4.
157
+ return { kind: 'richText', html: decodeRichHtmlEntities(value) };
158
+ }
159
+ if (ISO_DATE_RE.test(value)) {
160
+ const label = formatCmsDate(value, { dateStyle: 'medium' });
161
+ // Valid date → date cell; invalid (matched the prefix but unparseable) → fall through.
162
+ if (label) return { kind: 'date', iso: value, label };
163
+ }
164
+ if (URL_RE.test(value)) return { kind: 'url', href: value };
165
+ if (/title/i.test(name)) return { kind: 'title', text: value };
166
+ return { kind: 'text', text: value };
167
+ }
168
+
169
+ if (typeof value === 'number') {
170
+ return { kind: 'number', text: new Intl.NumberFormat().format(value) };
171
+ }
172
+
173
+ if (typeof value === 'boolean') {
174
+ return { kind: 'boolean', label: value ? 'Yes' : 'No' };
175
+ }
176
+
177
+ if (typeof value === 'object') {
178
+ const obj = value as Record<string, unknown>;
179
+ // dateTime shape
180
+ if (typeof obj.dateTime === 'string') {
181
+ const label = formatCmsDate(obj.dateTime, {
182
+ dateStyle: 'medium',
183
+ timeStyle: 'short',
184
+ timeZone: typeof obj.timeZone === 'string' ? obj.timeZone : undefined,
185
+ });
186
+ // Invalid dateTime → skip this field rather than crash the render.
187
+ return label ? { kind: 'date', iso: obj.dateTime, label } : null;
188
+ }
189
+ // image-shaped object
190
+ if (isImageObject(obj)) {
191
+ const src = this.resolveImageSrc(obj);
192
+ return src ? { kind: 'image', src, alt: resolveImageAlt(obj) } : null;
193
+ }
194
+ // contentReference shape
195
+ const refObj = (obj as { ref?: { contentKey?: string } }).ref;
196
+ if (refObj && typeof refObj.contentKey === 'string' && this.resolveRef) {
197
+ const resolved = this.resolveRef(refObj.contentKey);
198
+ if (!resolved) return null;
199
+ return { kind: 'text', text: (resolved as { title?: string }).title ?? refObj.contentKey };
200
+ }
201
+ return null;
202
+ }
203
+
204
+ return null;
205
+ }
206
+
207
+ /** Delivery image field → loadable `src` via the toolkit (resolves the relative
208
+ * Connect-API url; no options passed). */
209
+ private resolveImageSrc(field: unknown): string | undefined {
210
+ return resolveCmsImageUrl(field as CmsImageField | undefined);
211
+ }
212
+
213
+ private pickTitle(body: Record<string, unknown>): { name: string; value: string } | undefined {
214
+ if (typeof body['title'] === 'string') return { name: 'title', value: body['title'] };
215
+ for (const [k, v] of Object.entries(body)) {
216
+ if (typeof v === 'string' && /title/i.test(k)) return { name: k, value: v };
217
+ }
218
+ return undefined;
219
+ }
220
+
221
+ private pickFirstField<T = unknown>(
222
+ body: Record<string, unknown>,
223
+ priority: string[],
224
+ predicate: (v: unknown) => boolean,
225
+ ): { name: string; value: T } | undefined {
226
+ for (const k of priority) {
227
+ const v = body[k];
228
+ if (v != null && predicate(v)) return { name: k, value: v as T };
229
+ }
230
+ return undefined;
231
+ }
232
+ }
233
+
234
+ function isImageObject(v: unknown): boolean {
235
+ if (!v || typeof v !== 'object') return false;
236
+ const o = v as { url?: unknown; source?: { type?: unknown } };
237
+ return typeof o.url === 'string' || (o.source != null && typeof o.source.type === 'string');
238
+ }
239
+
240
+ /** Delivery image field → authored alt text. Empty string when the field carries none —
241
+ * a decorative image (WCAG 1.1.1), not a missing attribute. */
242
+ function resolveImageAlt(field: unknown): string {
243
+ if (field && typeof field === 'object') {
244
+ const alt = (field as { altText?: unknown }).altText;
245
+ if (typeof alt === 'string') return alt;
246
+ }
247
+ return '';
248
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * CmsItemService — single-content data layer for Angular (Init →
3
+ * src/cms/angular/cms-item.service.ts). Counterpart of React's `useCmsItem`.
4
+ * Owns a per-ref cache + inflight dedup and dispatches on ref type (CmsExternalRef
5
+ * `url` → getCmsContentByUrl / CDN; CmsRef `contentKey` → getCmsContentByKey /
6
+ * Connect). `providedIn: 'root'` → one instance app-wide, so the maps are shared.
7
+ * `load()` never rejects — failures resolve with `error` set.
8
+ */
9
+ import { Injectable } from '@angular/core';
10
+ import {
11
+ getCmsContentByUrl,
12
+ getCmsContentByKey,
13
+ CmsDeliveryError,
14
+ CmsDeliveryNotFoundError,
15
+ ConnectApiError,
16
+ CmsNotFoundError,
17
+ } from '@salesforce/ui-bundle-template-feature-cms-toolkit';
18
+ import type { AnyCmsRef, CmsRef } from '../shared/cmsCore.types';
19
+ import { CMS_CHANNEL_ID_FALLBACK } from '../shared/externalRefs';
20
+
21
+ /** Resolved state for one ref read — the component maps this onto signals. */
22
+ export interface CmsItemResult<TBody> {
23
+ body: TBody | undefined;
24
+ error: Error | undefined;
25
+ }
26
+
27
+ @Injectable({ providedIn: 'root' })
28
+ export class CmsItemService {
29
+ // Shared across every consumer of this singleton service.
30
+ private readonly cache = new Map<string, unknown>();
31
+ private readonly inflight = new Map<string, Promise<unknown>>();
32
+
33
+ /**
34
+ * Read one item by ref — resolves the unwrapped body or a typed error, never
35
+ * rejects. Deduped by `ref.name`; the shared request carries NO AbortSignal, so a
36
+ * component torn down mid-flight ignores the late result rather than aborting.
37
+ */
38
+ async load<TBody = unknown>(ref: AnyCmsRef): Promise<CmsItemResult<TBody>> {
39
+ const name = ref.name;
40
+
41
+ const cached = this.cache.get(name) as TBody | undefined;
42
+ if (cached !== undefined) {
43
+ return { body: cached, error: undefined };
44
+ }
45
+
46
+ let promise = this.inflight.get(name) as Promise<TBody> | undefined;
47
+ if (!promise) {
48
+ promise = this.createItemRequest<TBody>(ref);
49
+ this.inflight.set(name, promise);
50
+ // Self-cleaning: cache on success, always drop the inflight entry.
51
+ promise
52
+ .then((body) => {
53
+ this.cache.set(name, body);
54
+ })
55
+ .catch(() => {
56
+ /* surfaced to the caller below; swallow to avoid unhandled rejection */
57
+ })
58
+ .finally(() => {
59
+ if (this.inflight.get(name) === promise) this.inflight.delete(name);
60
+ });
61
+ }
62
+
63
+ try {
64
+ const body = await promise;
65
+ return { body, error: undefined };
66
+ } catch (err: unknown) {
67
+ return { body: undefined, error: this.normalizeError(err) };
68
+ }
69
+ }
70
+
71
+ /** Dispatch on ref type. NO abort signal — the request is shared (see `load`). */
72
+ private createItemRequest<TBody>(ref: AnyCmsRef): Promise<TBody> {
73
+ // Foreign ref — public delivery URL fetched AS-IS (Rule 1).
74
+ if ('url' in ref && typeof ref.url === 'string') {
75
+ return getCmsContentByUrl<TBody>(ref.url);
76
+ }
77
+ // uiBundle-space ref — toolkit remaps the contentKey (fail-open) and reads Connect.
78
+ const channelId = CMS_CHANNEL_ID_FALLBACK || undefined;
79
+ return getCmsContentByKey<TBody>((ref as CmsRef).contentKey, { channelId });
80
+ }
81
+
82
+ /** Pass through the toolkit's typed errors as-is; wrap anything else. */
83
+ private normalizeError(err: unknown): Error {
84
+ if (
85
+ err instanceof CmsDeliveryNotFoundError ||
86
+ err instanceof CmsDeliveryError ||
87
+ err instanceof CmsNotFoundError ||
88
+ err instanceof ConnectApiError
89
+ ) {
90
+ return err;
91
+ }
92
+ return err instanceof Error ? err : new Error('Unknown CMS fetch error');
93
+ }
94
+
95
+ /** Test-only escape hatch — reset the cache between test runs. */
96
+ __resetForTests(): void {
97
+ this.cache.clear();
98
+ this.inflight.clear();
99
+ }
100
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * {{PageName}} — auto-generated detail page for one {{type}} item (codegen-managed;
3
+ * regenerated in full). Customize via {{type}}Layouts/{{type}}Slots in
4
+ * `src/cms/react/types/{{type}}.ts`.
5
+ *
6
+ * Tokens: {{TypeRenderer}} renderer name · {{Type}} PascalCase · {{type}} camelCase ·
7
+ * {{REF_CONST}} ref catalog key · {{PageName}} export name · {{urlSlug}} route slug.
8
+ */
9
+ import { {{TypeRenderer}} } from '../types/{{TypeRenderer}}';
10
+ import { ref } from '../../shared/externalRefs';
11
+
12
+ export function {{PageName}}() {
13
+ return (
14
+ <main className="cms-detail-page cms-{{type}}-detail-page">
15
+ <{{TypeRenderer}} ref={ref('{{REF_CONST}}')} layout="detail" />
16
+ </main>
17
+ );
18
+ }
19
+
20
+ export default {{PageName}};
@@ -0,0 +1,129 @@
1
+ /**
2
+ * MediaRenderer — dedicated renderer for standalone CMS media (React; Init →
3
+ * src/cms/react/MediaRenderer.tsx). Media is a PREDEFINED type parallel to `news`:
4
+ * a media ref binds here at registration time and dispatches by `ref.cmsType`
5
+ * (`image`/`audio`/`video`/`document`) — NEVER by body shape, so audio/video/document
6
+ * can't be mis-caught by the heuristic renderer's `isImageObject` path (SKILL.md §B1).
7
+ *
8
+ * Dispatch is by `cmsType` AND ref shape (Rule 1/Rule 2):
9
+ * - Foreign ref (`url`): a media `unauthenticatedUrl` is the ASSET itself, so the url
10
+ * goes straight onto the element `src`/`href` — NO fetch, NO toolkit resolver. Alt
11
+ * text / document label ride on the ref (threaded from the search hand-off or asked
12
+ * at Ref Registration), since there is no fetched body to read them from.
13
+ * - uiBundle-space ref (`contentKey`): fetch the body via useCmsItem, then resolve the
14
+ * src by medium (image → resolveCmsImageUrl; audio/video/document → resolveMediaUrl).
15
+ *
16
+ * A media item is a single asset, not a field bag, so neither path delegates to
17
+ * heuristicRenderer. Players/links are elements, never injected HTML (Rule 4).
18
+ */
19
+ import type { ReactNode } from 'react';
20
+ import {
21
+ resolveCmsImageUrl,
22
+ resolveMediaUrl,
23
+ } from '@salesforce/ui-bundle-template-feature-cms-toolkit';
24
+ import { useCmsItem } from './useCmsItem';
25
+ import {
26
+ type AnyCmsRef,
27
+ type CmsMediaBody,
28
+ type CmsMediaType,
29
+ } from '../shared/cmsCore.types';
30
+
31
+ export interface MediaRendererProps {
32
+ /** A media ref — `cmsType` selects the element; a non-media ref is a compile error. */
33
+ ref: AnyCmsRef<CmsMediaType>;
34
+ className?: string;
35
+ }
36
+
37
+ /**
38
+ * Emit the one element for a resolved media `src`. Shared by both paths so the element
39
+ * markup (and its a11y contract) lives in exactly one place.
40
+ */
41
+ function mediaElement(
42
+ cmsType: CmsMediaType,
43
+ src: string,
44
+ alt: string,
45
+ docLabel: string,
46
+ className?: string,
47
+ ): ReactNode {
48
+ switch (cmsType) {
49
+ case 'image':
50
+ return <img className={className} src={src} alt={alt} loading="lazy" />;
51
+ case 'audio':
52
+ // altText is typically absent on audio → aria-label only when present.
53
+ return <audio className={className} controls src={src} aria-label={alt || undefined} />;
54
+ case 'video':
55
+ return <video className={className} controls src={src} aria-label={alt || undefined} />;
56
+ case 'document':
57
+ // Download link only — no inline preview (SKILL.md §B3). Name the link so it
58
+ // identifies WHAT downloads (WCAG 2.4.4), never a bare "Download" that is
59
+ // ambiguous when several media links share a page.
60
+ return <a className={className} href={src} download>{docLabel}</a>;
61
+ default:
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Foreign media ref — the `url` IS the asset (Rule 1/Rule 2). No fetch, no resolver:
68
+ * set it as the element src directly. `altText`/`title` come off the ref.
69
+ */
70
+ function DirectMedia(props: {
71
+ cmsType: CmsMediaType;
72
+ url: string;
73
+ alt: string;
74
+ title?: string;
75
+ className?: string;
76
+ }): ReactNode {
77
+ const { cmsType, url, alt, title, className } = props;
78
+ const docLabel = alt || title || 'Download';
79
+ return mediaElement(cmsType, url, alt, docLabel, className);
80
+ }
81
+
82
+ /**
83
+ * uiBundle-space media ref — fetch the body, then resolve the src by medium. This is the
84
+ * authenticated-Connect path; identical behaviour to before the direct-URL fork.
85
+ */
86
+ function FetchedMedia(props: { ref: AnyCmsRef<CmsMediaType>; className?: string }): ReactNode {
87
+ const { ref, className } = props;
88
+ const { body, loading, error } = useCmsItem<CmsMediaBody>(ref);
89
+
90
+ if (loading) return <div role="status">Loading…</div>;
91
+ if (error) return <div role="alert">{error.message}</div>;
92
+ if (!body) return null;
93
+
94
+ const media = body['sfdc_cms:media'];
95
+ const alt = body.altText ?? '';
96
+ const docLabel = body.altText || body.title || 'Download';
97
+
98
+ // Src resolution SPLITS by medium (Rule 2): image → resolveCmsImageUrl;
99
+ // audio/video/document → resolveMediaUrl. Both toolkit helpers resolve the
100
+ // relative Connect-API url; no options are passed.
101
+ const src =
102
+ ref.cmsType === 'image' ? resolveCmsImageUrl(media) : media?.url ? resolveMediaUrl(media.url) : undefined;
103
+ if (!src) return null;
104
+
105
+ return mediaElement(ref.cmsType, src, alt, docLabel, className);
106
+ }
107
+
108
+ export function MediaRenderer(props: MediaRendererProps): ReactNode {
109
+ const { ref, className } = props;
110
+
111
+ // Foreign media ref: the unauthenticatedUrl is the asset src directly — no fetch
112
+ // (Rule 1). alt/title ride on the ref (search hand-off or Ref Registration ask).
113
+ if ('url' in ref && typeof ref.url === 'string') {
114
+ return (
115
+ <DirectMedia
116
+ cmsType={ref.cmsType}
117
+ url={ref.url}
118
+ alt={ref.altText ?? ''}
119
+ title={ref.title}
120
+ className={className}
121
+ />
122
+ );
123
+ }
124
+
125
+ // uiBundle-space ref (contentKey): fetch + resolve by medium.
126
+ return <FetchedMedia ref={ref} className={className} />;
127
+ }
128
+
129
+ export default MediaRenderer;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * {{Type}}List — opt-in list/grid wrapper over N same-type refs (codegen-managed;
3
+ * scaffolded ONLY on a list/grid prompt, not by Init). Lands in src/cms/react/types/.
4
+ * One `{{Type}}Renderer` per ref → each item keeps its own useCmsItem + error branch
5
+ * (mixed contentKey/url refs are fine). List vs grid is `className` CSS
6
+ * (references/styling-scopes.md). For a single-round-trip batch read, call
7
+ * `getCmsContentByKeys` directly instead (references/bulk-loading.md).
8
+ *
9
+ * Tokens: {{Type}} PascalCase, {{type}} camelCase, {{TypeRenderer}} renderer name.
10
+ */
11
+ import type { ReactNode } from 'react';
12
+ import type { AnyCmsRef } from '../../shared/cmsCore.types';
13
+ import { {{TypeRenderer}} } from './{{TypeRenderer}}';
14
+ import type { {{Type}}Layout } from './{{type}}';
15
+
16
+ export interface {{Type}}ListProps {
17
+ /** The group to render, one renderer per entry (e.g. `[ref('EXT_A'), ref('EXT_B')]`). */
18
+ refs: ReadonlyArray<AnyCmsRef<'{{type}}'>>;
19
+ /** Per-item layout (default `list`). Grid is achieved via `className` CSS. */
20
+ layout?: {{Type}}Layout;
21
+ className?: string;
22
+ }
23
+
24
+ export function {{Type}}List({
25
+ refs,
26
+ layout = 'list',
27
+ className,
28
+ }: {{Type}}ListProps): ReactNode {
29
+ return (
30
+ <div role="list" className={className ?? 'cms-{{type}}-list'}>
31
+ {refs.map((r) => (
32
+ <div role="listitem" key={r.name}>
33
+ <{{Type}}Renderer ref={r} layout={layout} />
34
+ </div>
35
+ ))}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export default {{Type}}List;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * {{Type}}Renderer — per-type wrapper (codegen-managed; regenerated on schema sync).
3
+ * No user region — customize via {{type}}Layouts/{{type}}Slots in `./{{type}}.ts`.
4
+ * Delegates ALL field rendering to heuristicRenderer — do NOT inject HTML here
5
+ * (Rule 4). `ref` is typed `AnyCmsRef<'{{type}}'>` — a ref whose `cmsType` differs is
6
+ * a compile error. Fields are read directly, never `.value` (Rule 5).
7
+ * Tokens: {{Type}} PascalCase, {{type}} camelCase.
8
+ */
9
+ import type { ReactNode, ComponentType } from 'react';
10
+ import type { AnyCmsRef } from '../../shared/cmsCore.types';
11
+ import { useCmsItem } from '../useCmsItem';
12
+ import { heuristicRenderer } from '../heuristicRenderer';
13
+ import { {{type}}Layouts, {{type}}Slots, type {{Type}}Body, type {{Type}}Layout } from './{{type}}';
14
+
15
+ export interface {{Type}}RendererProps {
16
+ ref: AnyCmsRef<'{{type}}'>;
17
+ layout?: {{Type}}Layout;
18
+ /** Whitelist of {{Type}}Body field names. Bypasses layout's default field list. */
19
+ fields?: ReadonlyArray<keyof {{Type}}Body & string>;
20
+ components?: {
21
+ Title?: ComponentType<{ value: string }>;
22
+ Image?: ComponentType<{ src: string; alt?: string }>;
23
+ RichText?: ComponentType<{ html: string }>;
24
+ };
25
+ className?: string;
26
+ }
27
+
28
+ export function {{Type}}Renderer(props: {{Type}}RendererProps): ReactNode {
29
+ const {
30
+ ref,
31
+ layout = 'card',
32
+ fields,
33
+ components,
34
+ className,
35
+ } = props;
36
+
37
+ const { body, loading, error } = useCmsItem<{{Type}}Body>(ref);
38
+
39
+ // fields: per-embed prop → layout preset → undefined (heuristic mode).
40
+ const layoutPreset = {{type}}Layouts[layout];
41
+ const resolvedFields = fields ?? layoutPreset.fields;
42
+
43
+ // components: per-embed prop → type-level slot defaults.
44
+ const resolvedComponents = {
45
+ ...(( {{type}}Slots as unknown) as {{Type}}RendererProps['components']),
46
+ ...components,
47
+ };
48
+
49
+ const resolvedClassName = className ?? layoutPreset.className;
50
+
51
+ // 'detail' layout key routes to detail-mode rendering; every other custom
52
+ // layout falls through to list-mode. Add more layouts in `{{type}}.ts`.
53
+ const rendererLayout = layout === 'detail' ? 'detail' : 'list';
54
+
55
+ return heuristicRenderer({
56
+ body: body as Record<string, unknown> | undefined,
57
+ loading,
58
+ error,
59
+ layout: rendererLayout,
60
+ fields: resolvedFields as ReadonlyArray<string> | undefined,
61
+ components: resolvedComponents,
62
+ className: resolvedClassName,
63
+ });
64
+ }