ai-or-die 0.1.93 → 0.1.94
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/src/public/app.js +151 -80
- package/src/public/artifact-panel.js +62 -0
- package/src/public/components/artifact-panel.css +39 -0
- package/src/public/components/keys-panel.css +171 -0
- package/src/public/components/sticky-note.css +25 -4
- package/src/public/components/terminal.css +5 -0
- package/src/public/extra-keys.js +226 -102
- package/src/public/index.html +18 -0
- package/src/public/input-overlay.js +37 -12
- package/src/public/key-encoder.js +137 -0
- package/src/public/keys-panel.js +268 -0
- package/src/public/manifest.json +2 -2
- package/src/public/mobile.css +15 -15
- package/src/public/screenshot-narrow.png +0 -0
- package/src/public/screenshot-wide.png +0 -0
- package/src/public/service-worker.js +5 -1
- package/src/public/splash/ipad11-landscape.png +0 -0
- package/src/public/splash/ipad11-portrait.png +0 -0
- package/src/public/splash/iphone16-landscape.png +0 -0
- package/src/public/splash/iphone16-portrait.png +0 -0
- package/src/public/terminal-copy.js +56 -0
- package/src/server.js +67 -65
package/package.json
CHANGED
package/src/public/app.js
CHANGED
|
@@ -402,6 +402,10 @@ class ClaudeCodeWebInterface {
|
|
|
402
402
|
});
|
|
403
403
|
|
|
404
404
|
window.addEventListener('resize', () => {
|
|
405
|
+
// Skip during a soft-keyboard transition: the keyboard controller
|
|
406
|
+
// owns the (coalesced) fit. On layout-viewport-resizing browsers a
|
|
407
|
+
// window.resize fires mid-animation and would race a competing fit.
|
|
408
|
+
if (this._inKeyboardTransition) return;
|
|
405
409
|
this.fitTerminal();
|
|
406
410
|
});
|
|
407
411
|
|
|
@@ -571,6 +575,63 @@ class ClaudeCodeWebInterface {
|
|
|
571
575
|
});
|
|
572
576
|
}
|
|
573
577
|
|
|
578
|
+
// Upload an image over HTTP (POST /api/images/upload) instead of the WS
|
|
579
|
+
// image_upload path. A real photo's base64 (~5.5 MB) exceeds the 1 MiB WS
|
|
580
|
+
// JSON guard, which force-closes the socket with 1009 — previously surfaced
|
|
581
|
+
// (wrongly) as "A voice message was rejected". HTTP has a 20 MB body limit.
|
|
582
|
+
// On success the returned temp path is injected into the terminal.
|
|
583
|
+
_uploadImage(imageData, sessionId) {
|
|
584
|
+
sessionId = sessionId || this.currentClaudeSessionId;
|
|
585
|
+
if (!sessionId) { this._imageError('No session joined'); return; }
|
|
586
|
+
const caption = imageData.caption || '';
|
|
587
|
+
const body = JSON.stringify({
|
|
588
|
+
sessionId: sessionId,
|
|
589
|
+
base64: imageData.base64,
|
|
590
|
+
mimeType: imageData.mimeType,
|
|
591
|
+
fileName: imageData.fileName || 'pasted-image.png',
|
|
592
|
+
caption: caption
|
|
593
|
+
});
|
|
594
|
+
this.authFetch('/api/images/upload', {
|
|
595
|
+
method: 'POST',
|
|
596
|
+
headers: { 'Content-Type': 'application/json' },
|
|
597
|
+
body: body
|
|
598
|
+
}).then(async (resp) => {
|
|
599
|
+
if (!resp.ok) {
|
|
600
|
+
let msg = 'HTTP ' + resp.status;
|
|
601
|
+
try { const j = await resp.json(); if (j && j.error) msg = j.error; } catch (_) {}
|
|
602
|
+
this._imageError(msg);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const data = await resp.json();
|
|
606
|
+
if (!data || !data.filePath) {
|
|
607
|
+
this._imageError('Upload succeeded but no file path was returned');
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this._injectImagePath(data.filePath, caption);
|
|
611
|
+
}).catch((e) => this._imageError((e && e.message) || 'Upload failed'));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Inject an uploaded image's temp path into the active terminal (caption +
|
|
615
|
+
// quoted path), matching the legacy image_upload_complete behavior.
|
|
616
|
+
_injectImagePath(filePath, caption) {
|
|
617
|
+
if (!filePath) return;
|
|
618
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
619
|
+
const quotedPath = '"' + normalizedPath + '"';
|
|
620
|
+
const inputText = caption ? caption + ' ' + quotedPath : quotedPath;
|
|
621
|
+
let normalized = attachClipboardHandler.normalizeLineEndings(inputText);
|
|
622
|
+
if (this.terminal && this.terminal.modes && this.terminal.modes.bracketedPasteMode) {
|
|
623
|
+
normalized = attachClipboardHandler.wrapBracketedPaste(normalized);
|
|
624
|
+
}
|
|
625
|
+
this.send({ type: 'input', data: normalized });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
_imageError(msg) {
|
|
629
|
+
if (window.feedback) window.feedback.warning('Image upload failed: ' + msg);
|
|
630
|
+
if (this.terminal) {
|
|
631
|
+
this.terminal.write('\r\n\x1b[31m[Image upload error] ' + msg + '\x1b[0m\r\n');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
574
635
|
sendEscape() {
|
|
575
636
|
// Send ESC key to terminal
|
|
576
637
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
@@ -735,6 +796,10 @@ class ClaudeCodeWebInterface {
|
|
|
735
796
|
if (termContainerEl && typeof ResizeObserver !== 'undefined') {
|
|
736
797
|
let resizeTimeout;
|
|
737
798
|
new ResizeObserver(() => {
|
|
799
|
+
// During a soft-keyboard transition the dedicated keyboard
|
|
800
|
+
// controller owns the (single, coalesced) fit — skip here so we
|
|
801
|
+
// don't fire a second competing fit mid-animation (flicker).
|
|
802
|
+
if (this._inKeyboardTransition) return;
|
|
738
803
|
clearTimeout(resizeTimeout);
|
|
739
804
|
resizeTimeout = setTimeout(() => {
|
|
740
805
|
this.fitTerminal();
|
|
@@ -758,16 +823,7 @@ class ClaudeCodeWebInterface {
|
|
|
758
823
|
this._imageHandler = window.imageHandler.attachImageHandler(
|
|
759
824
|
this.terminal, termContainer, {
|
|
760
825
|
onImageReady: (imageData) => {
|
|
761
|
-
this.
|
|
762
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
763
|
-
this.send({
|
|
764
|
-
type: 'image_upload',
|
|
765
|
-
base64: imageData.base64,
|
|
766
|
-
mimeType: imageData.mimeType,
|
|
767
|
-
fileName: imageData.fileName || 'pasted-image.png',
|
|
768
|
-
caption: imageData.caption || ''
|
|
769
|
-
});
|
|
770
|
-
}
|
|
826
|
+
this._uploadImage(imageData);
|
|
771
827
|
},
|
|
772
828
|
// Non-image files pasted from a file manager (clipboardData
|
|
773
829
|
// carries File objects) — route through the generic pipeline.
|
|
@@ -799,16 +855,7 @@ class ClaudeCodeWebInterface {
|
|
|
799
855
|
files && files.length) {
|
|
800
856
|
try {
|
|
801
857
|
window.imageHandler.showImagePreview(files[0], (imageData) => {
|
|
802
|
-
this.
|
|
803
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
804
|
-
this.send({
|
|
805
|
-
type: 'image_upload',
|
|
806
|
-
base64: imageData.base64,
|
|
807
|
-
mimeType: imageData.mimeType,
|
|
808
|
-
fileName: imageData.fileName || 'pasted-image.png',
|
|
809
|
-
caption: imageData.caption || ''
|
|
810
|
-
});
|
|
811
|
-
}
|
|
858
|
+
this._uploadImage(imageData);
|
|
812
859
|
});
|
|
813
860
|
} catch (_) { /* ignore */ }
|
|
814
861
|
}
|
|
@@ -847,8 +894,10 @@ class ClaudeCodeWebInterface {
|
|
|
847
894
|
}
|
|
848
895
|
this._ctrlModifierPending = false;
|
|
849
896
|
if (this.extraKeys) {
|
|
850
|
-
|
|
851
|
-
|
|
897
|
+
// Use the shared consume so the sticky-Ctrl 5s timeout is
|
|
898
|
+
// cleared too (a stale timeout could otherwise cancel a
|
|
899
|
+
// later Ctrl press).
|
|
900
|
+
this.extraKeys._consumeCtrl();
|
|
852
901
|
}
|
|
853
902
|
}
|
|
854
903
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
@@ -932,7 +981,13 @@ class ClaudeCodeWebInterface {
|
|
|
932
981
|
_setupExtraKeys() {
|
|
933
982
|
if (!this.isMobile || typeof ExtraKeys === 'undefined') return;
|
|
934
983
|
|
|
984
|
+
document.body.classList.add('is-mobile');
|
|
935
985
|
this.extraKeys = new ExtraKeys({ app: this });
|
|
986
|
+
// Control-mode "all keys" panel (keyboard-down). Launcher FAB is shown
|
|
987
|
+
// via the .is-mobile body class. See keys-panel.js / ADR-0037.
|
|
988
|
+
if (typeof KeysPanel !== 'undefined') {
|
|
989
|
+
this.keysPanel = new KeysPanel({ app: this });
|
|
990
|
+
}
|
|
936
991
|
this._keyboardOpen = false;
|
|
937
992
|
|
|
938
993
|
// Browsers without visualViewport (Firefox Android, Samsung Internet):
|
|
@@ -970,6 +1025,11 @@ class ClaudeCodeWebInterface {
|
|
|
970
1025
|
this._keyboardOpen = false;
|
|
971
1026
|
document.body.classList.remove('keyboard-open');
|
|
972
1027
|
this._restoreTerminalFromKeyboard();
|
|
1028
|
+
} else if (heightDiff > threshold && this._keyboardOpen) {
|
|
1029
|
+
// Keyboard already open but the viewport height changed (final
|
|
1030
|
+
// animation frame, or rotation while open) — re-apply the size so
|
|
1031
|
+
// the terminal isn't left fit against a stale intermediate height.
|
|
1032
|
+
this._updateKeyboardHeight(currentHeight);
|
|
973
1033
|
}
|
|
974
1034
|
};
|
|
975
1035
|
|
|
@@ -1045,26 +1105,65 @@ class ClaudeCodeWebInterface {
|
|
|
1045
1105
|
}
|
|
1046
1106
|
|
|
1047
1107
|
_adjustTerminalForKeyboard(availableHeight) {
|
|
1048
|
-
if (this.extraKeys)
|
|
1108
|
+
if (this.extraKeys) {
|
|
1109
|
+
this.extraKeys.show();
|
|
1110
|
+
this.extraKeys._updateRow2Visibility();
|
|
1111
|
+
}
|
|
1112
|
+
this._updateKeyboardHeight(availableHeight);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Apply (or re-apply) the terminal size for the current keyboard-open
|
|
1116
|
+
// viewport height. Called on the first threshold crossing AND on every later
|
|
1117
|
+
// visualViewport frame while the keyboard stays open (final animation frame,
|
|
1118
|
+
// or rotation) so the terminal never fits against a stale intermediate
|
|
1119
|
+
// height and extends under the keyboard. Style writes are cheap; the xterm
|
|
1120
|
+
// fit is coalesced into one rAF below.
|
|
1121
|
+
_updateKeyboardHeight(availableHeight) {
|
|
1122
|
+
this._inKeyboardTransition = true;
|
|
1049
1123
|
document.documentElement.style.setProperty('--visual-viewport-height', availableHeight + 'px');
|
|
1050
1124
|
const termEl = document.getElementById('terminal');
|
|
1051
1125
|
if (termEl) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
termEl.style.height = (availableHeight - (this.extraKeys?.container?.offsetHeight || 44)) + 'px';
|
|
1056
|
-
if (this.fitAddon) this.fitAddon.fit();
|
|
1126
|
+
const barH = (this.extraKeys && this.extraKeys.container)
|
|
1127
|
+
? (this.extraKeys.container.offsetHeight || 44) : 0;
|
|
1128
|
+
termEl.style.height = (availableHeight - barH) + 'px';
|
|
1057
1129
|
}
|
|
1130
|
+
this._scheduleKeyboardFit();
|
|
1131
|
+
this._endKeyboardTransitionSoon();
|
|
1058
1132
|
}
|
|
1059
1133
|
|
|
1060
1134
|
_restoreTerminalFromKeyboard() {
|
|
1135
|
+
this._inKeyboardTransition = true;
|
|
1061
1136
|
if (this.extraKeys) this.extraKeys.hide();
|
|
1062
1137
|
document.documentElement.style.removeProperty('--visual-viewport-height');
|
|
1063
1138
|
const termEl = document.getElementById('terminal');
|
|
1064
|
-
if (termEl)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1139
|
+
if (termEl) termEl.style.height = '';
|
|
1140
|
+
this._scheduleKeyboardFit();
|
|
1141
|
+
this._endKeyboardTransitionSoon();
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Coalesce fits during a keyboard transition into a single rAF. Uses
|
|
1145
|
+
// fitTerminal() (which applies the mobile row/column adjustments) and refits
|
|
1146
|
+
// split panes — NOT a raw fitAddon.fit() that would bypass both.
|
|
1147
|
+
_scheduleKeyboardFit() {
|
|
1148
|
+
if (this._kbFitRaf) cancelAnimationFrame(this._kbFitRaf);
|
|
1149
|
+
this._kbFitRaf = requestAnimationFrame(() => {
|
|
1150
|
+
this._kbFitRaf = null;
|
|
1151
|
+
this.fitTerminal();
|
|
1152
|
+
if (this.splitContainer && this.splitContainer.splits) {
|
|
1153
|
+
this.splitContainer.splits.forEach(s => { try { s.fit(); } catch (_) {} });
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// End the transition window after the CSS chrome-collapse would have settled,
|
|
1159
|
+
// then do one final measured fit to reconcile the resting layout.
|
|
1160
|
+
_endKeyboardTransitionSoon() {
|
|
1161
|
+
if (this._kbTransitionTimer) clearTimeout(this._kbTransitionTimer);
|
|
1162
|
+
this._kbTransitionTimer = setTimeout(() => {
|
|
1163
|
+
this._kbTransitionTimer = null;
|
|
1164
|
+
this._inKeyboardTransition = false;
|
|
1165
|
+
this._scheduleKeyboardFit();
|
|
1166
|
+
}, 320);
|
|
1068
1167
|
}
|
|
1069
1168
|
|
|
1070
1169
|
showSessionSelectionModal() {
|
|
@@ -1190,16 +1289,7 @@ class ClaudeCodeWebInterface {
|
|
|
1190
1289
|
// Fallback: generic handler unavailable — keep the legacy image-only picker.
|
|
1191
1290
|
attachBtn.addEventListener('click', () => {
|
|
1192
1291
|
window.imageHandler.triggerFilePicker((imageData) => {
|
|
1193
|
-
this.
|
|
1194
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
1195
|
-
this.send({
|
|
1196
|
-
type: 'image_upload',
|
|
1197
|
-
base64: imageData.base64,
|
|
1198
|
-
mimeType: imageData.mimeType,
|
|
1199
|
-
fileName: imageData.fileName || 'attached-image.png',
|
|
1200
|
-
caption: imageData.caption || ''
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1292
|
+
this._uploadImage(imageData);
|
|
1203
1293
|
});
|
|
1204
1294
|
});
|
|
1205
1295
|
}
|
|
@@ -1300,6 +1390,22 @@ class ClaudeCodeWebInterface {
|
|
|
1300
1390
|
this.closeMobileMenu();
|
|
1301
1391
|
});
|
|
1302
1392
|
}
|
|
1393
|
+
|
|
1394
|
+
// Mobile image attach — the desktop attach button is hidden on mobile,
|
|
1395
|
+
// so this is the mobile entry point. The file picker offers Photo Library
|
|
1396
|
+
// / Take Photo / Files on iOS; the picked image goes through the preview
|
|
1397
|
+
// modal and the HTTP upload path (_uploadImage).
|
|
1398
|
+
const attachImageBtnMobile = document.getElementById('attachImageBtnMobile');
|
|
1399
|
+
if (attachImageBtnMobile) {
|
|
1400
|
+
attachImageBtnMobile.addEventListener('click', () => {
|
|
1401
|
+
this.closeMobileMenu();
|
|
1402
|
+
if (window.imageHandler && typeof window.imageHandler.triggerFilePicker === 'function') {
|
|
1403
|
+
window.imageHandler.triggerFilePicker((imageData) => this._uploadImage(imageData));
|
|
1404
|
+
} else if (window.feedback) {
|
|
1405
|
+
window.feedback.warning('Image upload is unavailable');
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1303
1409
|
|
|
1304
1410
|
// Mobile sessions button
|
|
1305
1411
|
const sessionsBtnMobile = document.getElementById('sessionsBtnMobile');
|
|
@@ -3900,7 +4006,6 @@ class ClaudeCodeWebInterface {
|
|
|
3900
4006
|
break;
|
|
3901
4007
|
}
|
|
3902
4008
|
case 'pasteImage': {
|
|
3903
|
-
const pasteSocket = activeSocket;
|
|
3904
4009
|
try {
|
|
3905
4010
|
if (navigator.clipboard && typeof navigator.clipboard.read === 'function') {
|
|
3906
4011
|
const items = await navigator.clipboard.read();
|
|
@@ -3911,17 +4016,7 @@ class ClaudeCodeWebInterface {
|
|
|
3911
4016
|
if (imageType) {
|
|
3912
4017
|
const blob = await item.getType(imageType);
|
|
3913
4018
|
window.imageHandler.showImagePreview(blob, (imageData) => {
|
|
3914
|
-
this.
|
|
3915
|
-
const msg = JSON.stringify({
|
|
3916
|
-
type: 'image_upload',
|
|
3917
|
-
base64: imageData.base64,
|
|
3918
|
-
mimeType: imageData.mimeType,
|
|
3919
|
-
fileName: imageData.fileName || 'pasted-image.png',
|
|
3920
|
-
caption: imageData.caption || ''
|
|
3921
|
-
});
|
|
3922
|
-
if (pasteSocket && pasteSocket.readyState === WebSocket.OPEN) {
|
|
3923
|
-
pasteSocket.send(msg);
|
|
3924
|
-
}
|
|
4019
|
+
this._uploadImage(imageData);
|
|
3925
4020
|
});
|
|
3926
4021
|
return;
|
|
3927
4022
|
}
|
|
@@ -3949,19 +4044,8 @@ class ClaudeCodeWebInterface {
|
|
|
3949
4044
|
{ multiple: true }
|
|
3950
4045
|
);
|
|
3951
4046
|
} else if (window.imageHandler) {
|
|
3952
|
-
const attachSocket = activeSocket;
|
|
3953
4047
|
window.imageHandler.triggerFilePicker((imageData) => {
|
|
3954
|
-
this.
|
|
3955
|
-
const msg = JSON.stringify({
|
|
3956
|
-
type: 'image_upload',
|
|
3957
|
-
base64: imageData.base64,
|
|
3958
|
-
mimeType: imageData.mimeType,
|
|
3959
|
-
fileName: imageData.fileName || 'attached-image.png',
|
|
3960
|
-
caption: imageData.caption || ''
|
|
3961
|
-
});
|
|
3962
|
-
if (attachSocket && attachSocket.readyState === WebSocket.OPEN) {
|
|
3963
|
-
attachSocket.send(msg);
|
|
3964
|
-
}
|
|
4048
|
+
this._uploadImage(imageData);
|
|
3965
4049
|
});
|
|
3966
4050
|
}
|
|
3967
4051
|
break;
|
|
@@ -4207,10 +4291,6 @@ class ClaudeCodeWebInterface {
|
|
|
4207
4291
|
|
|
4208
4292
|
// Capture the socket at attach-initiation time. The image preview modal
|
|
4209
4293
|
// is async (user-driven); the active session/socket can change while it
|
|
4210
|
-
// is open. Sending on a captured target avoids the upload landing on a
|
|
4211
|
-
// different session (mirrors the context-menu's existing capture).
|
|
4212
|
-
const targetSocket = this.socket;
|
|
4213
|
-
|
|
4214
4294
|
// Images: reuse the existing single-preview flow. The modal handles one
|
|
4215
4295
|
// image at a time, so if several images are selected we attach the first
|
|
4216
4296
|
// and tell the user rather than silently dropping the rest.
|
|
@@ -4219,16 +4299,7 @@ class ClaudeCodeWebInterface {
|
|
|
4219
4299
|
window.feedback.info('Only the first image is attached — attach images one at a time.');
|
|
4220
4300
|
}
|
|
4221
4301
|
ih.showImagePreview(images[0], (imageData) => {
|
|
4222
|
-
this.
|
|
4223
|
-
if (targetSocket && targetSocket.readyState === WebSocket.OPEN) {
|
|
4224
|
-
targetSocket.send(JSON.stringify({
|
|
4225
|
-
type: 'image_upload',
|
|
4226
|
-
base64: imageData.base64,
|
|
4227
|
-
mimeType: imageData.mimeType,
|
|
4228
|
-
fileName: imageData.fileName || 'attached-image.png',
|
|
4229
|
-
caption: imageData.caption || ''
|
|
4230
|
-
}));
|
|
4231
|
-
}
|
|
4302
|
+
this._uploadImage(imageData);
|
|
4232
4303
|
});
|
|
4233
4304
|
}
|
|
4234
4305
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
const SDK_SOURCE_IN = 'ai-or-die-artifact-sdk'; // messages FROM the iframe
|
|
21
21
|
const HOST_SOURCE_OUT = 'ai-or-die-artifact-host'; // messages TO the iframe
|
|
22
22
|
const STORE_KEY = 'ai-or-die:artifact-panel:layout';
|
|
23
|
+
const PHONE_MQ = '(max-width:640px)';
|
|
23
24
|
|
|
24
25
|
const MIN_W = 320;
|
|
25
26
|
const MIN_H = 240;
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
|
|
61
62
|
this._layout = this._loadLayout();
|
|
62
63
|
this._buildDom();
|
|
64
|
+
this._wirePhoneMode();
|
|
63
65
|
this._applyLayout();
|
|
64
66
|
this._minimized = !!this._layout.minimized;
|
|
65
67
|
this._applyMinimized();
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
return {};
|
|
83
85
|
}
|
|
84
86
|
_saveLayout() {
|
|
87
|
+
if (this._isPhone()) return;
|
|
85
88
|
try {
|
|
86
89
|
window.localStorage.setItem(STORE_KEY, JSON.stringify({
|
|
87
90
|
left: this._layout.left,
|
|
@@ -93,6 +96,39 @@
|
|
|
93
96
|
} catch (_) { /* non-fatal */ }
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
_isPhone() {
|
|
100
|
+
return !!(typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia(PHONE_MQ).matches);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_wirePhoneMode() {
|
|
104
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
|
105
|
+
const mql = window.matchMedia(PHONE_MQ);
|
|
106
|
+
this._phoneMql = mql;
|
|
107
|
+
this._onPhoneChange = (e) => {
|
|
108
|
+
const matches = !!(e && typeof e.matches === 'boolean' ? e.matches : mql.matches);
|
|
109
|
+
if (matches) {
|
|
110
|
+
this._cancelPointerInteraction();
|
|
111
|
+
this._clearInlineGeometry();
|
|
112
|
+
} else {
|
|
113
|
+
this._applyLayout();
|
|
114
|
+
this._clampToBounds();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', this._onPhoneChange);
|
|
118
|
+
else if (typeof mql.addListener === 'function') mql.addListener(this._onPhoneChange);
|
|
119
|
+
if (mql.matches) this._clearInlineGeometry();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_clearInlineGeometry() {
|
|
123
|
+
if (!this.el) return;
|
|
124
|
+
this.el.style.left = '';
|
|
125
|
+
this.el.style.top = '';
|
|
126
|
+
this.el.style.right = '';
|
|
127
|
+
this.el.style.bottom = '';
|
|
128
|
+
this.el.style.width = '';
|
|
129
|
+
this.el.style.height = '';
|
|
130
|
+
}
|
|
131
|
+
|
|
96
132
|
_buildDom() {
|
|
97
133
|
this.el = el('div', { class: 'artifact-panel', id: 'artifactPanel', hidden: 'hidden', role: 'dialog', 'aria-label': 'Artifact review' });
|
|
98
134
|
|
|
@@ -167,6 +203,10 @@
|
|
|
167
203
|
|
|
168
204
|
// ---- floating-window geometry ----------------------------------------
|
|
169
205
|
_applyLayout() {
|
|
206
|
+
if (this._isPhone()) {
|
|
207
|
+
this._clearInlineGeometry();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
170
210
|
const L = this._layout;
|
|
171
211
|
if (typeof L.width === 'number') this.el.style.width = clampNumber(L.width, MIN_W, 4000) + 'px';
|
|
172
212
|
if (typeof L.height === 'number') this.el.style.height = clampNumber(L.height, MIN_H, 4000) + 'px';
|
|
@@ -186,6 +226,7 @@
|
|
|
186
226
|
// live panel size, then persists the corrected value. Safe to call when the
|
|
187
227
|
// panel is visible (a hidden panel measures 0x0, so we skip then).
|
|
188
228
|
_clampToBounds() {
|
|
229
|
+
if (this._isPhone()) return;
|
|
189
230
|
const L = this._layout;
|
|
190
231
|
if (typeof L.left !== 'number' || typeof L.top !== 'number') return;
|
|
191
232
|
if (this.el.hidden) return;
|
|
@@ -211,10 +252,20 @@
|
|
|
211
252
|
return { width: wr.width, height: wr.height };
|
|
212
253
|
}
|
|
213
254
|
|
|
255
|
+
_cancelPointerInteraction() {
|
|
256
|
+
if (this._dragCleanup) { try { this._dragCleanup(); } catch (_) { /* ignore */ } this._dragCleanup = null; }
|
|
257
|
+
if (this._resizeCleanup) { try { this._resizeCleanup(); } catch (_) { /* ignore */ } this._resizeCleanup = null; }
|
|
258
|
+
}
|
|
259
|
+
|
|
214
260
|
_wireDrag(handle) {
|
|
215
261
|
let startX = 0, startY = 0, originLeft = 0, originTop = 0, dragging = false;
|
|
216
262
|
const onMove = (e) => {
|
|
217
263
|
if (!dragging) return;
|
|
264
|
+
if (this._isPhone()) {
|
|
265
|
+
dragging = false;
|
|
266
|
+
this._cancelPointerInteraction();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
218
269
|
const p = this._point(e);
|
|
219
270
|
const b = this._bounds();
|
|
220
271
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -236,6 +287,7 @@
|
|
|
236
287
|
this._saveLayout();
|
|
237
288
|
};
|
|
238
289
|
handle.addEventListener('mousedown', (e) => {
|
|
290
|
+
if (this._isPhone()) return;
|
|
239
291
|
// Ignore drags that start on a header button.
|
|
240
292
|
if (e.target && e.target.closest && e.target.closest('.artifact-panel__btn')) return;
|
|
241
293
|
e.preventDefault();
|
|
@@ -265,6 +317,11 @@
|
|
|
265
317
|
let startX = 0, startY = 0, originW = 0, originH = 0, resizing = false;
|
|
266
318
|
const onMove = (e) => {
|
|
267
319
|
if (!resizing) return;
|
|
320
|
+
if (this._isPhone()) {
|
|
321
|
+
resizing = false;
|
|
322
|
+
this._cancelPointerInteraction();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
268
325
|
const p = this._point(e);
|
|
269
326
|
const b = this._bounds();
|
|
270
327
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -288,6 +345,7 @@
|
|
|
288
345
|
this._saveLayout();
|
|
289
346
|
};
|
|
290
347
|
handle.addEventListener('mousedown', (e) => {
|
|
348
|
+
if (this._isPhone()) return;
|
|
291
349
|
e.preventDefault();
|
|
292
350
|
e.stopPropagation();
|
|
293
351
|
const p = this._point(e);
|
|
@@ -886,6 +944,10 @@
|
|
|
886
944
|
destroy() {
|
|
887
945
|
window.removeEventListener('message', this._onWindowMessage);
|
|
888
946
|
if (this._onWindowResize) window.removeEventListener('resize', this._onWindowResize);
|
|
947
|
+
if (this._phoneMql && this._onPhoneChange) {
|
|
948
|
+
if (typeof this._phoneMql.removeEventListener === 'function') this._phoneMql.removeEventListener('change', this._onPhoneChange);
|
|
949
|
+
else if (typeof this._phoneMql.removeListener === 'function') this._phoneMql.removeListener(this._onPhoneChange);
|
|
950
|
+
}
|
|
889
951
|
this._teardownSse();
|
|
890
952
|
if (this._dragCleanup) { try { this._dragCleanup(); } catch (_) { /* ignore */ } this._dragCleanup = null; }
|
|
891
953
|
if (this._resizeCleanup) { try { this._resizeCleanup(); } catch (_) { /* ignore */ } this._resizeCleanup = null; }
|
|
@@ -293,3 +293,42 @@
|
|
|
293
293
|
.artifact-panel--maximized .artifact-panel__frame { width: 100%; }
|
|
294
294
|
.artifact-panel--maximized .artifact-panel__chat { max-height: 30%; }
|
|
295
295
|
}
|
|
296
|
+
|
|
297
|
+
@media (max-width: 640px) {
|
|
298
|
+
.artifact-panel:not(.artifact-panel--maximized) {
|
|
299
|
+
left: 0 !important;
|
|
300
|
+
top: auto !important;
|
|
301
|
+
right: 0 !important;
|
|
302
|
+
bottom: 0 !important;
|
|
303
|
+
width: 100% !important;
|
|
304
|
+
max-width: none;
|
|
305
|
+
height: 70vh !important;
|
|
306
|
+
height: 70dvh !important;
|
|
307
|
+
min-width: 0;
|
|
308
|
+
border-radius: 12px 12px 0 0;
|
|
309
|
+
box-sizing: border-box;
|
|
310
|
+
padding-bottom: var(--sa-bottom, 0px);
|
|
311
|
+
box-shadow: 0 -10px 32px rgba(0, 0, 0, 0.45);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.artifact-panel__resize {
|
|
315
|
+
display: none;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.artifact-panel__header {
|
|
319
|
+
min-height: 44px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.artifact-panel__btn {
|
|
323
|
+
display: inline-flex;
|
|
324
|
+
align-items: center;
|
|
325
|
+
justify-content: center;
|
|
326
|
+
min-width: 44px;
|
|
327
|
+
min-height: 44px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.artifact-panel__reopen {
|
|
331
|
+
min-width: 44px;
|
|
332
|
+
min-height: 44px;
|
|
333
|
+
}
|
|
334
|
+
}
|