@stellartech/image-style-widget-directus 1.0.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/dist/index.js +1667 -0
- package/package.json +24 -0
- package/src/components/EditModal.vue +237 -0
- package/src/components/ImageCard.vue +380 -0
- package/src/components/StyleCard.vue +258 -0
- package/src/composables/useDirectusApi.ts +493 -0
- package/src/index.ts +44 -0
- package/src/interface.vue +970 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { useApi } from '@directus/extensions-sdk';
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
|
|
4
|
+
export interface Style {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
example_1: string | null;
|
|
9
|
+
example_2: string | null;
|
|
10
|
+
is_active: boolean;
|
|
11
|
+
sort: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RegenerateResponse {
|
|
15
|
+
file_id: string;
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ContentImage {
|
|
20
|
+
id: number;
|
|
21
|
+
image: string | null; // file UUID
|
|
22
|
+
prompt: string | null;
|
|
23
|
+
status: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ImageVariant {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string; // placeholder/caption
|
|
29
|
+
status: string;
|
|
30
|
+
lesson?: string;
|
|
31
|
+
images?: ContentImage[]; // related ContentImage items
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GeneratedImageSet {
|
|
35
|
+
placeholder: string;
|
|
36
|
+
prompt: string;
|
|
37
|
+
image1: string | null;
|
|
38
|
+
image2: string | null;
|
|
39
|
+
selected: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Response from the prompt generation service
|
|
43
|
+
export interface ImagePrompt {
|
|
44
|
+
placeholder: string;
|
|
45
|
+
prompts: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PromptGenerationResponse {
|
|
49
|
+
images: ImagePrompt[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Supported image generation models
|
|
53
|
+
export type ImageModel = 'flux-kontext-pro' | 'gemini-2.5-flash-image';
|
|
54
|
+
|
|
55
|
+
export const IMAGE_MODELS: { id: ImageModel; name: string }[] = [
|
|
56
|
+
{ id: 'flux-kontext-pro', name: 'Flux' },
|
|
57
|
+
{ id: 'gemini-2.5-flash-image', name: 'Nano Banana' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const MISC_SERVICE_URL = 'http://localhost:8004';
|
|
61
|
+
|
|
62
|
+
export function useDirectusApi() {
|
|
63
|
+
const api = useApi();
|
|
64
|
+
const loading = ref(false);
|
|
65
|
+
const error = ref<string | null>(null);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Fetch all active styles from the ImageStyles collection
|
|
69
|
+
*/
|
|
70
|
+
async function fetchStyles(collection: string = 'ImageStyles'): Promise<Style[]> {
|
|
71
|
+
loading.value = true;
|
|
72
|
+
error.value = null;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const response = await api.get(`/items/${collection}`, {
|
|
76
|
+
params: {
|
|
77
|
+
filter: { is_active: { _eq: true } },
|
|
78
|
+
sort: ['sort'],
|
|
79
|
+
fields: ['id', 'name', 'prompt', 'example_1', 'example_2', 'is_active', 'sort'],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
return response.data.data || [];
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
error.value = e.message || 'Failed to fetch styles';
|
|
85
|
+
return [];
|
|
86
|
+
} finally {
|
|
87
|
+
loading.value = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Update a style's prompt
|
|
93
|
+
*/
|
|
94
|
+
async function updateStylePrompt(collection: string, styleId: string, newPrompt: string): Promise<boolean> {
|
|
95
|
+
loading.value = true;
|
|
96
|
+
error.value = null;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await api.patch(`/items/${collection}/${styleId}`, {
|
|
100
|
+
prompt: newPrompt,
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
} catch (e: any) {
|
|
104
|
+
error.value = e.message || 'Failed to update prompt';
|
|
105
|
+
return false;
|
|
106
|
+
} finally {
|
|
107
|
+
loading.value = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Helper function to generate a single image via misc service
|
|
113
|
+
*/
|
|
114
|
+
async function generateSingleImage(prompt: string, model: ImageModel = 'flux-kontext-pro'): Promise<string | null> {
|
|
115
|
+
const miscServiceUrl = `${MISC_SERVICE_URL}/api/misc/images/upload`;
|
|
116
|
+
|
|
117
|
+
const response = await fetch(miscServiceUrl, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
prompt,
|
|
124
|
+
model,
|
|
125
|
+
format: 'png',
|
|
126
|
+
aspect_ratio: '16:9',
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(`Image generation failed: ${response.status}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
return data?.id || null; // Backend returns 'id' not 'file_id'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Regenerate example images for a style
|
|
140
|
+
*/
|
|
141
|
+
async function regenerateStyleExamples(
|
|
142
|
+
styleId: string,
|
|
143
|
+
prompt: string,
|
|
144
|
+
collection: string = 'ImageStyles',
|
|
145
|
+
model: ImageModel = 'flux-kontext-pro'
|
|
146
|
+
): Promise<{ example_1: string; example_2: string } | null> {
|
|
147
|
+
loading.value = true;
|
|
148
|
+
error.value = null;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Generate two new images via misc service directly
|
|
152
|
+
const [example_1, example_2] = await Promise.all([
|
|
153
|
+
generateSingleImage(prompt, model),
|
|
154
|
+
generateSingleImage(prompt, model),
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
if (example_1 && example_2) {
|
|
158
|
+
// Update the style with new example images
|
|
159
|
+
await api.patch(`/items/${collection}/${styleId}`, {
|
|
160
|
+
example_1,
|
|
161
|
+
example_2,
|
|
162
|
+
});
|
|
163
|
+
return { example_1, example_2 };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
error.value = 'Failed to generate images';
|
|
167
|
+
return null;
|
|
168
|
+
} catch (e: any) {
|
|
169
|
+
error.value = e.message || 'Failed to regenerate images';
|
|
170
|
+
return null;
|
|
171
|
+
} finally {
|
|
172
|
+
loading.value = false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Regenerate images for a specific placeholder using both prompts
|
|
178
|
+
*/
|
|
179
|
+
async function regeneratePlaceholderImages(
|
|
180
|
+
prompt1: string,
|
|
181
|
+
prompt2: string,
|
|
182
|
+
model: ImageModel = 'flux-kontext-pro'
|
|
183
|
+
): Promise<{ image1: string; image2: string } | null> {
|
|
184
|
+
loading.value = true;
|
|
185
|
+
error.value = null;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// Generate one image per prompt
|
|
189
|
+
const [image1, image2] = await Promise.all([
|
|
190
|
+
generateSingleImage(prompt1, model),
|
|
191
|
+
generateSingleImage(prompt2, model),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
if (image1 && image2) {
|
|
195
|
+
return { image1, image2 };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
error.value = 'Failed to generate images';
|
|
199
|
+
return null;
|
|
200
|
+
} catch (e: any) {
|
|
201
|
+
error.value = e.message || 'Failed to regenerate images';
|
|
202
|
+
return null;
|
|
203
|
+
} finally {
|
|
204
|
+
loading.value = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the URL for a file by its ID
|
|
210
|
+
*/
|
|
211
|
+
function getFileUrl(fileId: string | null): string {
|
|
212
|
+
if (!fileId) return '';
|
|
213
|
+
return `/assets/${fileId}?width=200&height=200&fit=cover`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generate image prompts using the Python misc service
|
|
218
|
+
* Calls the misc service directly (not through Directus)
|
|
219
|
+
*/
|
|
220
|
+
async function generateImagePrompts(
|
|
221
|
+
style: string,
|
|
222
|
+
description: string
|
|
223
|
+
): Promise<PromptGenerationResponse | null> {
|
|
224
|
+
loading.value = true;
|
|
225
|
+
error.value = null;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const miscServiceUrl = `${MISC_SERVICE_URL}/api/misc/prompts`;
|
|
229
|
+
|
|
230
|
+
const response = await fetch(miscServiceUrl, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: {
|
|
233
|
+
'Content-Type': 'application/json',
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify({
|
|
236
|
+
style,
|
|
237
|
+
description,
|
|
238
|
+
model: 'gpt-4o-mini'
|
|
239
|
+
})
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
const errorData = await response.json().catch(() => ({ detail: response.statusText }));
|
|
244
|
+
throw new Error(errorData.detail || `HTTP ${response.status}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return await response.json() as PromptGenerationResponse;
|
|
248
|
+
} catch (e: any) {
|
|
249
|
+
error.value = e.message || 'Failed to generate image prompts';
|
|
250
|
+
return null;
|
|
251
|
+
} finally {
|
|
252
|
+
loading.value = false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get ImageVariants created for a specific lesson, including their images
|
|
258
|
+
*/
|
|
259
|
+
async function getImageVariants(lessonId: string): Promise<ImageVariant[]> {
|
|
260
|
+
loading.value = true;
|
|
261
|
+
error.value = null;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const response = await api.get('/items/ImageVariants', {
|
|
265
|
+
params: {
|
|
266
|
+
filter: { lesson: { _eq: lessonId } },
|
|
267
|
+
fields: ['id', 'title', 'status', 'lesson', 'images.id', 'images.image', 'images.prompt', 'images.status'],
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
return response.data.data || [];
|
|
271
|
+
} catch (e: any) {
|
|
272
|
+
error.value = e.message || 'Failed to fetch image variants';
|
|
273
|
+
return [];
|
|
274
|
+
} finally {
|
|
275
|
+
loading.value = false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Save generated images to ImageVariants and ContentImage collections
|
|
281
|
+
*/
|
|
282
|
+
async function saveGeneratedImages(
|
|
283
|
+
lessonId: string,
|
|
284
|
+
items: Array<{
|
|
285
|
+
placeholder: string;
|
|
286
|
+
prompt1: string;
|
|
287
|
+
prompt2: string;
|
|
288
|
+
image1: string | null;
|
|
289
|
+
image2: string | null;
|
|
290
|
+
}>
|
|
291
|
+
): Promise<boolean> {
|
|
292
|
+
loading.value = true;
|
|
293
|
+
error.value = null;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
for (const item of items) {
|
|
297
|
+
// Create ImageVariant record
|
|
298
|
+
const variantResponse = await api.post('/items/ImageVariants', {
|
|
299
|
+
title: item.placeholder,
|
|
300
|
+
lesson: lessonId,
|
|
301
|
+
status: 'found',
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const variantId = variantResponse.data.data.id;
|
|
305
|
+
|
|
306
|
+
// Create ContentImage records for each image
|
|
307
|
+
const contentImages = [];
|
|
308
|
+
|
|
309
|
+
if (item.image1) {
|
|
310
|
+
contentImages.push({
|
|
311
|
+
image: item.image1,
|
|
312
|
+
prompt: item.prompt1,
|
|
313
|
+
variants: variantId,
|
|
314
|
+
status: 'generated',
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (item.image2) {
|
|
319
|
+
contentImages.push({
|
|
320
|
+
image: item.image2,
|
|
321
|
+
prompt: item.prompt2,
|
|
322
|
+
variants: variantId,
|
|
323
|
+
status: 'generated',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Batch create ContentImage records
|
|
328
|
+
if (contentImages.length > 0) {
|
|
329
|
+
await api.post('/items/ContentImage', contentImages);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return true;
|
|
334
|
+
} catch (e: any) {
|
|
335
|
+
error.value = e.message || 'Failed to save generated images';
|
|
336
|
+
return false;
|
|
337
|
+
} finally {
|
|
338
|
+
loading.value = false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Delete an ImageVariant and its associated ContentImage records and files
|
|
344
|
+
*/
|
|
345
|
+
async function deleteImageVariant(lessonId: string, placeholder: string): Promise<boolean> {
|
|
346
|
+
loading.value = true;
|
|
347
|
+
error.value = null;
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
// Find the ImageVariant by lesson and title (placeholder), including image file IDs
|
|
351
|
+
const response = await api.get('/items/ImageVariants', {
|
|
352
|
+
params: {
|
|
353
|
+
filter: {
|
|
354
|
+
lesson: { _eq: lessonId },
|
|
355
|
+
title: { _eq: placeholder }
|
|
356
|
+
},
|
|
357
|
+
fields: ['id', 'images.id', 'images.image'],
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const variants = response.data.data || [];
|
|
362
|
+
|
|
363
|
+
for (const variant of variants) {
|
|
364
|
+
// Collect file IDs to delete
|
|
365
|
+
const fileIdsToDelete: string[] = [];
|
|
366
|
+
|
|
367
|
+
// Delete associated ContentImage records first
|
|
368
|
+
if (variant.images && variant.images.length > 0) {
|
|
369
|
+
for (const img of variant.images) {
|
|
370
|
+
// Collect the file ID if present
|
|
371
|
+
if (img.image) {
|
|
372
|
+
fileIdsToDelete.push(img.image);
|
|
373
|
+
}
|
|
374
|
+
// Delete the ContentImage record
|
|
375
|
+
await api.delete(`/items/ContentImage/${img.id}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Delete the ImageVariant
|
|
380
|
+
await api.delete(`/items/ImageVariants/${variant.id}`);
|
|
381
|
+
|
|
382
|
+
// Delete the actual files from directus_files
|
|
383
|
+
for (const fileId of fileIdsToDelete) {
|
|
384
|
+
try {
|
|
385
|
+
await api.delete(`/files/${fileId}`);
|
|
386
|
+
} catch (fileError: any) {
|
|
387
|
+
// Log but don't fail if file deletion fails (file might already be deleted)
|
|
388
|
+
console.warn(`Failed to delete file ${fileId}:`, fileError.message);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return true;
|
|
394
|
+
} catch (e: any) {
|
|
395
|
+
error.value = e.message || 'Failed to delete image variant';
|
|
396
|
+
return false;
|
|
397
|
+
} finally {
|
|
398
|
+
loading.value = false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Extract [placeholder] patterns from content
|
|
404
|
+
*/
|
|
405
|
+
function extractPlaceholders(content: string): string[] {
|
|
406
|
+
const regex = /\[([^\]]+)\]/g;
|
|
407
|
+
const matches: string[] = [];
|
|
408
|
+
let match;
|
|
409
|
+
while ((match = regex.exec(content)) !== null) {
|
|
410
|
+
matches.push(match[1]);
|
|
411
|
+
}
|
|
412
|
+
return matches;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Update lesson content by replacing [placeholder] with HTML image tag
|
|
417
|
+
* Uses HTML format for WYSIWYG compatibility
|
|
418
|
+
*/
|
|
419
|
+
function updateContentWithImages(
|
|
420
|
+
content: string,
|
|
421
|
+
selections: { caption: string; fileId: string }[]
|
|
422
|
+
): string {
|
|
423
|
+
let updated = content;
|
|
424
|
+
for (const { caption, fileId } of selections) {
|
|
425
|
+
const placeholder = `[${caption}]`;
|
|
426
|
+
const html = `<img src="/assets/${fileId}" alt="${caption}" />`;
|
|
427
|
+
updated = updated.replace(placeholder, html);
|
|
428
|
+
}
|
|
429
|
+
return updated;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Save updated content back to the lesson
|
|
434
|
+
* Note: field name is 'description' (lowercase) in Directus schema
|
|
435
|
+
*/
|
|
436
|
+
async function updateLessonContent(
|
|
437
|
+
lessonId: string,
|
|
438
|
+
content: string
|
|
439
|
+
): Promise<boolean> {
|
|
440
|
+
loading.value = true;
|
|
441
|
+
error.value = null;
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await api.patch(`/items/SM_Lessons/${lessonId}`, {
|
|
445
|
+
description: content,
|
|
446
|
+
});
|
|
447
|
+
return true;
|
|
448
|
+
} catch (e: any) {
|
|
449
|
+
error.value = e.message || 'Failed to update lesson content';
|
|
450
|
+
return false;
|
|
451
|
+
} finally {
|
|
452
|
+
loading.value = false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Fetch lesson content from the API
|
|
458
|
+
* This is more reliable than relying on inject('values') context
|
|
459
|
+
*/
|
|
460
|
+
async function fetchLessonContent(lessonId: string): Promise<string | null> {
|
|
461
|
+
try {
|
|
462
|
+
const response = await api.get(`/items/SM_Lessons/${lessonId}`, {
|
|
463
|
+
params: {
|
|
464
|
+
fields: ['description'],
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
// Try both lowercase and capitalized field names for compatibility
|
|
468
|
+
return response.data.data?.description || response.data.data?.Description || null;
|
|
469
|
+
} catch (e: any) {
|
|
470
|
+
console.error('Failed to fetch lesson content:', e);
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
loading,
|
|
477
|
+
error,
|
|
478
|
+
fetchStyles,
|
|
479
|
+
updateStylePrompt,
|
|
480
|
+
regenerateStyleExamples,
|
|
481
|
+
regeneratePlaceholderImages,
|
|
482
|
+
getFileUrl,
|
|
483
|
+
generateImagePrompts,
|
|
484
|
+
getImageVariants,
|
|
485
|
+
saveGeneratedImages,
|
|
486
|
+
deleteImageVariant,
|
|
487
|
+
extractPlaceholders,
|
|
488
|
+
generateSingleImage,
|
|
489
|
+
updateContentWithImages,
|
|
490
|
+
updateLessonContent,
|
|
491
|
+
fetchLessonContent,
|
|
492
|
+
};
|
|
493
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineInterface } from '@directus/extensions-sdk';
|
|
2
|
+
import InterfaceComponent from './interface.vue';
|
|
3
|
+
|
|
4
|
+
export default defineInterface({
|
|
5
|
+
id: 'image-style-widget',
|
|
6
|
+
name: 'Image Style Widget',
|
|
7
|
+
icon: 'palette',
|
|
8
|
+
description: 'Visual style selector and image reviewer',
|
|
9
|
+
component: InterfaceComponent,
|
|
10
|
+
options: [
|
|
11
|
+
{
|
|
12
|
+
field: 'mode',
|
|
13
|
+
name: 'Mode',
|
|
14
|
+
type: 'string',
|
|
15
|
+
meta: {
|
|
16
|
+
width: 'half',
|
|
17
|
+
interface: 'select-dropdown',
|
|
18
|
+
options: {
|
|
19
|
+
choices: [
|
|
20
|
+
{ text: 'Style Selection', value: 'style-select' },
|
|
21
|
+
{ text: 'Image Review', value: 'image-review' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
schema: {
|
|
26
|
+
default_value: 'style-select',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
field: 'stylesCollection',
|
|
31
|
+
name: 'Styles Collection',
|
|
32
|
+
type: 'string',
|
|
33
|
+
meta: {
|
|
34
|
+
width: 'half',
|
|
35
|
+
interface: 'input',
|
|
36
|
+
note: 'Collection containing style definitions (default: ImageStyles)',
|
|
37
|
+
},
|
|
38
|
+
schema: {
|
|
39
|
+
default_value: 'ImageStyles',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
types: ['json', 'string'],
|
|
44
|
+
});
|