@jay-framework/wix-media 0.19.6 → 0.20.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.
@@ -28,11 +28,28 @@ URL format: `https://static.wixstatic.com/media/{mediaId}/v1/{mode}/{params}/fil
28
28
  - **crop** — extract a rectangle at specific coordinates
29
29
  `<img src="https://static.wixstatic.com/media/{mediaId}/v1/crop/x_100,y_50,w_800,h_600/file.jpg" alt="" />`
30
30
 
31
+ #### Crop + Resize (chained transforms)
32
+
33
+ Add a `/crop/` segment before `/fill/` (or `/fit/`) to first extract a region, then resize it:
34
+
35
+ ```
36
+ /v1/crop/x_{x},y_{y},w_{cropW},h_{cropH}/fill/w_{outputW},h_{outputH}/file.ext
37
+ ```
38
+
39
+ Example — crop a 2688×1278 region starting at (0, 514), then fill-resize to 1437×683:
40
+
41
+ ```html
42
+ <img src="https://static.wixstatic.com/media/{mediaId}/v1/crop/x_0,y_514,w_2688,h_1278/fill/w_1437,h_683/file.jpg" alt="" />
43
+ ```
44
+
45
+ The crop coordinates refer to the original image pixels. The fill/fit parameters set the final output size.
46
+
31
47
  #### Parameters
32
48
 
33
49
  - `w_{width}` — target width (1–5000 px)
34
50
  - `h_{height}` — target height (1–5000 px)
35
51
  - `x_{x},y_{y}` — crop start position (crop mode only)
52
+ - `w_{width},h_{height}` — crop region size (crop mode / chained crop)
36
53
  - `q_{quality}` — JPEG quality (1–100)
37
54
 
38
55
  #### Output format (set via file extension)
package/dist/index.d.ts CHANGED
@@ -16,9 +16,48 @@ interface MediaFileInfo {
16
16
  folderId: string;
17
17
  folderName: string;
18
18
  }
19
+ interface UploadUrlResult {
20
+ uploadUrl: string;
21
+ }
22
+ interface WixMediaService {
23
+ listPublicFiles(): Promise<MediaFileInfo[]>;
24
+ generateUploadUrl(mimeType: string, fileName: string): Promise<UploadUrlResult>;
25
+ }
19
26
 
20
27
  declare function generateMediaIndex(files: MediaFileInfo[]): string;
21
28
 
29
+ /**
30
+ * Generate Add Menu items for indexed Wix Media Manager files (Design Log #19 M19.2).
31
+ */
32
+
33
+ interface MediaAddMenuInteraction {
34
+ mode: 'stage-place' | 'reference';
35
+ persistOnPage?: boolean;
36
+ stagePromptTemplate?: string;
37
+ }
38
+ interface MediaAddMenuItem {
39
+ id: string;
40
+ title: string;
41
+ category: string;
42
+ subCategory: string;
43
+ pluginName: string;
44
+ packageName: string;
45
+ thumbnail?: string;
46
+ prompt: string;
47
+ interaction?: MediaAddMenuInteraction;
48
+ }
49
+ /** Preview URL for picker chips — raster images, vectors, and SVGs served from Wix CDN. */
50
+ declare function thumbnailUrlForMedia(file: MediaFileInfo): string | undefined;
51
+ declare function buildMediaAddMenuItems(files: MediaFileInfo[]): MediaAddMenuItem[];
52
+
53
+ declare const ADD_MENU_GENERATED_REL = "agent-kit/aiditor/add-menu/wix-media.generated.yaml";
54
+ declare function writeGeneratedAddMenuCatalog(projectRoot: string, items: MediaAddMenuItem[]): string;
55
+
56
+ declare function refreshMediaAddMenuCatalog(projectRoot: string, mediaService: WixMediaService): Promise<{
57
+ itemCount: number;
58
+ outputRel: string;
59
+ }>;
60
+
22
61
  declare function setupWixMedia(ctx: PluginSetupContext): Promise<PluginSetupResult>;
23
62
  declare function generateWixMediaReferences(ctx: PluginReferencesContext): Promise<PluginReferencesResult>;
24
63
 
@@ -32,4 +71,4 @@ declare const uploadPublic: _jay_framework_fullstack_component.JayCliCommand<{
32
71
  dryRun?: boolean;
33
72
  }, [_jay_framework_fullstack_component.ConsoleContext]>;
34
73
 
35
- export { generateMediaIndex, generateWixMediaReferences, rebuildIndex, setupWixMedia, uploadPublic, validate };
74
+ export { ADD_MENU_GENERATED_REL, buildMediaAddMenuItems, generateMediaIndex, generateWixMediaReferences, rebuildIndex, refreshMediaAddMenuCatalog, setupWixMedia, thumbnailUrlForMedia, uploadPublic, validate, writeGeneratedAddMenuCatalog };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { walkElements, resolveBinding } from "@jay-framework/compiler-shared";
2
2
  import { parseTemplateParts } from "@jay-framework/compiler-jay-html";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
+ import * as yaml from "js-yaml";
5
6
  import { registerService, getService } from "@jay-framework/stack-server-runtime";
6
7
  import { WIX_CLIENT_SERVICE } from "@jay-framework/wix-server-client";
7
8
  import { files, folders } from "@wix/media";
@@ -12,6 +13,8 @@ const WIXSTATIC_MEDIA_RE = /static\.wixstatic\.com\/media\//;
12
13
  const V1_TRANSFORM_RE = /\/v1\//;
13
14
  const IMAGE_EXTENSIONS_RE = /\.(jpe?g|png|gif|webp|svg|bmp|ico)$/i;
14
15
  const SRCSET_UNENCODED_COMMA_RE = /\/v1\/[^/]+\/[^/]*[a-z]_\d+,[a-z]_\d+/;
16
+ const WIDTH_PARAM_RE = /w_(\d+)/;
17
+ const SRCSET_MIN_WIDTH_THRESHOLD = 400;
15
18
  const MEDIA_SRC_ATTRS = ["src", "poster"];
16
19
  const MEDIA_SRCSET_ATTRS = ["srcset"];
17
20
  const MEDIA_ELEMENTS = /* @__PURE__ */ new Set(["img", "video", "source"]);
@@ -80,6 +83,24 @@ const validate = (ctx) => {
80
83
  }
81
84
  }
82
85
  }
86
+ if (tagName === "img" && !el.getAttribute("srcset")) {
87
+ const srcValue = el.getAttribute("src");
88
+ if (srcValue) {
89
+ const srcParts = parseTemplateParts(srcValue);
90
+ const staticText = srcParts.filter((p) => p.kind === "static").map((p) => p.value).join("");
91
+ if (V1_TRANSFORM_RE.test(staticText)) {
92
+ const widthMatch = staticText.match(WIDTH_PARAM_RE);
93
+ if (widthMatch && parseInt(widthMatch[1], 10) >= SRCSET_MIN_WIDTH_THRESHOLD) {
94
+ findings.push({
95
+ severity: "warning",
96
+ message: "This image is ≥400px wide but has no srcset attribute. Add srcset with sizes for common breakpoints to serve appropriately sized images across devices. See agent-kit/wix-media.md for the responsive image pattern.",
97
+ element: "<img>",
98
+ attribute: "src"
99
+ });
100
+ }
101
+ }
102
+ }
103
+ }
83
104
  for (const attr of MEDIA_SRCSET_ATTRS) {
84
105
  const value = el.getAttribute(attr);
85
106
  if (!value) continue;
@@ -100,7 +121,7 @@ const validate = (ctx) => {
100
121
  });
101
122
  return findings;
102
123
  };
103
- function formatDimensions(file) {
124
+ function formatDimensions$1(file) {
104
125
  if (file.width && file.height) {
105
126
  return `${file.width}x${file.height}`;
106
127
  }
@@ -120,12 +141,134 @@ function generateMediaIndex(files2) {
120
141
  lines.push("| ------ | ---- | ------------ | ---- | ---------- | ------ | --- |");
121
142
  for (const file of files2) {
122
143
  lines.push(
123
- `| ${escapeMarkdownCell(file.folderName)} | ${file.slug} | ${escapeMarkdownCell(file.displayName)} | ${file.mediaType} | ${formatDimensions(file)} | ${formatLabels(file.labels)} | ${file.url} |`
144
+ `| ${escapeMarkdownCell(file.folderName)} | ${file.slug} | ${escapeMarkdownCell(file.displayName)} | ${file.mediaType} | ${formatDimensions$1(file)} | ${formatLabels(file.labels)} | ${file.url} |`
124
145
  );
125
146
  }
126
147
  lines.push("");
127
148
  return lines.join("\n");
128
149
  }
150
+ const CATEGORY = "Media";
151
+ function subCategoryForMediaType(mediaType) {
152
+ switch (mediaType.toLowerCase()) {
153
+ case "image":
154
+ return "Images";
155
+ case "video":
156
+ return "Videos";
157
+ case "audio":
158
+ return "Audio";
159
+ case "document":
160
+ return "Documents";
161
+ default:
162
+ return "Other";
163
+ }
164
+ }
165
+ function uniqueItemId(slug, mediaId, usedIds) {
166
+ const base = slug.trim() || mediaId.slice(0, 8);
167
+ let id = `wix-media:${base}`;
168
+ let suffix = 2;
169
+ while (usedIds.has(id)) {
170
+ id = `wix-media:${base}-${suffix++}`;
171
+ }
172
+ usedIds.add(id);
173
+ return id;
174
+ }
175
+ function formatDimensions(file) {
176
+ if (file.width && file.height) {
177
+ return `${file.width}×${file.height}`;
178
+ }
179
+ return "-";
180
+ }
181
+ function formatTitle(file) {
182
+ const folder = file.folderName?.trim();
183
+ if (folder && folder !== "Media Root" && folder !== "Unknown") {
184
+ return `${folder} — ${file.displayName}`;
185
+ }
186
+ return file.displayName;
187
+ }
188
+ const VISUAL_FILE_EXTENSION_RE = /\.(svg|png|jpe?g|gif|webp|bmp|ico)($|[?#])/i;
189
+ function hasVisualFileExtension(file) {
190
+ return VISUAL_FILE_EXTENSION_RE.test(`${file.displayName} ${file.url}`);
191
+ }
192
+ function isPreviewableVisualMedia(file) {
193
+ const type = file.mediaType.toLowerCase();
194
+ if (type === "image" || type === "vector") return true;
195
+ return hasVisualFileExtension(file);
196
+ }
197
+ function thumbnailUrlForMedia(file) {
198
+ const url = file.url.trim();
199
+ if (!url || !isPreviewableVisualMedia(file)) return void 0;
200
+ return url;
201
+ }
202
+ function stagePlaceInteractionForMedia(file) {
203
+ const mediaType = file.mediaType.toLowerCase();
204
+ if (mediaType === "image" || mediaType === "vector" || isPreviewableVisualMedia(file)) {
205
+ return {
206
+ mode: "stage-place",
207
+ persistOnPage: true,
208
+ stagePromptTemplate: `Place this image at the marker location on the page.
209
+ URL: ${file.url}`
210
+ };
211
+ }
212
+ if (mediaType === "video") {
213
+ return {
214
+ mode: "stage-place",
215
+ persistOnPage: true,
216
+ stagePromptTemplate: `Place this video at the marker location on the page.
217
+ URL: ${file.url}`
218
+ };
219
+ }
220
+ return void 0;
221
+ }
222
+ function buildMediaPrompt(file) {
223
+ const labels = file.labels.length > 0 ? `Labels: ${file.labels.join(", ")}
224
+ ` : "";
225
+ const folderLine = file.folderName && file.folderName !== "Unknown" ? `Folder: ${file.folderName}
226
+ ` : "";
227
+ return [
228
+ "Use this Wix Media Manager asset in jay-html (do not copy to public/):",
229
+ `URL: ${file.url}`,
230
+ `Media id: ${file.id}`,
231
+ `Slug: ${file.slug}`,
232
+ `Type: ${file.mediaType} · ${formatDimensions(file)}`,
233
+ folderLine.trimEnd(),
234
+ labels.trimEnd(),
235
+ "Full media index: agent-kit/references/wix-media/MEDIA-INDEX.md",
236
+ "See agent-kit/designer/wix-media.md for fit/fill/crop transforms."
237
+ ].filter((line) => line.length > 0).join("\n");
238
+ }
239
+ function buildMediaAddMenuItems(files2) {
240
+ const usedIds = /* @__PURE__ */ new Set();
241
+ return files2.map((file) => {
242
+ const thumbnail = thumbnailUrlForMedia(file);
243
+ const interaction = stagePlaceInteractionForMedia(file);
244
+ return {
245
+ id: uniqueItemId(file.slug, file.id, usedIds),
246
+ title: formatTitle(file),
247
+ category: CATEGORY,
248
+ subCategory: subCategoryForMediaType(file.mediaType),
249
+ pluginName: "wix-media",
250
+ packageName: "@jay-framework/wix-media",
251
+ ...thumbnail ? { thumbnail } : {},
252
+ ...interaction ? { interaction } : {},
253
+ prompt: buildMediaPrompt(file)
254
+ };
255
+ });
256
+ }
257
+ const ADD_MENU_GENERATED_REL = "agent-kit/aiditor/add-menu/wix-media.generated.yaml";
258
+ function writeGeneratedAddMenuCatalog(projectRoot, items) {
259
+ const outputPath = path.join(projectRoot, ADD_MENU_GENERATED_REL);
260
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
261
+ const header = "# DO NOT EDIT BY HAND — regenerated by wix-media (setup / rebuild-index)\n\n";
262
+ const body = yaml.dump({ items }, { indent: 2, lineWidth: 120, noRefs: true });
263
+ fs.writeFileSync(outputPath, header + body, "utf-8");
264
+ return ADD_MENU_GENERATED_REL;
265
+ }
266
+ async function refreshMediaAddMenuCatalog(projectRoot, mediaService) {
267
+ const files2 = await mediaService.listPublicFiles();
268
+ const items = buildMediaAddMenuItems(files2);
269
+ const outputRel = writeGeneratedAddMenuCatalog(projectRoot, items);
270
+ return { itemCount: items.length, outputRel };
271
+ }
129
272
  const WIX_MEDIA_SERVICE_MARKER = createJayService("Wix Media Service");
130
273
  let filesClientInstance;
131
274
  let foldersClientInstance;
@@ -288,9 +431,14 @@ async function generateWixMediaReferences(ctx) {
288
431
  const mediaIndexContent = generateMediaIndex(mediaFiles);
289
432
  const mediaIndexPath = path.join(ctx.referencesDir, "MEDIA-INDEX.md");
290
433
  fs.writeFileSync(mediaIndexPath, mediaIndexContent, "utf-8");
434
+ const addMenuItems = buildMediaAddMenuItems(mediaFiles);
435
+ const addMenuGenerated = writeGeneratedAddMenuCatalog(ctx.projectRoot, addMenuItems);
291
436
  return {
292
- referencesCreated: [`agent-kit/references/${ctx.pluginName}/MEDIA-INDEX.md`],
293
- message: `${mediaFiles.length} media files indexed.`
437
+ referencesCreated: [
438
+ `agent-kit/references/${ctx.pluginName}/MEDIA-INDEX.md`,
439
+ addMenuGenerated
440
+ ],
441
+ message: `${mediaFiles.length} media files indexed; ${addMenuItems.length} Add Menu items.`
294
442
  };
295
443
  }
296
444
  const rebuildIndex = makeCliCommand("rebuild-index").withServices(CONSOLE_CONTEXT).withHandler(async (input, console) => {
@@ -309,8 +457,13 @@ const rebuildIndex = makeCliCommand("rebuild-index").withServices(CONSOLE_CONTEX
309
457
  const indexContent = generateMediaIndex(files2);
310
458
  const indexPath = path$1.join(referencesDir, "MEDIA-INDEX.md");
311
459
  fs$1.writeFileSync(indexPath, indexContent, "utf-8");
460
+ const addMenuPath = writeGeneratedAddMenuCatalog(
461
+ console.projectRoot,
462
+ buildMediaAddMenuItems(files2)
463
+ );
312
464
  console.log(`Media index written to ${indexPath}`);
313
- return { success: true };
465
+ console.log(`Add Menu catalog written to ${addMenuPath}`);
466
+ return { success: true, fileCount: files2.length, addMenuPath };
314
467
  });
315
468
  const UPLOAD_INDEX_FILE = ".wix-media-uploads.json";
316
469
  const MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -450,10 +603,15 @@ const uploadPublic = makeCliCommand("upload-public").withServices(CONSOLE_CONTEX
450
603
  return { success: failed === 0 };
451
604
  });
452
605
  export {
606
+ ADD_MENU_GENERATED_REL,
607
+ buildMediaAddMenuItems,
453
608
  generateMediaIndex,
454
609
  generateWixMediaReferences,
455
610
  rebuildIndex,
611
+ refreshMediaAddMenuCatalog,
456
612
  setupWixMedia,
613
+ thumbnailUrlForMedia,
457
614
  uploadPublic,
458
- validate
615
+ validate,
616
+ writeGeneratedAddMenuCatalog
459
617
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jay-framework/wix-media",
3
- "version": "0.19.6",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "description": "Wix Media Manager plugin for Jay Framework — media validation, index generation, and agent-kit references",
6
6
  "license": "Apache-2.0",
@@ -25,19 +25,20 @@
25
25
  "test": "vitest run"
26
26
  },
27
27
  "dependencies": {
28
- "@jay-framework/compiler-jay-html": "^0.19.6",
29
- "@jay-framework/compiler-shared": "^0.19.6",
30
- "@jay-framework/component": "^0.19.6",
31
- "@jay-framework/fullstack-component": "^0.19.6",
32
- "@jay-framework/stack-server-runtime": "^0.19.6",
33
- "@jay-framework/wix-server-client": "^0.19.6",
34
- "@jay-framework/wix-utils": "^0.19.6",
28
+ "@jay-framework/compiler-jay-html": "^0.20.0",
29
+ "@jay-framework/compiler-shared": "^0.20.0",
30
+ "@jay-framework/component": "^0.20.0",
31
+ "@jay-framework/fullstack-component": "^0.20.0",
32
+ "@jay-framework/stack-server-runtime": "^0.20.0",
33
+ "@jay-framework/wix-server-client": "^0.20.0",
34
+ "@jay-framework/wix-utils": "^0.20.0",
35
35
  "@wix/media": "^1.0.247",
36
- "@wix/sdk": "^1.21.5"
36
+ "@wix/sdk": "^1.21.5",
37
+ "js-yaml": "^4.1.0"
37
38
  },
38
39
  "devDependencies": {
39
- "@jay-framework/compiler-jay-stack": "^0.19.6",
40
- "@jay-framework/vite-plugin": "^0.19.6",
40
+ "@jay-framework/compiler-jay-stack": "^0.20.0",
41
+ "@jay-framework/vite-plugin": "^0.20.0",
41
42
  "@types/node": "^20.11.0",
42
43
  "node-html-parser": "^6.1.0",
43
44
  "rimraf": "^5.0.5",