@narumitw/pi-webui 0.20.2 → 0.22.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/README.md +44 -12
- package/package.json +3 -1
- package/src/attachments.ts +715 -0
- package/src/conversation.ts +23 -6
- package/src/drafts.ts +226 -0
- package/src/image-limits.ts +36 -0
- package/src/image-pipeline.ts +325 -0
- package/src/images.ts +182 -140
- package/src/pi-settings.ts +11 -6
- package/src/runtime.ts +138 -34
- package/src/sent-images.ts +279 -0
- package/src/server.ts +490 -30
- package/src/settings.ts +102 -7
- package/src/vendor.d.ts +30 -0
- package/src/web/app.js +558 -100
- package/src/web/image-drag.js +86 -0
- package/src/web/index.html +25 -2
- package/src/web/state.js +178 -8
- package/src/web/styles.css +176 -11
- package/src/web/transcript.js +66 -9
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const IMAGE_DRAG_TYPE = "application/x-pi-webui-image";
|
|
2
|
+
|
|
3
|
+
export function createImageDragController(previews, { isLocked, onDrop }) {
|
|
4
|
+
let draggedId = "";
|
|
5
|
+
|
|
6
|
+
function clearDropTargets() {
|
|
7
|
+
previews.classList.remove("vertical-drop");
|
|
8
|
+
for (const candidate of previews.children) {
|
|
9
|
+
candidate.classList.remove("drag-before", "drag-after");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function setDropTarget(item, after, vertical) {
|
|
14
|
+
previews.classList.toggle("vertical-drop", vertical);
|
|
15
|
+
for (const candidate of previews.children) {
|
|
16
|
+
candidate.classList.toggle("drag-before", candidate === item && !after);
|
|
17
|
+
candidate.classList.toggle("drag-after", candidate === item && after);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function focus(id) {
|
|
22
|
+
requestAnimationFrame(() => {
|
|
23
|
+
const escapedId = CSS.escape(id);
|
|
24
|
+
const item = previews.querySelector(`[data-image-id="${escapedId}"]`);
|
|
25
|
+
(item?.tabIndex === 0 ? item : item?.querySelector(".remove-image"))?.focus();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function bind(item, { id, orderingLocked }) {
|
|
30
|
+
item.addEventListener("dragstart", (event) => {
|
|
31
|
+
if (isLocked() || orderingLocked || !event.dataTransfer) return;
|
|
32
|
+
draggedId = id;
|
|
33
|
+
event.dataTransfer.effectAllowed = "move";
|
|
34
|
+
event.dataTransfer.setData(IMAGE_DRAG_TYPE, id);
|
|
35
|
+
item.classList.add("dragging");
|
|
36
|
+
});
|
|
37
|
+
item.addEventListener("dragend", () => {
|
|
38
|
+
draggedId = "";
|
|
39
|
+
item.classList.remove("dragging");
|
|
40
|
+
clearDropTargets();
|
|
41
|
+
});
|
|
42
|
+
item.addEventListener("dragover", (event) => {
|
|
43
|
+
const sourceId = draggedId || event.dataTransfer?.getData(IMAGE_DRAG_TYPE);
|
|
44
|
+
if (isLocked() || orderingLocked || !sourceId) return;
|
|
45
|
+
if (sourceId === id) {
|
|
46
|
+
clearDropTargets();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
51
|
+
const vertical = imagesStackVertically(previews.children);
|
|
52
|
+
setDropTarget(item, dropAfterTarget(event, item, vertical), vertical);
|
|
53
|
+
});
|
|
54
|
+
item.addEventListener("dragleave", (event) => {
|
|
55
|
+
if (event.relatedTarget && item.contains(event.relatedTarget)) return;
|
|
56
|
+
item.classList.remove("drag-before", "drag-after");
|
|
57
|
+
});
|
|
58
|
+
item.addEventListener("drop", (event) => {
|
|
59
|
+
const sourceId = draggedId || event.dataTransfer?.getData(IMAGE_DRAG_TYPE);
|
|
60
|
+
if (isLocked() || orderingLocked || !sourceId || sourceId === id) return;
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
event.stopPropagation();
|
|
63
|
+
const vertical = imagesStackVertically(previews.children);
|
|
64
|
+
const after = dropAfterTarget(event, item, vertical);
|
|
65
|
+
draggedId = "";
|
|
66
|
+
clearDropTargets();
|
|
67
|
+
onDrop({ sourceId, targetId: id, after });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { bind, clearDropTargets, focus };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function imagesStackVertically(items) {
|
|
75
|
+
const list = [...items];
|
|
76
|
+
return !list.some((item, index) =>
|
|
77
|
+
list.slice(index + 1).some((candidate) => Math.abs(item.offsetTop - candidate.offsetTop) < 2),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function dropAfterTarget(event, item, vertical) {
|
|
82
|
+
const bounds = item.getBoundingClientRect();
|
|
83
|
+
return vertical
|
|
84
|
+
? event.clientY >= bounds.top + bounds.height / 2
|
|
85
|
+
: event.clientX >= bounds.left + bounds.width / 2;
|
|
86
|
+
}
|
package/src/web/index.html
CHANGED
|
@@ -55,8 +55,15 @@
|
|
|
55
55
|
rows="1"
|
|
56
56
|
placeholder="Ask Pi to do something…"
|
|
57
57
|
></textarea>
|
|
58
|
+
<p id="attachment-summary" class="attachment-summary" hidden></p>
|
|
58
59
|
<ul id="image-previews" class="image-previews" aria-label="Attached images"></ul>
|
|
59
|
-
<p
|
|
60
|
+
<p
|
|
61
|
+
id="attachment-announcement"
|
|
62
|
+
class="visually-hidden"
|
|
63
|
+
role="status"
|
|
64
|
+
aria-live="polite"
|
|
65
|
+
aria-atomic="true"
|
|
66
|
+
></p>
|
|
60
67
|
<p id="composer-error" class="composer-error" role="alert" hidden></p>
|
|
61
68
|
<div class="composer-bottom">
|
|
62
69
|
<div class="composer-support">
|
|
@@ -64,11 +71,14 @@
|
|
|
64
71
|
<input
|
|
65
72
|
id="image-input"
|
|
66
73
|
type="file"
|
|
67
|
-
accept="image/png,image/jpeg,image/webp,image/gif"
|
|
74
|
+
accept="image/png,image/jpeg,image/webp,image/gif,image/bmp,image/tiff,image/heic,image/heif,image/avif,.bmp,.tif,.tiff,.heic,.heif,.avif"
|
|
68
75
|
multiple
|
|
69
76
|
hidden
|
|
70
77
|
/>
|
|
71
78
|
<span class="image-hint">Paste, drop, or choose images.</span>
|
|
79
|
+
<button id="clear-attachments" class="button secondary" type="button" hidden>
|
|
80
|
+
Clear attachments
|
|
81
|
+
</button>
|
|
72
82
|
</div>
|
|
73
83
|
<div class="send-actions">
|
|
74
84
|
<button id="steer" class="button secondary" type="button" hidden>Steer</button>
|
|
@@ -91,6 +101,19 @@
|
|
|
91
101
|
</details>
|
|
92
102
|
</footer>
|
|
93
103
|
|
|
104
|
+
<dialog id="clear-attachments-dialog" class="confirmation-dialog" aria-labelledby="clear-attachments-title">
|
|
105
|
+
<form method="dialog">
|
|
106
|
+
<h2 id="clear-attachments-title">Clear attachments?</h2>
|
|
107
|
+
<p id="clear-attachments-message">Remove all unsent image attachments? Message text will be kept.</p>
|
|
108
|
+
<div class="dialog-actions">
|
|
109
|
+
<button type="submit" value="cancel">Cancel</button>
|
|
110
|
+
<button id="confirm-clear-attachments" class="danger" type="submit" value="confirm">
|
|
111
|
+
Clear attachments
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</form>
|
|
115
|
+
</dialog>
|
|
116
|
+
|
|
94
117
|
<dialog id="image-preview-dialog" class="image-preview-dialog" aria-labelledby="image-preview-title">
|
|
95
118
|
<div class="image-preview-content">
|
|
96
119
|
<header>
|
package/src/web/state.js
CHANGED
|
@@ -13,6 +13,20 @@ export function initialState() {
|
|
|
13
13
|
readingImages: 0,
|
|
14
14
|
leaseClaimed: false,
|
|
15
15
|
leaseGeneration: 0,
|
|
16
|
+
draftRevision: 0,
|
|
17
|
+
authoritativeText: "",
|
|
18
|
+
textDirty: false,
|
|
19
|
+
attachmentRevision: 0,
|
|
20
|
+
attachmentPhase: "empty",
|
|
21
|
+
imageLimits: undefined,
|
|
22
|
+
sentImages: {
|
|
23
|
+
revision: 0,
|
|
24
|
+
enabled: false,
|
|
25
|
+
items: [],
|
|
26
|
+
totalBytes: 0,
|
|
27
|
+
maxImages: 0,
|
|
28
|
+
maxBytes: 0,
|
|
29
|
+
},
|
|
16
30
|
following: true,
|
|
17
31
|
unseenUpdateIds: [],
|
|
18
32
|
text: "",
|
|
@@ -22,17 +36,130 @@ export function initialState() {
|
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
export function applySnapshot(current, snapshot) {
|
|
25
|
-
|
|
39
|
+
let next = current;
|
|
40
|
+
if (Number.isSafeInteger(snapshot?.sequence) && snapshot.sequence >= current.sequence) {
|
|
41
|
+
next = {
|
|
42
|
+
...current,
|
|
43
|
+
sequence: snapshot.sequence,
|
|
44
|
+
session: snapshot.session,
|
|
45
|
+
messages: Array.isArray(snapshot.messages) ? snapshot.messages : [],
|
|
46
|
+
tools: Array.isArray(snapshot.tools) ? snapshot.tools : [],
|
|
47
|
+
activity: snapshot.activity ?? "idle",
|
|
48
|
+
closed: Boolean(snapshot.closed),
|
|
49
|
+
needsSnapshot: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return applySentImages(
|
|
53
|
+
applyImageLimits(
|
|
54
|
+
applyAttachments(applyDraft(next, snapshot?.draft), snapshot?.attachments),
|
|
55
|
+
snapshot?.imageLimits,
|
|
56
|
+
),
|
|
57
|
+
snapshot?.sentImages,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function applyDraft(current, draft) {
|
|
62
|
+
if (
|
|
63
|
+
!Number.isSafeInteger(draft?.revision) ||
|
|
64
|
+
draft.revision < current.draftRevision ||
|
|
65
|
+
typeof draft.text !== "string"
|
|
66
|
+
) {
|
|
26
67
|
return current;
|
|
68
|
+
}
|
|
69
|
+
const preserveLocal = current.textDirty && current.text !== draft.text;
|
|
27
70
|
return {
|
|
28
71
|
...current,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
72
|
+
draftRevision: draft.revision,
|
|
73
|
+
authoritativeText: draft.text,
|
|
74
|
+
text: preserveLocal ? current.text : draft.text,
|
|
75
|
+
textDirty: preserveLocal,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function editDraftText(current, text) {
|
|
80
|
+
return invalidateSendAttempt({
|
|
81
|
+
...current,
|
|
82
|
+
text,
|
|
83
|
+
textDirty: text !== current.authoritativeText,
|
|
84
|
+
error: "",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function acknowledgeDraftText(current, draft, submittedText) {
|
|
89
|
+
if (current.text === submittedText) {
|
|
90
|
+
return applyDraft({ ...current, textDirty: false }, draft);
|
|
91
|
+
}
|
|
92
|
+
return applyDraft(current, draft);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function applyImageLimits(current, limits) {
|
|
96
|
+
if (
|
|
97
|
+
!limits ||
|
|
98
|
+
!["maxImages", "maxImageBytes", "maxBatchBytes", "maxImagePixels"].every(
|
|
99
|
+
(key) => Number.isSafeInteger(limits[key]) && limits[key] > 0,
|
|
100
|
+
)
|
|
101
|
+
) {
|
|
102
|
+
return current;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...current,
|
|
106
|
+
imageLimits: {
|
|
107
|
+
maxImages: limits.maxImages,
|
|
108
|
+
maxImageBytes: limits.maxImageBytes,
|
|
109
|
+
maxBatchBytes: limits.maxBatchBytes,
|
|
110
|
+
maxImagePixels: limits.maxImagePixels,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function applySentImages(current, sentImages) {
|
|
116
|
+
if (
|
|
117
|
+
!Number.isSafeInteger(sentImages?.revision) ||
|
|
118
|
+
sentImages.revision < current.sentImages.revision ||
|
|
119
|
+
!Array.isArray(sentImages.items)
|
|
120
|
+
) {
|
|
121
|
+
return current;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...current,
|
|
125
|
+
sentImages: {
|
|
126
|
+
revision: sentImages.revision,
|
|
127
|
+
enabled: Boolean(sentImages.enabled),
|
|
128
|
+
items: sentImages.items
|
|
129
|
+
.filter((item) => item && typeof item.id === "string")
|
|
130
|
+
.map((item) => ({ ...item })),
|
|
131
|
+
totalBytes: Number.isSafeInteger(sentImages.totalBytes) ? sentImages.totalBytes : 0,
|
|
132
|
+
maxImages: Number.isSafeInteger(sentImages.maxImages) ? sentImages.maxImages : 0,
|
|
133
|
+
maxBytes: Number.isSafeInteger(sentImages.maxBytes) ? sentImages.maxBytes : 0,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function applyAttachments(current, attachments) {
|
|
139
|
+
if (
|
|
140
|
+
!Number.isSafeInteger(attachments?.revision) ||
|
|
141
|
+
attachments.revision < current.attachmentRevision ||
|
|
142
|
+
!Array.isArray(attachments.items)
|
|
143
|
+
) {
|
|
144
|
+
return current;
|
|
145
|
+
}
|
|
146
|
+
const revisionChanged = attachments.revision !== current.attachmentRevision;
|
|
147
|
+
const images = attachments.items
|
|
148
|
+
.filter((item) => item && typeof item.id === "string" && typeof item.status === "string")
|
|
149
|
+
.map((item) => ({
|
|
150
|
+
...item,
|
|
151
|
+
notes: Array.isArray(item.notes) ? [...item.notes] : [],
|
|
152
|
+
}));
|
|
153
|
+
return {
|
|
154
|
+
...current,
|
|
155
|
+
attachmentRevision: attachments.revision,
|
|
156
|
+
attachmentPhase: typeof attachments.phase === "string" ? attachments.phase : "blocked",
|
|
157
|
+
images,
|
|
158
|
+
readingImages: images.filter(
|
|
159
|
+
(image) => image.status === "uploading" || image.status === "processing",
|
|
160
|
+
).length,
|
|
161
|
+
outbox: revisionChanged ? undefined : current.outbox,
|
|
162
|
+
lastDelivery: revisionChanged ? undefined : current.lastDelivery,
|
|
36
163
|
};
|
|
37
164
|
}
|
|
38
165
|
|
|
@@ -82,6 +209,9 @@ export function prepareSend(current, requestId, delivery = "next") {
|
|
|
82
209
|
const attempt = current.outbox ?? {
|
|
83
210
|
requestId,
|
|
84
211
|
text: current.text,
|
|
212
|
+
draftRevision: current.draftRevision,
|
|
213
|
+
attachmentRevision: current.attachmentRevision,
|
|
214
|
+
attachmentIds: current.images.map((image) => image.id),
|
|
85
215
|
images: [...current.images],
|
|
86
216
|
delivery,
|
|
87
217
|
};
|
|
@@ -117,6 +247,10 @@ export function invalidateSendAttempt(current) {
|
|
|
117
247
|
return { ...current, outbox: undefined, lastDelivery: undefined };
|
|
118
248
|
}
|
|
119
249
|
|
|
250
|
+
export function clearDraftImages(current) {
|
|
251
|
+
return invalidateSendAttempt({ ...current, images: [], error: "" });
|
|
252
|
+
}
|
|
253
|
+
|
|
120
254
|
export function setNearBottom(current, nearBottom) {
|
|
121
255
|
if (!nearBottom) return current.following ? { ...current, following: false } : current;
|
|
122
256
|
if (current.following && current.unseenUpdateIds.length === 0) return current;
|
|
@@ -132,6 +266,41 @@ export function followLatest(current) {
|
|
|
132
266
|
return { ...current, following: true, unseenUpdateIds: [] };
|
|
133
267
|
}
|
|
134
268
|
|
|
269
|
+
export function moveImage(images, id, direction) {
|
|
270
|
+
const from = images.findIndex((image) => image.id === id);
|
|
271
|
+
if (from === -1) return [...images];
|
|
272
|
+
const to = Math.max(0, Math.min(images.length - 1, from + direction));
|
|
273
|
+
if (to === from) return [...images];
|
|
274
|
+
const next = [...images];
|
|
275
|
+
const [image] = next.splice(from, 1);
|
|
276
|
+
if (image) next.splice(to, 0, image);
|
|
277
|
+
return next;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function moveImageBefore(images, id, targetId) {
|
|
281
|
+
if (id === targetId) return [...images];
|
|
282
|
+
const from = images.findIndex((image) => image.id === id);
|
|
283
|
+
const target = images.findIndex((image) => image.id === targetId);
|
|
284
|
+
if (from === -1 || target === -1) return [...images];
|
|
285
|
+
const next = images.filter((image) => image.id !== id);
|
|
286
|
+
const targetIndex = next.findIndex((image) => image.id === targetId);
|
|
287
|
+
if (targetIndex === -1) return [...images];
|
|
288
|
+
next.splice(targetIndex, 0, images[from]);
|
|
289
|
+
return next;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function moveImageAfter(images, id, targetId) {
|
|
293
|
+
if (id === targetId) return [...images];
|
|
294
|
+
const from = images.findIndex((image) => image.id === id);
|
|
295
|
+
const target = images.findIndex((image) => image.id === targetId);
|
|
296
|
+
if (from === -1 || target === -1) return [...images];
|
|
297
|
+
const next = images.filter((image) => image.id !== id);
|
|
298
|
+
const targetIndex = next.findIndex((image) => image.id === targetId);
|
|
299
|
+
if (targetIndex === -1) return [...images];
|
|
300
|
+
next.splice(targetIndex + 1, 0, images[from]);
|
|
301
|
+
return next;
|
|
302
|
+
}
|
|
303
|
+
|
|
135
304
|
export function upsertById(items, value) {
|
|
136
305
|
const index = items.findIndex((item) => item.id === value.id);
|
|
137
306
|
if (index < 0) return [...items, value];
|
|
@@ -147,6 +316,7 @@ export function canSend(current) {
|
|
|
147
316
|
!current.stale &&
|
|
148
317
|
!current.pending &&
|
|
149
318
|
current.readingImages === 0 &&
|
|
319
|
+
(current.images.length === 0 || current.attachmentPhase === "ready") &&
|
|
150
320
|
(current.text.trim() || current.images.length > 0),
|
|
151
321
|
);
|
|
152
322
|
}
|
package/src/web/styles.css
CHANGED
|
@@ -301,14 +301,33 @@ main {
|
|
|
301
301
|
display: inline-flex;
|
|
302
302
|
min-height: 32px;
|
|
303
303
|
align-items: center;
|
|
304
|
+
flex-wrap: wrap;
|
|
305
|
+
gap: 0.35rem;
|
|
304
306
|
margin-top: 0.65rem;
|
|
305
307
|
padding: 0.25rem 0.55rem;
|
|
306
308
|
border: 1px solid var(--border);
|
|
307
|
-
border-radius:
|
|
309
|
+
border-radius: 0.75rem;
|
|
308
310
|
color: var(--muted);
|
|
309
311
|
font-size: 0.82rem;
|
|
310
312
|
}
|
|
311
313
|
|
|
314
|
+
.message-image-action {
|
|
315
|
+
min-height: 44px;
|
|
316
|
+
padding-inline: 0.7rem;
|
|
317
|
+
border-color: var(--border);
|
|
318
|
+
background: var(--surface);
|
|
319
|
+
color: var(--text);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.message-image-action.subtle,
|
|
323
|
+
.message-image-expired {
|
|
324
|
+
color: var(--muted);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.message-image-action.subtle {
|
|
328
|
+
background: transparent;
|
|
329
|
+
}
|
|
330
|
+
|
|
312
331
|
details {
|
|
313
332
|
max-width: 100%;
|
|
314
333
|
}
|
|
@@ -463,6 +482,12 @@ textarea {
|
|
|
463
482
|
overflow-y: auto;
|
|
464
483
|
}
|
|
465
484
|
|
|
485
|
+
textarea:focus-visible {
|
|
486
|
+
border-color: transparent;
|
|
487
|
+
outline-width: 2px;
|
|
488
|
+
outline-offset: 1px;
|
|
489
|
+
}
|
|
490
|
+
|
|
466
491
|
textarea:disabled {
|
|
467
492
|
opacity: 0.68;
|
|
468
493
|
}
|
|
@@ -490,22 +515,22 @@ textarea:disabled {
|
|
|
490
515
|
|
|
491
516
|
.composer-status,
|
|
492
517
|
.composer-error,
|
|
493
|
-
.attachment-
|
|
518
|
+
.attachment-summary {
|
|
494
519
|
margin: 0;
|
|
495
520
|
font-size: 0.84rem;
|
|
496
521
|
}
|
|
497
522
|
|
|
498
523
|
.composer-status,
|
|
499
|
-
.attachment-
|
|
524
|
+
.attachment-summary {
|
|
500
525
|
color: var(--muted);
|
|
501
526
|
}
|
|
502
527
|
|
|
503
528
|
.image-previews {
|
|
504
529
|
display: flex;
|
|
530
|
+
flex-wrap: wrap;
|
|
505
531
|
gap: 0.6rem;
|
|
506
532
|
margin: 0;
|
|
507
533
|
padding: 0;
|
|
508
|
-
overflow-x: auto;
|
|
509
534
|
list-style: none;
|
|
510
535
|
}
|
|
511
536
|
|
|
@@ -515,14 +540,85 @@ textarea:disabled {
|
|
|
515
540
|
|
|
516
541
|
.image-preview-item {
|
|
517
542
|
display: grid;
|
|
518
|
-
|
|
519
|
-
|
|
543
|
+
width: fit-content;
|
|
544
|
+
max-width: 100%;
|
|
545
|
+
min-width: 0;
|
|
546
|
+
flex: 0 1 auto;
|
|
547
|
+
grid-template-columns: 68px fit-content(14rem) auto;
|
|
520
548
|
align-items: center;
|
|
521
549
|
gap: 0.5rem;
|
|
522
550
|
padding: 0.4rem;
|
|
523
551
|
border: 1px solid var(--border);
|
|
524
552
|
border-radius: 10px;
|
|
525
553
|
background: var(--surface-secondary);
|
|
554
|
+
transition:
|
|
555
|
+
border-color 0.12s ease,
|
|
556
|
+
box-shadow 0.12s ease,
|
|
557
|
+
transform 0.12s ease;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.image-preview-item[draggable="true"] {
|
|
561
|
+
cursor: grab;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.image-preview-item[draggable="true"]:active {
|
|
565
|
+
cursor: grabbing;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.image-preview-item.dragging {
|
|
569
|
+
opacity: 0.5;
|
|
570
|
+
transform: scale(0.985);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.image-preview-item.drag-before {
|
|
574
|
+
border-color: var(--accent);
|
|
575
|
+
box-shadow: inset 3px 0 var(--accent);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.image-preview-item.drag-after {
|
|
579
|
+
border-color: var(--accent);
|
|
580
|
+
box-shadow: inset -3px 0 var(--accent);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.image-previews.vertical-drop .image-preview-item.drag-before {
|
|
584
|
+
box-shadow: inset 0 3px var(--accent);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.image-previews.vertical-drop .image-preview-item.drag-after {
|
|
588
|
+
box-shadow: inset 0 -3px var(--accent);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.attachment-details {
|
|
592
|
+
display: grid;
|
|
593
|
+
gap: 0.2rem;
|
|
594
|
+
min-width: 0;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.attachment-item-status {
|
|
598
|
+
color: var(--muted);
|
|
599
|
+
font-size: 0.78rem;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.attachment-item-status.error {
|
|
603
|
+
color: var(--danger);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.attachment-conversion-summary,
|
|
607
|
+
.attachment-order-context {
|
|
608
|
+
color: var(--muted);
|
|
609
|
+
font-size: 0.74rem;
|
|
610
|
+
line-height: 1.35;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.attachment-order-context {
|
|
614
|
+
font-weight: 650;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.image-actions {
|
|
618
|
+
display: flex;
|
|
619
|
+
align-items: center;
|
|
620
|
+
justify-content: flex-end;
|
|
621
|
+
gap: 0.35rem;
|
|
526
622
|
}
|
|
527
623
|
|
|
528
624
|
.attachment-preview {
|
|
@@ -538,6 +634,12 @@ textarea:disabled {
|
|
|
538
634
|
cursor: zoom-in;
|
|
539
635
|
}
|
|
540
636
|
|
|
637
|
+
.attachment-preview:empty::after {
|
|
638
|
+
content: "…";
|
|
639
|
+
color: var(--muted);
|
|
640
|
+
font-size: 1.4rem;
|
|
641
|
+
}
|
|
642
|
+
|
|
541
643
|
.image-previews img {
|
|
542
644
|
width: 100%;
|
|
543
645
|
height: 100%;
|
|
@@ -579,12 +681,38 @@ button {
|
|
|
579
681
|
}
|
|
580
682
|
|
|
581
683
|
.button.secondary,
|
|
582
|
-
.remove-image
|
|
684
|
+
.remove-image,
|
|
685
|
+
.retry-image {
|
|
583
686
|
border-color: var(--border);
|
|
584
687
|
color: var(--text);
|
|
585
688
|
background: var(--surface);
|
|
586
689
|
}
|
|
587
690
|
|
|
691
|
+
.remove-image {
|
|
692
|
+
display: grid;
|
|
693
|
+
width: 44px;
|
|
694
|
+
padding: 0;
|
|
695
|
+
place-items: center;
|
|
696
|
+
color: var(--muted);
|
|
697
|
+
background: transparent;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.remove-image:hover:not(:disabled) {
|
|
701
|
+
border-color: var(--danger);
|
|
702
|
+
color: var(--danger);
|
|
703
|
+
background: var(--danger-soft);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.remove-image svg {
|
|
707
|
+
width: 20px;
|
|
708
|
+
height: 20px;
|
|
709
|
+
fill: none;
|
|
710
|
+
stroke: currentcolor;
|
|
711
|
+
stroke-linecap: round;
|
|
712
|
+
stroke-linejoin: round;
|
|
713
|
+
stroke-width: 1.8;
|
|
714
|
+
}
|
|
715
|
+
|
|
588
716
|
button:disabled,
|
|
589
717
|
.button.disabled,
|
|
590
718
|
.button.primary:disabled {
|
|
@@ -595,6 +723,11 @@ button:disabled,
|
|
|
595
723
|
border-color: var(--border);
|
|
596
724
|
}
|
|
597
725
|
|
|
726
|
+
.remove-image:disabled {
|
|
727
|
+
border-color: transparent;
|
|
728
|
+
background: transparent;
|
|
729
|
+
}
|
|
730
|
+
|
|
598
731
|
.button.disabled {
|
|
599
732
|
pointer-events: none;
|
|
600
733
|
}
|
|
@@ -609,6 +742,42 @@ footer p {
|
|
|
609
742
|
margin-top: 0;
|
|
610
743
|
}
|
|
611
744
|
|
|
745
|
+
.confirmation-dialog {
|
|
746
|
+
width: min(92vw, 30rem);
|
|
747
|
+
border: 1px solid var(--border);
|
|
748
|
+
border-radius: 14px;
|
|
749
|
+
padding: 1.25rem;
|
|
750
|
+
color: var(--text);
|
|
751
|
+
background: var(--surface);
|
|
752
|
+
box-shadow: var(--shadow);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.confirmation-dialog::backdrop {
|
|
756
|
+
background: rgb(11 17 30 / 72%);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.confirmation-dialog h2 {
|
|
760
|
+
margin-block: 0 0.5rem;
|
|
761
|
+
font-size: 1.15rem;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.confirmation-dialog p {
|
|
765
|
+
margin-block: 0 1rem;
|
|
766
|
+
color: var(--muted);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.dialog-actions {
|
|
770
|
+
display: flex;
|
|
771
|
+
justify-content: flex-end;
|
|
772
|
+
gap: 0.6rem;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
.danger {
|
|
776
|
+
border-color: var(--danger);
|
|
777
|
+
color: var(--danger);
|
|
778
|
+
background: var(--danger-soft);
|
|
779
|
+
}
|
|
780
|
+
|
|
612
781
|
.image-preview-dialog {
|
|
613
782
|
width: min(96vw, 1400px);
|
|
614
783
|
max-width: none;
|
|
@@ -740,10 +909,6 @@ footer p {
|
|
|
740
909
|
.send-actions {
|
|
741
910
|
flex: 0 1 auto;
|
|
742
911
|
}
|
|
743
|
-
|
|
744
|
-
.image-preview-item {
|
|
745
|
-
min-width: min(100%, 270px);
|
|
746
|
-
}
|
|
747
912
|
}
|
|
748
913
|
|
|
749
914
|
@media (prefers-color-scheme: dark) {
|