feat: add remote URL attachment support and project editing endpoint

This commit is contained in:
2026-06-10 22:17:25 +00:00
parent 9b425b33a5
commit 3540bc8fa6
43 changed files with 1520 additions and 133 deletions
+4
View File
@@ -248,10 +248,14 @@
.project-detail-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
}
.project-actions-overflow {
display: none;
}
.project-star-btn {
display: inline-flex;
align-items: center;
+1 -1
View File
@@ -18,7 +18,7 @@ export class FormManager {
if (content && contentCount) {
content.addEventListener("input", () => {
contentCount.textContent = `${content.value.length}/2000`;
contentCount.textContent = `${content.value.length}/125000`;
});
}
if (title && titleCount) {
+8
View File
@@ -63,6 +63,14 @@ export class ProfileEditor {
hiddenInput.value = values.join(",");
};
const seeded = hiddenInput.value;
hiddenInput.value = "";
seeded
.split(",")
.map((value) => value.trim())
.filter(Boolean)
.forEach(addPlatform);
platformsInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -239,6 +239,9 @@ export default class ContainerTerminalElement extends FloatingWindow {
_contextItems() {
const slug = this.getAttribute("project-slug");
return [
{ label: "Copy", disabled: !(this.term && this.term.hasSelection()), onSelect: () => this._copySelection() },
{ label: "Paste", onSelect: () => this._paste() },
{ separator: true },
{ label: "Sync files", onSelect: () => this._lifecycle("sync", "Files synced") },
{ label: "Restart", onSelect: () => this._lifecycle("restart", "Restart requested") },
{ label: "Terminate", danger: true, onSelect: () => this._terminate() },
@@ -279,6 +282,52 @@ export default class ContainerTerminalElement extends FloatingWindow {
this.close();
}
_copySelection() {
if (!this.term || !this.term.hasSelection()) return;
this._clipboardWrite(this.term.getSelection());
}
async _paste() {
if (!navigator.clipboard || !navigator.clipboard.readText) {
this._toast("clipboard read unavailable", "error");
return;
}
let text = "";
try {
text = await navigator.clipboard.readText();
} catch (error) {
this._toast("paste failed", "error");
return;
}
if (text && this._ws && this._ws.readyState === 1) this._ws.send(text);
if (this.term) this.term.focus();
}
_clipboardWrite(text) {
if (!text) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => this._clipboardWriteFallback(text));
} else {
this._clipboardWriteFallback(text);
}
}
_clipboardWriteFallback(text) {
const area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
try {
document.execCommand("copy");
} catch (error) {
document.body.removeChild(area);
return;
}
document.body.removeChild(area);
}
_navigate(url) {
window.location.assign(url);
}
@@ -297,7 +297,11 @@ export default class DeviiTerminalElement extends FloatingWindow {
}
_contextItems() {
const selection = window.getSelection ? String(window.getSelection()) : "";
return [
{ label: "Copy", disabled: !selection && !this.input.value, onSelect: () => this._copySelection() },
{ label: "Paste", onSelect: () => this._paste() },
{ separator: true },
{ label: "Minimize", onSelect: () => this.minimizeWindow() },
{ label: "Normalize", onSelect: () => this.normalizeWindow() },
{ separator: true },
@@ -305,6 +309,47 @@ export default class DeviiTerminalElement extends FloatingWindow {
];
}
_copySelection() {
const selection = window.getSelection ? String(window.getSelection()) : "";
const text = selection || this.input.value;
if (!text) return;
this._clipboardWrite(text);
}
async _paste() {
if (!navigator.clipboard || !navigator.clipboard.readText) {
this._error("clipboard read unavailable.");
return;
}
let text = "";
try {
text = await navigator.clipboard.readText();
} catch (error) {
this._error("paste failed.");
return;
}
this._insertInput(text);
}
_insertInput(text) {
if (!text) return;
const start = this.input.selectionStart != null ? this.input.selectionStart : this.input.value.length;
const end = this.input.selectionEnd != null ? this.input.selectionEnd : this.input.value.length;
this.input.value = this.input.value.slice(0, start) + text + this.input.value.slice(end);
const caret = start + text.length;
this.input.focus({ preventScroll: true });
this.input.setSelectionRange(caret, caret);
}
_clipboardWrite(text) {
if (!text) return;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => this._fallbackCopy(text, () => {}));
} else {
this._fallbackCopy(text, () => {});
}
}
_fontKey() {
return `${FONT_KEY}:${userScope()}`;
}
@@ -583,8 +628,27 @@ export default class DeviiTerminalElement extends FloatingWindow {
}
_append(node) {
const pinned = this._atBottom();
this.output.appendChild(node);
this.output.scrollTop = this.output.scrollHeight;
this._scrollOnMediaLoad(node, pinned);
}
_atBottom() {
const slack = 40;
return this.output.scrollHeight - this.output.scrollTop - this.output.clientHeight <= slack;
}
_scrollOnMediaLoad(node, pinned) {
const images = node.querySelectorAll ? node.querySelectorAll("img") : [];
images.forEach((image) => {
if (image.complete) return;
const settle = () => {
if (pinned) this.output.scrollTop = this.output.scrollHeight;
};
image.addEventListener("load", settle, { once: true });
image.addEventListener("error", settle, { once: true });
});
}
_onOutputClick(event) {