@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/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@stellartech/image-style-widget-directus",
3
+ "version": "1.0.0",
4
+ "description": "Visual style selector and image reviewer for Directus",
5
+ "type": "module",
6
+ "directus:extension": {
7
+ "type": "interface",
8
+ "path": "dist/index.js",
9
+ "source": "src/index.ts",
10
+ "host": "^11.0.0"
11
+ },
12
+ "scripts": {
13
+ "build": "directus-extension build --no-minify",
14
+ "dev": "directus-extension build -w --no-minify"
15
+ },
16
+ "devDependencies": {
17
+ "@directus/extensions-sdk": "^12.0.0",
18
+ "typescript": "^5.0.0",
19
+ "vue": "^3.4.0"
20
+ },
21
+ "peerDependencies": {
22
+ "vue": "^3.4.0"
23
+ }
24
+ }
@@ -0,0 +1,237 @@
1
+ <template>
2
+ <div v-if="isOpen" class="edit-modal__overlay" @click.self="$emit('close')">
3
+ <div class="edit-modal">
4
+ <div class="edit-modal__header">
5
+ <h3 class="edit-modal__title">
6
+ <v-icon name="edit" />
7
+ Edit Prompt
8
+ </h3>
9
+ <button class="edit-modal__close" @click="$emit('close')">
10
+ <v-icon name="close" />
11
+ </button>
12
+ </div>
13
+
14
+ <div class="edit-modal__body">
15
+ <label class="edit-modal__label">
16
+ {{ title }}
17
+ </label>
18
+ <textarea
19
+ ref="textareaRef"
20
+ v-model="localPrompt"
21
+ class="edit-modal__textarea"
22
+ rows="6"
23
+ :placeholder="placeholder"
24
+ ></textarea>
25
+ <p class="edit-modal__hint">
26
+ Edit the prompt text and click Save to apply changes.
27
+ </p>
28
+ </div>
29
+
30
+ <div class="edit-modal__footer">
31
+ <button
32
+ class="edit-modal__btn edit-modal__btn--secondary"
33
+ @click="$emit('close')"
34
+ >
35
+ Cancel
36
+ </button>
37
+ <button
38
+ class="edit-modal__btn edit-modal__btn--primary"
39
+ :disabled="!hasChanges || saving"
40
+ @click="handleSave"
41
+ >
42
+ <v-icon v-if="saving" name="refresh" small />
43
+ <span>{{ saving ? 'Saving...' : 'Save' }}</span>
44
+ </button>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { ref, computed, watch, nextTick } from 'vue';
52
+
53
+ const props = defineProps<{
54
+ isOpen: boolean;
55
+ title: string;
56
+ prompt: string;
57
+ placeholder?: string;
58
+ saving?: boolean;
59
+ }>();
60
+
61
+ const emit = defineEmits<{
62
+ (e: 'close'): void;
63
+ (e: 'save', newPrompt: string): void;
64
+ }>();
65
+
66
+ const localPrompt = ref(props.prompt);
67
+ const textareaRef = ref<HTMLTextAreaElement | null>(null);
68
+
69
+ const hasChanges = computed(() => localPrompt.value !== props.prompt);
70
+
71
+ // Reset local prompt when modal opens with new data
72
+ watch(() => props.prompt, (newPrompt) => {
73
+ localPrompt.value = newPrompt;
74
+ });
75
+
76
+ // Focus textarea when modal opens
77
+ watch(() => props.isOpen, async (isOpen) => {
78
+ if (isOpen) {
79
+ localPrompt.value = props.prompt;
80
+ await nextTick();
81
+ textareaRef.value?.focus();
82
+ textareaRef.value?.select();
83
+ }
84
+ });
85
+
86
+ function handleSave() {
87
+ if (hasChanges.value) {
88
+ emit('save', localPrompt.value);
89
+ }
90
+ }
91
+ </script>
92
+
93
+ <style scoped>
94
+ .edit-modal__overlay {
95
+ position: fixed;
96
+ inset: 0;
97
+ background: rgba(0, 0, 0, 0.5);
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ z-index: 1000;
102
+ padding: 20px;
103
+ }
104
+
105
+ .edit-modal {
106
+ width: 100%;
107
+ max-width: 600px;
108
+ background: var(--theme--background, #fff);
109
+ border-radius: 12px;
110
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
111
+ overflow: hidden;
112
+ }
113
+
114
+ .edit-modal__header {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: space-between;
118
+ padding: 16px 20px;
119
+ border-bottom: 1px solid var(--theme--border-color-subdued, #e0e0e0);
120
+ }
121
+
122
+ .edit-modal__title {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 8px;
126
+ margin: 0;
127
+ font-size: 18px;
128
+ font-weight: 600;
129
+ color: var(--theme--foreground, #333);
130
+ }
131
+
132
+ .edit-modal__close {
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ width: 32px;
137
+ height: 32px;
138
+ border: none;
139
+ border-radius: 6px;
140
+ background: transparent;
141
+ color: var(--theme--foreground-subdued, #666);
142
+ cursor: pointer;
143
+ transition: all 0.2s ease;
144
+ }
145
+
146
+ .edit-modal__close:hover {
147
+ background: var(--theme--background-subdued, #f5f5f5);
148
+ color: var(--theme--foreground, #333);
149
+ }
150
+
151
+ .edit-modal__body {
152
+ padding: 20px;
153
+ }
154
+
155
+ .edit-modal__label {
156
+ display: block;
157
+ margin-bottom: 8px;
158
+ font-size: 13px;
159
+ font-weight: 600;
160
+ color: var(--theme--foreground, #333);
161
+ }
162
+
163
+ .edit-modal__textarea {
164
+ width: 100%;
165
+ padding: 12px;
166
+ border: 2px solid var(--theme--border-color-subdued, #e0e0e0);
167
+ border-radius: 6px;
168
+ background: var(--theme--background, #fff);
169
+ color: var(--theme--foreground, #333);
170
+ font-size: 14px;
171
+ font-family: inherit;
172
+ line-height: 1.5;
173
+ resize: vertical;
174
+ transition: border-color 0.2s ease;
175
+ }
176
+
177
+ .edit-modal__textarea:focus {
178
+ outline: none;
179
+ border-color: var(--theme--primary, #6644ff);
180
+ }
181
+
182
+ .edit-modal__textarea::placeholder {
183
+ color: var(--theme--foreground-subdued, #999);
184
+ }
185
+
186
+ .edit-modal__hint {
187
+ margin: 8px 0 0 0;
188
+ font-size: 12px;
189
+ color: var(--theme--foreground-subdued, #666);
190
+ }
191
+
192
+ .edit-modal__footer {
193
+ display: flex;
194
+ justify-content: flex-end;
195
+ gap: 12px;
196
+ padding: 16px 20px;
197
+ border-top: 1px solid var(--theme--border-color-subdued, #e0e0e0);
198
+ background: var(--theme--background-subdued, #f9f9f9);
199
+ }
200
+
201
+ .edit-modal__btn {
202
+ display: inline-flex;
203
+ align-items: center;
204
+ gap: 6px;
205
+ padding: 10px 20px;
206
+ border: none;
207
+ border-radius: 6px;
208
+ font-size: 14px;
209
+ font-weight: 500;
210
+ cursor: pointer;
211
+ transition: all 0.2s ease;
212
+ }
213
+
214
+ .edit-modal__btn:disabled {
215
+ opacity: 0.5;
216
+ cursor: not-allowed;
217
+ }
218
+
219
+ .edit-modal__btn--secondary {
220
+ background: var(--theme--background, #fff);
221
+ color: var(--theme--foreground-subdued, #666);
222
+ border: 1px solid var(--theme--border-color, #ccc);
223
+ }
224
+
225
+ .edit-modal__btn--secondary:hover:not(:disabled) {
226
+ background: var(--theme--background-subdued, #f5f5f5);
227
+ }
228
+
229
+ .edit-modal__btn--primary {
230
+ background: var(--theme--primary, #6644ff);
231
+ color: #fff;
232
+ }
233
+
234
+ .edit-modal__btn--primary:hover:not(:disabled) {
235
+ background: var(--theme--primary-accent, #5533ee);
236
+ }
237
+ </style>
@@ -0,0 +1,380 @@
1
+ <template>
2
+ <div class="image-card">
3
+ <!-- Placeholder Header -->
4
+ <div class="image-card__header">
5
+ <v-icon name="image" />
6
+ <span class="image-card__placeholder-text">{{ item.placeholder }}</span>
7
+ </div>
8
+
9
+ <div class="image-card__body">
10
+ <!-- Images Section -->
11
+ <div class="image-card__images">
12
+ <div
13
+ class="image-card__image"
14
+ :class="{ 'image-card__image--selected': item.selected === 1 }"
15
+ @click="item.image1 && openLightbox(item.image1)"
16
+ >
17
+ <img
18
+ v-if="item.image1"
19
+ :src="getFileUrl(item.image1)"
20
+ alt="Generated image 1"
21
+ />
22
+ <div v-else class="image-card__image-placeholder">
23
+ <v-icon name="image" />
24
+ </div>
25
+ <div class="image-card__image-badge">1</div>
26
+ </div>
27
+ <div
28
+ class="image-card__image"
29
+ :class="{ 'image-card__image--selected': item.selected === 2 }"
30
+ @click="item.image2 && openLightbox(item.image2)"
31
+ >
32
+ <img
33
+ v-if="item.image2"
34
+ :src="getFileUrl(item.image2)"
35
+ alt="Generated image 2"
36
+ />
37
+ <div v-else class="image-card__image-placeholder">
38
+ <v-icon name="image" />
39
+ </div>
40
+ <div class="image-card__image-badge">2</div>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Content Section -->
45
+ <div class="image-card__content">
46
+ <!-- Prompt Display -->
47
+ <div class="image-card__prompts-section">
48
+ <div class="image-card__prompt-item">
49
+ <span class="image-card__prompt-label">Prompt:</span>
50
+ <p class="image-card__prompt">{{ truncatedPrompt1 }}</p>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- Actions -->
55
+ <div class="image-card__actions">
56
+ <button
57
+ class="image-card__btn image-card__btn--secondary"
58
+ :disabled="loading"
59
+ @click="$emit('edit', item)"
60
+ >
61
+ <v-icon name="edit" small />
62
+ <span>Edit Prompt</span>
63
+ </button>
64
+ <button
65
+ class="image-card__btn image-card__btn--secondary"
66
+ :disabled="loading"
67
+ @click="$emit('regenerate', item)"
68
+ >
69
+ <v-icon name="refresh" small />
70
+ <span>{{ loading ? 'Generating...' : 'Regenerate' }}</span>
71
+ </button>
72
+ <button
73
+ class="image-card__btn image-card__btn--danger"
74
+ :disabled="loading"
75
+ @click="$emit('delete', item)"
76
+ >
77
+ <v-icon name="delete" small />
78
+ <span>Delete</span>
79
+ </button>
80
+ </div>
81
+
82
+ <!-- Selection Buttons -->
83
+ <div class="image-card__selection">
84
+ <button
85
+ class="image-card__select-btn"
86
+ :class="{ 'image-card__select-btn--active': item.selected === 1 }"
87
+ @click="$emit('select-image', item.placeholder, 1)"
88
+ >
89
+ <v-icon :name="item.selected === 1 ? 'check_box' : 'check_box_outline_blank'" small />
90
+ <span>Select 1</span>
91
+ </button>
92
+ <button
93
+ class="image-card__select-btn"
94
+ :class="{ 'image-card__select-btn--active': item.selected === 2 }"
95
+ @click="$emit('select-image', item.placeholder, 2)"
96
+ >
97
+ <v-icon :name="item.selected === 2 ? 'check_box' : 'check_box_outline_blank'" small />
98
+ <span>Select 2</span>
99
+ </button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Lightbox Modal -->
105
+ <div v-if="enlargedImage" class="image-card__lightbox" @click="closeLightbox">
106
+ <img :src="enlargedImage" alt="Enlarged image" />
107
+ </div>
108
+ </div>
109
+ </template>
110
+
111
+ <script setup lang="ts">
112
+ import { computed, ref } from 'vue';
113
+
114
+ export interface ImageItem {
115
+ placeholder: string;
116
+ prompt1: string;
117
+ prompt2: string;
118
+ image1: string | null;
119
+ image2: string | null;
120
+ selected: 1 | 2 | null;
121
+ }
122
+
123
+ const props = defineProps<{
124
+ item: ImageItem;
125
+ loading?: boolean;
126
+ getFileUrl: (fileId: string | null) => string;
127
+ }>();
128
+
129
+ defineEmits<{
130
+ (e: 'select-image', placeholder: string, imageNum: 1 | 2): void;
131
+ (e: 'edit', item: ImageItem): void;
132
+ (e: 'regenerate', item: ImageItem): void;
133
+ (e: 'delete', item: ImageItem): void;
134
+ }>();
135
+
136
+ // Lightbox state
137
+ const enlargedImage = ref<string | null>(null);
138
+
139
+ function getOriginalFileUrl(fileId: string | null): string {
140
+ if (!fileId) return '';
141
+ return `/assets/${fileId}`;
142
+ }
143
+
144
+ function openLightbox(fileId: string) {
145
+ enlargedImage.value = getOriginalFileUrl(fileId);
146
+ }
147
+
148
+ function closeLightbox() {
149
+ enlargedImage.value = null;
150
+ }
151
+
152
+ const truncatedPrompt1 = computed(() => {
153
+ const maxLength = 120;
154
+ if (props.item.prompt1.length <= maxLength) return props.item.prompt1;
155
+ return props.item.prompt1.substring(0, maxLength) + '...';
156
+ });
157
+ </script>
158
+
159
+ <style scoped>
160
+ .image-card {
161
+ border: 2px solid var(--theme--border-color-subdued, #e0e0e0);
162
+ border-radius: 8px;
163
+ background: var(--theme--background, #fff);
164
+ overflow: hidden;
165
+ }
166
+
167
+ .image-card__header {
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 8px;
171
+ padding: 10px 16px;
172
+ background: var(--theme--background-subdued, #f5f5f5);
173
+ border-bottom: 1px solid var(--theme--border-color-subdued, #e0e0e0);
174
+ }
175
+
176
+ .image-card__placeholder-text {
177
+ font-size: 14px;
178
+ font-weight: 600;
179
+ color: var(--theme--foreground, #333);
180
+ }
181
+
182
+ .image-card__body {
183
+ display: flex;
184
+ gap: 20px;
185
+ padding: 16px;
186
+ }
187
+
188
+ .image-card__images {
189
+ display: flex;
190
+ gap: 12px;
191
+ flex-shrink: 0;
192
+ }
193
+
194
+ .image-card__image {
195
+ position: relative;
196
+ width: 100px;
197
+ height: 100px;
198
+ border-radius: 6px;
199
+ overflow: hidden;
200
+ background: var(--theme--background-subdued, #f5f5f5);
201
+ border: 3px solid transparent;
202
+ cursor: pointer;
203
+ transition: all 0.2s ease;
204
+ }
205
+
206
+ .image-card__image:hover {
207
+ border-color: var(--theme--primary-subdued, #c4b8ff);
208
+ }
209
+
210
+ .image-card__image--selected {
211
+ border-color: var(--theme--primary, #6644ff);
212
+ }
213
+
214
+ .image-card__image img {
215
+ width: 100%;
216
+ height: 100%;
217
+ object-fit: cover;
218
+ }
219
+
220
+ .image-card__image-placeholder {
221
+ width: 100%;
222
+ height: 100%;
223
+ display: flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ color: var(--theme--foreground-subdued, #999);
227
+ }
228
+
229
+ .image-card__image-badge {
230
+ position: absolute;
231
+ top: 4px;
232
+ right: 4px;
233
+ width: 20px;
234
+ height: 20px;
235
+ border-radius: 50%;
236
+ background: rgba(0, 0, 0, 0.6);
237
+ color: #fff;
238
+ font-size: 11px;
239
+ font-weight: 600;
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ }
244
+
245
+ .image-card__image--selected .image-card__image-badge {
246
+ background: var(--theme--primary, #6644ff);
247
+ }
248
+
249
+ .image-card__content {
250
+ flex: 1;
251
+ display: flex;
252
+ flex-direction: column;
253
+ gap: 12px;
254
+ min-width: 0;
255
+ }
256
+
257
+ .image-card__prompts-section {
258
+ flex: 1;
259
+ display: flex;
260
+ flex-direction: column;
261
+ gap: 8px;
262
+ }
263
+
264
+ .image-card__prompt-item {
265
+ padding: 8px;
266
+ background: var(--theme--background-subdued, #f9f9f9);
267
+ border-radius: 4px;
268
+ }
269
+
270
+ .image-card__prompt-label {
271
+ font-size: 11px;
272
+ font-weight: 600;
273
+ color: var(--theme--foreground-subdued, #666);
274
+ text-transform: uppercase;
275
+ letter-spacing: 0.5px;
276
+ }
277
+
278
+ .image-card__prompt {
279
+ margin: 4px 0 0 0;
280
+ font-size: 12px;
281
+ color: var(--theme--foreground, #333);
282
+ line-height: 1.4;
283
+ }
284
+
285
+ .image-card__actions {
286
+ display: flex;
287
+ gap: 8px;
288
+ }
289
+
290
+ .image-card__btn {
291
+ display: inline-flex;
292
+ align-items: center;
293
+ gap: 4px;
294
+ padding: 6px 12px;
295
+ border: 1px solid var(--theme--border-color, #ccc);
296
+ border-radius: 4px;
297
+ background: var(--theme--background, #fff);
298
+ color: var(--theme--foreground-subdued, #666);
299
+ font-size: 12px;
300
+ cursor: pointer;
301
+ transition: all 0.2s ease;
302
+ }
303
+
304
+ .image-card__btn:hover:not(:disabled) {
305
+ border-color: var(--theme--primary, #6644ff);
306
+ color: var(--theme--primary, #6644ff);
307
+ }
308
+
309
+ .image-card__btn:disabled {
310
+ opacity: 0.5;
311
+ cursor: not-allowed;
312
+ }
313
+
314
+ .image-card__btn--secondary {
315
+ background: transparent;
316
+ }
317
+
318
+ .image-card__btn--danger {
319
+ background: transparent;
320
+ border-color: var(--theme--danger, #f44336);
321
+ color: var(--theme--danger, #f44336);
322
+ }
323
+
324
+ .image-card__btn--danger:hover:not(:disabled) {
325
+ background: var(--theme--danger-background, #ffebee);
326
+ border-color: var(--theme--danger, #f44336);
327
+ color: var(--theme--danger, #f44336);
328
+ }
329
+
330
+ .image-card__selection {
331
+ display: flex;
332
+ gap: 8px;
333
+ }
334
+
335
+ .image-card__select-btn {
336
+ display: inline-flex;
337
+ align-items: center;
338
+ gap: 6px;
339
+ padding: 8px 16px;
340
+ border: 2px solid var(--theme--border-color, #ccc);
341
+ border-radius: 6px;
342
+ background: var(--theme--background, #fff);
343
+ color: var(--theme--foreground-subdued, #666);
344
+ font-size: 13px;
345
+ font-weight: 500;
346
+ cursor: pointer;
347
+ transition: all 0.2s ease;
348
+ }
349
+
350
+ .image-card__select-btn:hover {
351
+ border-color: var(--theme--primary-subdued, #c4b8ff);
352
+ }
353
+
354
+ .image-card__select-btn--active {
355
+ border-color: var(--theme--primary, #6644ff);
356
+ background: var(--theme--primary-background, #f5f3ff);
357
+ color: var(--theme--primary, #6644ff);
358
+ }
359
+
360
+ /* Lightbox Modal */
361
+ .image-card__lightbox {
362
+ position: fixed;
363
+ inset: 0;
364
+ background: rgba(0, 0, 0, 0.85);
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ z-index: 1000;
369
+ cursor: zoom-out;
370
+ padding: 40px;
371
+ }
372
+
373
+ .image-card__lightbox img {
374
+ max-width: 100%;
375
+ max-height: 100%;
376
+ object-fit: contain;
377
+ border-radius: 8px;
378
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
379
+ }
380
+ </style>