@salesforce/afv-skills 1.56.0 → 1.57.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/skills/commerce-b2b-open-code-components-integrate/SKILL.md +1 -1
- package/skills/commerce-b2b-open-code-components-replace/SKILL.md +16 -16
- package/skills/dx-devops-project-manage/SKILL.md +1 -1
- package/skills/experience-cms-content-generate/SKILL.md +1 -1
- package/skills/experience-cms-content-render/SKILL.md +3 -3
- package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +35 -7
- package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +13 -4
- package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +30 -8
- package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +14 -4
- package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +11 -1
- package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +1 -1
- package/skills/experience-cms-content-render/assets/shared/mediaLabels.ts +62 -0
- package/skills/experience-cms-content-render/references/init-scaffold.md +13 -6
- package/skills/experience-content-media-stock-image-search/SKILL.md +0 -1
- package/skills/experience-lwc-design-generate/SKILL.md +1 -1
- package/skills/experience-lwc-legacy-migrate/SKILL.md +212 -0
- package/skills/experience-lwc-legacy-migrate/assets/lo20-host-page-template.html +192 -0
- package/skills/experience-lwc-legacy-migrate/references/aura-to-lwc-completeness-checklist.md +188 -0
- package/skills/experience-lwc-legacy-migrate/references/lightning-out-2-system-reference.md +495 -0
- package/skills/experience-lwc-legacy-migrate/references/lightning-out-beta-to-2-migration.md +824 -0
- package/skills/experience-lwc-legacy-migrate/scripts/convert-lo-names.py +94 -0
- package/skills/experience-lwc-legacy-migrate/scripts/validate-lo20-page.py +133 -0
- package/skills/experience-ui-bundle-project-generate/SKILL.md +1 -1
- package/skills/integration-connectivity-connected-app-configure/SKILL.md +5 -5
- package/skills/integration-connectivity-generate/SKILL.md +10 -10
- package/skills/integration-connectivity-generate/scripts/configure-named-credential.sh +2 -2
- package/skills/platform-apex-test-generate/SKILL.md +1 -1
- package/skills/platform-apex-test-run/SKILL.md +1 -1
- package/skills/platform-custom-field-generate/SKILL.md +1 -1
- package/skills/platform-custom-lightning-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-metadata-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-object-generate/SKILL.md +1 -1
- package/skills/platform-custom-report-type-generate/SKILL.md +1 -1
- package/skills/platform-custom-setting-generate/SKILL.md +1 -1
- package/skills/platform-data-and-tooling-api-context-get/SKILL.md +1 -1
- package/skills/platform-data-manage/SKILL.md +10 -10
- package/skills/platform-flexipage-generate/SKILL.md +1 -1
- package/skills/platform-lightning-app-coordinate/SKILL.md +1 -1
- package/skills/platform-metadata-api-context-get/SKILL.md +1 -1
- package/skills/platform-metadata-retrieve/SKILL.md +1 -1
- package/skills/platform-permission-set-generate/SKILL.md +1 -1
- package/skills/platform-report-generate/SKILL.md +1 -1
- package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +4 -4
- package/skills/platform-sharing-owd-configure/SKILL.md +1 -1
- package/skills/platform-sharing-rules-generate/SKILL.md +1 -1
- package/skills/platform-soql-query/SKILL.md +1 -1
- package/skills/platform-value-set-generate/SKILL.md +1 -1
- package/skills/service-agentforce-human-escalation-configure/SKILL.md +4 -0
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +26 -6
- package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +5 -4
- package/skills/service-de-waba-integrate/SKILL.md +2 -1
- package/skills/service-digital-engagement-deployment-configure/SKILL.md +2 -0
- package/skills/service-digital-engagement-deployment-configure/scripts/check-api-version.sh +29 -0
- package/skills/service-email-to-case-configure/SKILL.md +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: commerce-b2b-open-code-components-integrate
|
|
3
3
|
description: "Integrate Salesforce B2B Commerce open source components from GitHub into B2B Commerce stores. Use when users mention \"integrate open code components\", \"open source B2B commerce\", \"add open code components\", \"forcedotcom/b2b-commerce-open-source-components\", or want to add open source commerce components to their store. Copies all components and labels so they become available in Experience Builder."
|
|
4
|
-
allowed-tools: Bash(git clone:*) Bash(cp:*) Read
|
|
4
|
+
allowed-tools: Bash(git clone:*) Bash(cp:*) Bash(sf org list:*) Bash(sf config get:*) Bash(sf project retrieve start:*) Read
|
|
5
5
|
metadata:
|
|
6
6
|
version: "1.0"
|
|
7
7
|
domains: ["Commerce", "Experience"]
|
|
@@ -3,10 +3,10 @@ name: commerce-b2b-open-code-components-replace
|
|
|
3
3
|
description: "Replace OOTB (out-of-the-box) B2B Commerce components with open source equivalents in site metadata content.json files, or look up the equivalent open code `site:` component for OOTB definitions. Use when users mention \"replace OOTB components\", \"replace commerce components with open code\", \"swap OOTB for open source\", \"replace commerce_builder:\", \"replace OOTB in site\", \"replace component in site metadata\", \"replace component definition\", \"find open code equivalent\", \"equivalent open code component\", \"OOTB to open code mapping\", \"what is the site component for\", components \"in this view\" or \"for a given view\", or a specific list of component names — and want to update or only discover mappings in their store metadata."
|
|
4
4
|
allowed-tools: Bash(grep:*) Bash(ls:*) Read Write
|
|
5
5
|
metadata:
|
|
6
|
-
relatedSkills:
|
|
7
|
-
- "commerce-b2b-open-code-components-integrate"
|
|
8
6
|
version: "1.0"
|
|
9
7
|
domains: ["Commerce", "Experience"]
|
|
8
|
+
relatedSkills:
|
|
9
|
+
- "commerce-b2b-open-code-components-integrate"
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Replacing OOTB B2B Commerce Components with Open Code
|
|
@@ -94,10 +94,10 @@ Then present the replaceable components as a **multi-select list** so the user c
|
|
|
94
94
|
```text
|
|
95
95
|
Which components would you like to replace?
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
[ ] commerce_builder:heading → site:productHeading
|
|
98
|
+
[ ] commerce_builder:cartBadge → site:cartBadge
|
|
99
|
+
[ ] commerce_builder:searchInput → site:searchInput
|
|
100
|
+
[ ] All of the above
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
If user provided specific component name(s) in the original request, pre-filter to those and skip the selection prompt.
|
|
@@ -119,7 +119,7 @@ The affected files are already known from Step 1. For each file that contains se
|
|
|
119
119
|
### Step 3: Report
|
|
120
120
|
|
|
121
121
|
```text
|
|
122
|
-
|
|
122
|
+
Replacement Complete!
|
|
123
123
|
|
|
124
124
|
Replaced X components across Y files:
|
|
125
125
|
- commerce_builder:heading → site:productHeading (3 files)
|
|
@@ -152,9 +152,9 @@ Next Steps:
|
|
|
152
152
|
**User:** "Replace OOTB components with open code versions"
|
|
153
153
|
|
|
154
154
|
**Agent:** "Before replacing components, I need to verify that the open code components are set up in your store. Let me check..."
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
155
|
+
- [done] Open source repo cloned
|
|
156
|
+
- [done] Store metadata available for My_B2B_Store1
|
|
157
|
+
- [done] Open code components are integrated
|
|
158
158
|
|
|
159
159
|
**Agent:** "I'm scanning your store's site metadata to find all OOTB commerce components and checking which have open code equivalents."
|
|
160
160
|
|
|
@@ -174,11 +174,11 @@ No mapping available (not in mapping table):
|
|
|
174
174
|
```text
|
|
175
175
|
Which components would you like to replace?
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
177
|
+
[ ] commerce_builder:heading → site:productHeading
|
|
178
|
+
[ ] commerce_builder:cartBadge → site:cartBadge
|
|
179
|
+
[ ] commerce_builder:searchInput → site:searchInput
|
|
180
|
+
[ ] commerce_builder:cartSummary → site:cartSummary
|
|
181
|
+
[ ] All of the above
|
|
182
182
|
```
|
|
183
183
|
|
|
184
184
|
**User:** _(selects heading and cartBadge)_
|
|
@@ -188,7 +188,7 @@ Which components would you like to replace?
|
|
|
188
188
|
_(Files already known from scan — one Read/Write pass per affected file, all replacements batched)_
|
|
189
189
|
|
|
190
190
|
```text
|
|
191
|
-
|
|
191
|
+
Replacement Complete!
|
|
192
192
|
|
|
193
193
|
Replaced 2 components across 5 files:
|
|
194
194
|
- commerce_builder:heading → site:productHeading (3 files)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dx-devops-project-manage
|
|
3
|
-
description: "Use
|
|
3
|
+
description: "Use to list, view, or manage DevOps Center projects in a Salesforce org — show all projects, create a project, or update an existing project's name, description, or active status. Invoke to run sf devops project list on any listing request, or to create a project, set up a new pipeline foundation, initialize DevOps Center for a feature, update project settings, or activate/deactivate (archive) a project. Consolidates sf devops project list/create/update. DO NOT TRIGGER for work item, pipeline, promotion, or conflict operations — those are separate skills."
|
|
4
4
|
metadata:
|
|
5
5
|
version: "1.0"
|
|
6
6
|
domains: ["Developer Experience"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: experience-cms-content-render
|
|
3
|
-
description: "Renders, embeds, or displays existing Salesforce CMS content in a React or Angular uiBundle app: installs the CMS toolkit
|
|
3
|
+
description: "Renders, embeds, or displays existing Salesforce CMS content in a React or Angular uiBundle app: installs the CMS toolkit, registers a typed reference, generates a framework-matched renderer from the live schema, and wires the render path (delivery API, contentKey, RichText, image resolution). Prefer over general UI-bundle skills like experience-ui-bundle-frontend-generate (page/layout shell only) for the CMS-rendering part, even under src/. Triggers on prompts like 'render the CMS article on the home page', 'embed this CMS blog post here', 'show the CMS video in the hero', 'display the CMS image in the banner', or 'add this CMS content block to the detail page'; for mixed prompts, activate for the CMS part only. Do not trigger for searching a CMS item or defining a new CMS content type/schema (use experience-cms-content-type-generate)."
|
|
4
4
|
metadata:
|
|
5
5
|
version: "2.1"
|
|
6
6
|
domains: ["Experience"]
|
|
@@ -168,6 +168,6 @@ Dynamic feed discovery ("latest 10 news"); cross-ref audit tooling; media/image-
|
|
|
168
168
|
|
|
169
169
|
## Files & references
|
|
170
170
|
|
|
171
|
-
**Templates** — `assets/shared/`: `cmsCore.types.ts`, `externalRefs.ts`, `cmsContentType.ts` (foundation + per-type body). `assets/react/`: `useCmsItem.ts`, `heuristicRenderer.tsx` (renderer + `RenderField` dispatcher), `TypeRenderer.tsx`, `TypeList.tsx`, `DetailPage.tsx`, `MediaRenderer.tsx`. `assets/angular/`: `cms-item.service.ts`, `cms-content.component.ts`, `TypeRenderer.component.ts`, `TypeList.component.ts`, `DetailPage.component.ts`, `MediaRenderer.component.ts`.
|
|
171
|
+
**Templates** — `assets/shared/`: `cmsCore.types.ts`, `externalRefs.ts`, `cmsContentType.ts` (foundation + per-type body), `mediaLabels.ts`. `assets/react/`: `useCmsItem.ts`, `heuristicRenderer.tsx` (renderer + `RenderField` dispatcher), `TypeRenderer.tsx`, `TypeList.tsx`, `DetailPage.tsx`, `MediaRenderer.tsx`. `assets/angular/`: `cms-item.service.ts`, `cms-content.component.ts`, `TypeRenderer.component.ts`, `TypeList.component.ts`, `DetailPage.component.ts`, `MediaRenderer.component.ts`.
|
|
172
172
|
|
|
173
|
-
**Docs** (`references/`): `package-api.md` (toolkit API +
|
|
173
|
+
**Docs** (`references/`): `package-api.md` (toolkit API + contract — ground truth), `interaction-model.md` (wording + terminal stops), `init-scaffold.md` (runtime tree, copy set, foundation sets), `schema-sync.md` (retrieval + regen), `verify.md` (fix policy + report), `codegen-guardrails.md` (full Rule 1–5), `embed-recipes.md` (URL regex + placement), `heuristic-render-rules.md`, `detail-pages.md`, `styling-scopes.md`, `bulk-loading.md`, `failure-modes.md`.
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type CmsMediaBody,
|
|
31
31
|
type CmsMediaType,
|
|
32
32
|
} from '../shared/cmsCore.types';
|
|
33
|
+
import { deriveDownloadLabel, effectiveAudioLabel } from '../shared/mediaLabels';
|
|
33
34
|
|
|
34
35
|
@Component({
|
|
35
36
|
selector: 'cms-media',
|
|
@@ -48,10 +49,21 @@ import {
|
|
|
48
49
|
@if (src) { <img [class]="className" [src]="src" [alt]="alt" loading="lazy" /> }
|
|
49
50
|
}
|
|
50
51
|
@case ('audio') {
|
|
51
|
-
|
|
52
|
+
<!-- [attr.aria-label]="alt || null" would REMOVE the attribute when alt is
|
|
53
|
+
empty (Angular attribute-binding semantics) — audioLabel always has a
|
|
54
|
+
value, so the control never loses its accessible name. -->
|
|
55
|
+
@if (src) { <audio [class]="className" controls [src]="src" [attr.aria-label]="audioLabel"></audio> }
|
|
52
56
|
}
|
|
53
57
|
@case ('video') {
|
|
54
|
-
|
|
58
|
+
<!-- Captions render only when the ref supplies `captionsSrc` (SKILL.md §B4)
|
|
59
|
+
— there is no fetched-body field to derive real caption text from. -->
|
|
60
|
+
@if (src) {
|
|
61
|
+
<video [class]="className" controls [src]="src" [attr.aria-label]="alt || null">
|
|
62
|
+
@if (captionsSrc) {
|
|
63
|
+
<track kind="captions" [src]="captionsSrc" [label]="captionsLabel" default />
|
|
64
|
+
}
|
|
65
|
+
</video>
|
|
66
|
+
}
|
|
55
67
|
}
|
|
56
68
|
@case ('document') {
|
|
57
69
|
<!-- Download link only — no inline preview (SKILL.md §B3). Named with the
|
|
@@ -75,13 +87,25 @@ export class MediaRendererComponent implements OnChanges {
|
|
|
75
87
|
/** Resolved src + alt for the template (recomputed after each load). */
|
|
76
88
|
src: string | undefined;
|
|
77
89
|
alt = '';
|
|
78
|
-
/** Accessible name for the document download link (altText → title →
|
|
79
|
-
|
|
90
|
+
/** Accessible name for the document download link (altText → title → derived
|
|
91
|
+
* filename → "Download file"; see `deriveDownloadLabel`). */
|
|
92
|
+
docLabel = 'Download file';
|
|
93
|
+
/** Caller-supplied captions-track URL/label for `<video>` (foreign ref only —
|
|
94
|
+
* see `CmsExternalRef.captionsSrc` in cmsCore.types.ts). Absent on the
|
|
95
|
+
* contentKey path, since the fetched media body carries no captions field. */
|
|
96
|
+
captionsSrc: string | undefined;
|
|
97
|
+
captionsLabel = 'Captions';
|
|
80
98
|
|
|
81
99
|
private loadedRefName?: string;
|
|
82
100
|
|
|
83
101
|
constructor(private readonly cms: CmsItemService) {}
|
|
84
102
|
|
|
103
|
+
/** Accessible name for `<audio>` when `altText` is empty — never falls back to
|
|
104
|
+
* `null`/removed, unlike `[attr.aria-label]="alt || null"`. */
|
|
105
|
+
get audioLabel(): string {
|
|
106
|
+
return effectiveAudioLabel(this.alt, this.src);
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
ngOnChanges(): void {
|
|
86
110
|
const ref = this.ref;
|
|
87
111
|
// Only (re)load when the ref identity changes (mirrors TypeRendererComponent).
|
|
@@ -92,14 +116,18 @@ export class MediaRendererComponent implements OnChanges {
|
|
|
92
116
|
this.error = undefined;
|
|
93
117
|
this.src = undefined;
|
|
94
118
|
this.alt = '';
|
|
95
|
-
this.docLabel = 'Download';
|
|
119
|
+
this.docLabel = 'Download file';
|
|
120
|
+
this.captionsSrc = undefined;
|
|
121
|
+
this.captionsLabel = 'Captions';
|
|
96
122
|
|
|
97
123
|
// Foreign media ref: the unauthenticatedUrl is the asset src directly — no load
|
|
98
124
|
// (Rule 1). alt/title ride on the ref (search hand-off or Ref Registration ask).
|
|
99
125
|
if ('url' in ref && typeof ref.url === 'string') {
|
|
100
126
|
this.src = ref.url;
|
|
101
127
|
this.alt = ref.altText ?? '';
|
|
102
|
-
this.docLabel = ref.altText
|
|
128
|
+
this.docLabel = deriveDownloadLabel(ref.url, ref.altText, ref.title);
|
|
129
|
+
this.captionsSrc = ref.captionsSrc;
|
|
130
|
+
this.captionsLabel = ref.captionsLabel || 'Captions';
|
|
103
131
|
this.loading = false;
|
|
104
132
|
return;
|
|
105
133
|
}
|
|
@@ -123,7 +151,7 @@ export class MediaRendererComponent implements OnChanges {
|
|
|
123
151
|
private resolveSrc(cmsType: CmsMediaType, body: CmsMediaBody): void {
|
|
124
152
|
const media = body['sfdc_cms:media'];
|
|
125
153
|
this.alt = body.altText ?? '';
|
|
126
|
-
this.docLabel = body.altText
|
|
154
|
+
this.docLabel = deriveDownloadLabel(media?.url, body.altText, body.title);
|
|
127
155
|
if (cmsType === 'image') {
|
|
128
156
|
this.src = resolveCmsImageUrl(media);
|
|
129
157
|
return;
|
|
@@ -18,6 +18,15 @@ import {
|
|
|
18
18
|
import type { AnyCmsRef, CmsRef } from '../shared/cmsCore.types';
|
|
19
19
|
import { CMS_CHANNEL_ID_FALLBACK } from '../shared/externalRefs';
|
|
20
20
|
|
|
21
|
+
/** Runtime guard before the cache's `as TBody` cast: a CMS body is always an
|
|
22
|
+
* object/array, so this screens out a stale primitive from a `ref.name` reused
|
|
23
|
+
* under a different, incompatible `TBody` — it cannot verify the object's shape
|
|
24
|
+
* actually matches `TBody` (no schema is available here), so callers must still
|
|
25
|
+
* keep `ref.name` unique per content shape. */
|
|
26
|
+
function isCacheableBody(value: unknown): boolean {
|
|
27
|
+
return value !== null && typeof value === 'object';
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
/** Resolved state for one ref read — the component maps this onto signals. */
|
|
22
31
|
export interface CmsItemResult<TBody> {
|
|
23
32
|
body: TBody | undefined;
|
|
@@ -38,9 +47,9 @@ export class CmsItemService {
|
|
|
38
47
|
async load<TBody = unknown>(ref: AnyCmsRef): Promise<CmsItemResult<TBody>> {
|
|
39
48
|
const name = ref.name;
|
|
40
49
|
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
return { body:
|
|
50
|
+
const rawCached = this.cache.get(name);
|
|
51
|
+
if (isCacheableBody(rawCached)) {
|
|
52
|
+
return { body: rawCached as TBody, error: undefined };
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
let promise = this.inflight.get(name) as Promise<TBody> | undefined;
|
|
@@ -89,7 +98,7 @@ export class CmsItemService {
|
|
|
89
98
|
) {
|
|
90
99
|
return err;
|
|
91
100
|
}
|
|
92
|
-
return err instanceof Error ? err : new Error('Unknown CMS fetch error');
|
|
101
|
+
return err instanceof Error ? err : new Error('Unknown CMS fetch error', { cause: err });
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
/** Test-only escape hatch — reset the cache between test runs. */
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type CmsMediaBody,
|
|
28
28
|
type CmsMediaType,
|
|
29
29
|
} from '../shared/cmsCore.types';
|
|
30
|
+
import { deriveDownloadLabel, effectiveAudioLabel } from '../shared/mediaLabels';
|
|
30
31
|
|
|
31
32
|
export interface MediaRendererProps {
|
|
32
33
|
/** A media ref — `cmsType` selects the element; a non-media ref is a compile error. */
|
|
@@ -43,16 +44,30 @@ function mediaElement(
|
|
|
43
44
|
src: string,
|
|
44
45
|
alt: string,
|
|
45
46
|
docLabel: string,
|
|
47
|
+
captionsSrc: string | undefined,
|
|
48
|
+
captionsLabel: string | undefined,
|
|
46
49
|
className?: string,
|
|
47
50
|
): ReactNode {
|
|
48
51
|
switch (cmsType) {
|
|
49
52
|
case 'image':
|
|
50
53
|
return <img className={className} src={src} alt={alt} loading="lazy" />;
|
|
51
54
|
case 'audio':
|
|
52
|
-
//
|
|
53
|
-
|
|
55
|
+
// `aria-label={alt || undefined}` would drop the attribute entirely when alt
|
|
56
|
+
// is empty (same JSX-prop-omission semantics as Angular's `|| null` binding) —
|
|
57
|
+
// effectiveAudioLabel always returns a value, so the control never loses its
|
|
58
|
+
// accessible name.
|
|
59
|
+
return <audio className={className} controls src={src} aria-label={effectiveAudioLabel(alt, src)} />;
|
|
54
60
|
case 'video':
|
|
55
|
-
|
|
61
|
+
// Captions require a caller-supplied `captionsSrc` (SKILL.md §B4) — the CMS
|
|
62
|
+
// media body/ref carries no captions-track field, so there is no data to
|
|
63
|
+
// fabricate a `<track>` from when the caller hasn't provided one.
|
|
64
|
+
return (
|
|
65
|
+
<video className={className} controls src={src} aria-label={alt || undefined}>
|
|
66
|
+
{captionsSrc && (
|
|
67
|
+
<track kind="captions" src={captionsSrc} label={captionsLabel || 'Captions'} default />
|
|
68
|
+
)}
|
|
69
|
+
</video>
|
|
70
|
+
);
|
|
56
71
|
case 'document':
|
|
57
72
|
// Download link only — no inline preview (SKILL.md §B3). Name the link so it
|
|
58
73
|
// identifies WHAT downloads (WCAG 2.4.4), never a bare "Download" that is
|
|
@@ -72,11 +87,13 @@ function DirectMedia(props: {
|
|
|
72
87
|
url: string;
|
|
73
88
|
alt: string;
|
|
74
89
|
title?: string;
|
|
90
|
+
captionsSrc?: string;
|
|
91
|
+
captionsLabel?: string;
|
|
75
92
|
className?: string;
|
|
76
93
|
}): ReactNode {
|
|
77
|
-
const { cmsType, url, alt, title, className } = props;
|
|
78
|
-
const docLabel = alt
|
|
79
|
-
return mediaElement(cmsType, url, alt, docLabel, className);
|
|
94
|
+
const { cmsType, url, alt, title, captionsSrc, captionsLabel, className } = props;
|
|
95
|
+
const docLabel = deriveDownloadLabel(url, alt, title);
|
|
96
|
+
return mediaElement(cmsType, url, alt, docLabel, captionsSrc, captionsLabel, className);
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
/**
|
|
@@ -93,7 +110,7 @@ function FetchedMedia(props: { ref: AnyCmsRef<CmsMediaType>; className?: string
|
|
|
93
110
|
|
|
94
111
|
const media = body['sfdc_cms:media'];
|
|
95
112
|
const alt = body.altText ?? '';
|
|
96
|
-
const docLabel = body.altText
|
|
113
|
+
const docLabel = deriveDownloadLabel(media?.url, body.altText, body.title);
|
|
97
114
|
|
|
98
115
|
// Src resolution SPLITS by medium (Rule 2): image → resolveCmsImageUrl;
|
|
99
116
|
// audio/video/document → resolveMediaUrl. Both toolkit helpers resolve the
|
|
@@ -102,7 +119,10 @@ function FetchedMedia(props: { ref: AnyCmsRef<CmsMediaType>; className?: string
|
|
|
102
119
|
ref.cmsType === 'image' ? resolveCmsImageUrl(media) : media?.url ? resolveMediaUrl(media.url) : undefined;
|
|
103
120
|
if (!src) return null;
|
|
104
121
|
|
|
105
|
-
|
|
122
|
+
// No fetched-body field carries a captions-track URL today (SKILL.md §B4), so the
|
|
123
|
+
// contentKey path never has real caption data to pass — only a foreign ref with a
|
|
124
|
+
// caller-supplied `captionsSrc` (see DirectMedia) can render a `<track>`.
|
|
125
|
+
return mediaElement(ref.cmsType, src, alt, docLabel, undefined, undefined, className);
|
|
106
126
|
}
|
|
107
127
|
|
|
108
128
|
export function MediaRenderer(props: MediaRendererProps): ReactNode {
|
|
@@ -117,6 +137,8 @@ export function MediaRenderer(props: MediaRendererProps): ReactNode {
|
|
|
117
137
|
url={ref.url}
|
|
118
138
|
alt={ref.altText ?? ''}
|
|
119
139
|
title={ref.title}
|
|
140
|
+
captionsSrc={ref.captionsSrc}
|
|
141
|
+
captionsLabel={ref.captionsLabel}
|
|
120
142
|
className={className}
|
|
121
143
|
/>
|
|
122
144
|
);
|
|
@@ -30,6 +30,15 @@ interface UseCmsItemState<TBody> {
|
|
|
30
30
|
const cache = new Map<string, unknown>();
|
|
31
31
|
const inflight = new Map<string, Promise<unknown>>();
|
|
32
32
|
|
|
33
|
+
/** Runtime guard before the cache's `as TBody` cast: a CMS body is always an
|
|
34
|
+
* object/array, so this screens out a stale primitive from a `ref.name` reused
|
|
35
|
+
* under a different, incompatible `TBody` — it cannot verify the object's shape
|
|
36
|
+
* actually matches `TBody` (no schema is available here), so callers must still
|
|
37
|
+
* keep `ref.name` unique per content shape. */
|
|
38
|
+
function isCacheableBody(value: unknown): boolean {
|
|
39
|
+
return value !== null && typeof value === 'object';
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
/** Shared request for a ref — NO abort signal. `CMS_CHANNEL_ID_FALLBACK` is passed
|
|
34
43
|
* as `options.channelId` only when set (empty → catalog supplies it). */
|
|
35
44
|
function createItemRequest<TBody>(ref: AnyCmsRef): Promise<TBody> {
|
|
@@ -46,7 +55,8 @@ export function useCmsItem<TBody = unknown>(
|
|
|
46
55
|
ref: AnyCmsRef,
|
|
47
56
|
): UseCmsItemState<TBody> {
|
|
48
57
|
const [state, setState] = useState<UseCmsItemState<TBody>>(() => {
|
|
49
|
-
const
|
|
58
|
+
const rawCached = cache.get(ref.name);
|
|
59
|
+
const cached = isCacheableBody(rawCached) ? (rawCached as TBody) : undefined;
|
|
50
60
|
return {
|
|
51
61
|
body: cached,
|
|
52
62
|
loading: cached === undefined,
|
|
@@ -64,9 +74,9 @@ export function useCmsItem<TBody = unknown>(
|
|
|
64
74
|
let cancelled = false;
|
|
65
75
|
const name = ref.name;
|
|
66
76
|
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
setState({ body:
|
|
77
|
+
const rawCached = cache.get(name);
|
|
78
|
+
if (isCacheableBody(rawCached)) {
|
|
79
|
+
setState({ body: rawCached as TBody, loading: false, error: undefined });
|
|
70
80
|
return () => {
|
|
71
81
|
cancelled = true;
|
|
72
82
|
};
|
|
@@ -25,9 +25,19 @@ export interface CmsExternalRef<K extends string = string> {
|
|
|
25
25
|
* from. The skill threads these off the search hand-off (or asks the user) at Ref
|
|
26
26
|
* Registration so the accessible name survives. Ignored on non-media refs (news
|
|
27
27
|
* reads its title from the fetched envelope). Both optional; `altText` absent → `''`
|
|
28
|
-
* (decorative image, WCAG 1.1.1); `title` names the document download link.
|
|
28
|
+
* (decorative image, WCAG 1.1.1); `title` names the document download link.
|
|
29
|
+
*
|
|
30
|
+
* `captionsSrc` is MEDIA-ONLY (video), also foreign-ref only: a caller who has a
|
|
31
|
+
* captions/subtitles track (e.g. a VTT file) for this video supplies its URL here
|
|
32
|
+
* so MediaRenderer can render an accessible `<track kind="captions">`. There is no
|
|
33
|
+
* fetched-body equivalent for the uiBundle-space (`contentKey`) path today — the
|
|
34
|
+
* predefined media body carries no captions field — so captions render only when
|
|
35
|
+
* a caller populates this on a foreign ref. `captionsLabel` names the track
|
|
36
|
+
* (defaults to "Captions" when omitted). */
|
|
29
37
|
readonly altText?: string;
|
|
30
38
|
readonly title?: string;
|
|
39
|
+
readonly captionsSrc?: string;
|
|
40
|
+
readonly captionsLabel?: string;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* src/cms/shared/externalRefs.ts; each embed appends one entry inside the marker block.
|
|
5
5
|
* Names are EXT_<TITLE_SNAKE>, unique — object-literal keys collide at compile time.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Warning: do NOT edit the marker block by hand. Add entries via the skill.
|
|
8
8
|
*/
|
|
9
9
|
// Only `AnyCmsRef` is used in the empty template; Ref Registration adds
|
|
10
10
|
// CmsExternalRef/CmsRef when it appends an entry (importing them eagerly trips
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessible-name derivation for MediaRenderer (FRAMEWORK-AGNOSTIC). Shared by the
|
|
3
|
+
* React and Angular renderers so the document-download label and the audio
|
|
4
|
+
* fallback label are computed identically regardless of framework.
|
|
5
|
+
*
|
|
6
|
+
* The CMS media body/ref (`CmsMediaField`/`CmsExternalRef`) carries no filename
|
|
7
|
+
* field and no captions-track field — only `url`. These helpers derive a label
|
|
8
|
+
* from real, already-present data (the URL itself) rather than fabricating text
|
|
9
|
+
* that isn't there.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Last path segment of `url`, URL-decoded, kept only when it looks like a filename
|
|
13
|
+
* (has an extension). Returns `undefined` when `url` is absent or unparsable, or
|
|
14
|
+
* when the last segment has no extension to trust as a filename. */
|
|
15
|
+
function deriveFilenameFromUrl(url: string | undefined): string | undefined {
|
|
16
|
+
if (!url) return undefined;
|
|
17
|
+
let pathname: string;
|
|
18
|
+
try {
|
|
19
|
+
pathname = url.includes('://') ? new URL(url).pathname : url.split(/[?#]/)[0];
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const lastSegment = pathname.split('/').filter(Boolean).pop();
|
|
24
|
+
if (!lastSegment) return undefined;
|
|
25
|
+
let decoded: string;
|
|
26
|
+
try {
|
|
27
|
+
decoded = decodeURIComponent(lastSegment);
|
|
28
|
+
} catch {
|
|
29
|
+
decoded = lastSegment;
|
|
30
|
+
}
|
|
31
|
+
return /\.[a-zA-Z0-9]+$/.test(decoded) ? decoded : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Accessible name for the document download link (WCAG 2.4.4) — identifies WHAT
|
|
36
|
+
* downloads instead of a bare, ambiguous "Download". Prefers `altText`/`title`
|
|
37
|
+
* when present; otherwise derives the real filename off the URL; otherwise falls
|
|
38
|
+
* back to the still-more-specific-than-bare-"Download" literal `'Download file'`.
|
|
39
|
+
*/
|
|
40
|
+
export function deriveDownloadLabel(
|
|
41
|
+
url: string | undefined,
|
|
42
|
+
altText?: string,
|
|
43
|
+
title?: string,
|
|
44
|
+
): string {
|
|
45
|
+
if (altText) return altText;
|
|
46
|
+
if (title) return title;
|
|
47
|
+
const filename = deriveFilenameFromUrl(url);
|
|
48
|
+
return filename ? `Download ${filename}` : 'Download file';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Accessible name for an `<audio>` element when `altText` is absent. Angular's
|
|
53
|
+
* `[attr.aria-label]="alt || null"` REMOVES the attribute when `alt` is falsy
|
|
54
|
+
* (unlike a JSX `undefined` prop, which is the same removal but explicitly
|
|
55
|
+
* intended there); this default keeps the attribute present so the control
|
|
56
|
+
* always has an accessible name.
|
|
57
|
+
*/
|
|
58
|
+
export function effectiveAudioLabel(altText: string | undefined, url?: string): string {
|
|
59
|
+
if (altText) return altText;
|
|
60
|
+
const filename = deriveFilenameFromUrl(url);
|
|
61
|
+
return filename ? `Audio: ${filename}` : 'Audio';
|
|
62
|
+
}
|
|
@@ -78,21 +78,28 @@ renderer (skip if it already exists — never overwrite):
|
|
|
78
78
|
|-------------------------|------------------------------|
|
|
79
79
|
| `assets/react/MediaRenderer.tsx` *(react)* | `src/cms/react/MediaRenderer.tsx` |
|
|
80
80
|
| `assets/angular/MediaRenderer.component.ts` *(angular)* | `src/cms/angular/MediaRenderer.component.ts` |
|
|
81
|
+
| `assets/shared/mediaLabels.ts` | `src/cms/shared/mediaLabels.ts` |
|
|
82
|
+
|
|
83
|
+
Both `MediaRenderer.tsx` and `MediaRenderer.component.ts` import `deriveDownloadLabel`/
|
|
84
|
+
`effectiveAudioLabel` from `../shared/mediaLabels`, so `mediaLabels.ts` MUST be copied
|
|
85
|
+
in the same pass as the renderer — copying the renderer without it leaves an
|
|
86
|
+
unresolved import and breaks the app's build. Skip if it already exists, exactly like
|
|
87
|
+
the renderer files.
|
|
81
88
|
|
|
82
89
|
The media types (`CmsMediaBody`, `CmsMediaField`, `CmsMediaType`) ship inside the
|
|
83
90
|
always-written `shared/cmsCore.types.ts`, so a media embed onto an existing runtime
|
|
84
|
-
needs only the
|
|
91
|
+
needs only the renderer, `mediaLabels.ts`, and the ref entry — provided the runtime's
|
|
85
92
|
`cmsCore.types.ts` carries the media exports (a pre-media scaffold that lacks them is
|
|
86
93
|
Init drift on that file; HALT, don't silently patch). On the `contentKey` path URL
|
|
87
94
|
resolution is entirely toolkit-owned (`resolveCmsImageUrl` / `resolveMediaUrl`); no
|
|
88
95
|
local prefix helper ships. On the foreign `url` path the renderer uses `ref.url`
|
|
89
96
|
directly — no resolver, no fetch (Rule 1/Rule 2).
|
|
90
97
|
|
|
91
|
-
**The media renderer
|
|
92
|
-
unchanged: a runtime scaffolded before media existed is still
|
|
93
|
-
runtime, and
|
|
94
|
-
like a per-type `<type>.ts`. Do not add `MediaRenderer.*`
|
|
95
|
-
every pre-media app would mis-classify as drift.
|
|
98
|
+
**The media renderer and `mediaLabels.ts` are NOT in the foundation sets below.**
|
|
99
|
+
Init/Embed detection is unchanged: a runtime scaffolded before media existed is still
|
|
100
|
+
a complete `embed` runtime, and both files are written lazily on the first media embed
|
|
101
|
+
exactly like a per-type `<type>.ts`. Do not add `MediaRenderer.*` or `mediaLabels.ts`
|
|
102
|
+
to the detection sets, or every pre-media app would mis-classify as drift.
|
|
96
103
|
|
|
97
104
|
## Foundation sets (used by Init vs Embed detection)
|
|
98
105
|
|
|
@@ -175,7 +175,7 @@ A new component built from this workflow is "done" only when **every** item belo
|
|
|
175
175
|
- Optional: add o11y instrumentation as a separate pass once the component stabilizes.
|
|
176
176
|
- `experience-lwc-typescript-migrate` — optional once JS is green.
|
|
177
177
|
- When the new component ships behind a flag, gate it with a feature flag during rollout.
|
|
178
|
-
-
|
|
178
|
+
- Generate API-surface documentation once the public API is stable (no dedicated skill for this yet).
|
|
179
179
|
- Org-aware inputs used by this workflow:
|
|
180
180
|
- Figma URL + screenshot (and, if available, the developer's Dev Mode metadata export) — Phase 1.2 Figma input.
|
|
181
181
|
- `experience-lds-data-requirements-generate` owns the org-schema introspection and data-spec validation used in Phase 1 when the component needs org-backed data — hand off to that skill rather than duplicating its work here.
|