fix: correct "bugs" to "issues" in routing table and README references across multiple documentation files

This commit is contained in:
2026-06-14 07:48:10 +00:00
parent f79a427f32
commit f2b910ea75
162 changed files with 7011 additions and 1134 deletions
+37 -1
View File
@@ -458,6 +458,30 @@ img {
justify-content: center;
}
@keyframes btn-spin {
to { transform: rotate(360deg); }
}
.btn-spinner {
display: none;
width: 0.85em;
height: 0.85em;
margin-right: 0.4em;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
vertical-align: -0.15em;
animation: btn-spin 0.6s linear infinite;
}
.btn.is-loading {
pointer-events: none;
}
.btn.is-loading .btn-spinner {
display: inline-block;
}
.badge {
display: inline-flex;
align-items: center;
@@ -544,8 +568,9 @@ img {
.topnav-icon { position: relative; padding: 0.375rem; color: var(--text-secondary); font-size: 1.25rem; background: none; border: none; cursor: pointer; font-family: inherit; line-height: 1; }
.topnav-icon:hover { color: var(--text-primary); }
.topnav-icon.active { color: var(--accent); }
.nav-bell { position: relative; display: inline-flex; align-items: center; justify-content: center; line-height: 1; }
.nav-badge {
position: absolute; top: 0; right: 0; z-index: 1; min-width: 16px; height: 16px;
position: absolute; top: -6px; right: -8px; z-index: 1; min-width: 16px; height: 16px;
padding: 0 0.25rem; border-radius: 8px; background: var(--accent); color: var(--white);
font-size: 0.6875rem; font-weight: 700;
display: flex; align-items: center; justify-content: center;
@@ -867,8 +892,19 @@ button.comment-form-submit:hover {
color: var(--text-primary);
}
body:has(.page-messages) {
display: flex;
flex-direction: column;
height: 100dvh;
overflow: hidden;
}
.page:has(.page-messages) {
padding-top: 0;
padding-bottom: 0;
flex: 1 1 auto;
min-height: 0;
display: flex;
}
@@ -1,25 +1,25 @@
/* retoor <retoor@molodetz.nl> */
.bugs-layout {
.issues-layout {
max-width: 720px;
margin: 0 auto;
}
.bugs-header {
.issues-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.bugs-header h1 {
.issues-header h1 {
font-size: 1.5rem;
font-weight: 700;
}
.bugs-filters {
.issues-filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.bugs-filter {
.issues-filter {
font-size: 0.8125rem;
font-weight: 600;
color: var(--text-secondary);
@@ -28,20 +28,20 @@
border: 1px solid var(--border);
text-decoration: none;
}
.bugs-filter:hover {
.issues-filter:hover {
background: var(--bg-hover);
}
.bugs-filter.active {
.issues-filter.active {
background: var(--accent);
color: var(--white);
border-color: var(--accent);
}
.bugs-modal-hint {
.issues-modal-hint {
font-size: 0.8125rem;
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.bug-card {
.issue-card {
display: block;
background: var(--bg-card);
border: 1px solid var(--border);
@@ -51,29 +51,29 @@
text-decoration: none;
transition: border-color 0.15s ease;
}
.bug-card:hover {
.issue-card:hover {
border-color: var(--accent);
}
.bug-card-header {
.issue-card-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.bug-number {
.issue-number {
font-size: 0.8125rem;
font-weight: 700;
color: var(--text-muted);
}
.bug-title {
.issue-title {
flex: 1;
}
.bug-title {
.issue-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
.bug-status {
.issue-status {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
@@ -81,75 +81,75 @@
padding: 0.125rem 0.5rem;
border-radius: 999px;
}
.bug-status.open {
.issue-status.open {
background: var(--accent-light);
color: var(--accent);
}
.bug-status.closed {
.issue-status.closed {
background: rgba(76, 175, 80, 0.1);
color: var(--success);
}
.bug-desc {
.issue-desc {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 0.5rem;
}
.bug-meta {
.issue-meta {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.bug-dot {
.issue-dot {
color: var(--border);
}
.bug-detail-nav {
.issue-detail-nav {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.bug-back,
.bug-gitea-link {
.issue-back,
.issue-gitea-link {
font-size: 0.8125rem;
font-weight: 600;
color: var(--accent);
text-decoration: none;
}
.bug-detail-card {
.issue-detail-card {
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.bug-detail-header {
.issue-detail-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.bug-detail-title {
.issue-detail-title {
flex: 1;
font-size: 1.25rem;
font-weight: 700;
}
.bug-admin-actions {
.issue-admin-actions {
margin: 1rem 0;
}
.bug-detail-body {
.issue-detail-body {
margin-top: 1rem;
color: var(--text-primary);
line-height: 1.6;
}
.bug-comment {
.issue-comment {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 1rem 1.25rem;
margin-bottom: 0.75rem;
}
.bug-comment-head {
.issue-comment-head {
display: flex;
align-items: center;
gap: 0.4rem;
@@ -157,7 +157,7 @@
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.bug-comment-badge {
.issue-comment-badge {
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
@@ -167,15 +167,15 @@
background: var(--accent-light);
color: var(--accent);
}
.bug-comment-body {
.issue-comment-body {
font-size: 0.9375rem;
color: var(--text-primary);
line-height: 1.55;
}
.bug-comment-form {
.issue-comment-form {
margin-top: 1rem;
}
.bug-comment-form textarea {
.issue-comment-form textarea {
width: 100%;
border: 1px solid var(--border);
border-radius: var(--radius);
@@ -185,28 +185,28 @@
color: var(--text-primary);
resize: vertical;
}
.bug-comment-form-footer {
.issue-comment-form-footer {
display: flex;
justify-content: flex-end;
margin-top: 0.5rem;
}
@media (max-width: 480px) {
.bugs-header {
.issues-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.bugs-header h1 {
.issues-header h1 {
font-size: 1.25rem;
}
.bug-card {
.issue-card {
padding: 1rem;
}
.bug-card-header {
.issue-card-header {
flex-wrap: wrap;
gap: 0.5rem;
}
+21 -4
View File
@@ -1,10 +1,18 @@
/* retoor <retoor@molodetz.nl> */
.page-messages {
flex: 1 1 auto;
min-height: 0;
display: flex;
width: 100%;
}
.messages-layout {
display: grid;
grid-template-columns: minmax(0, 320px) 1fr;
gap: 0;
height: calc(100dvh - var(--nav-height) - 3.5rem);
flex: 1 1 auto;
min-height: 0;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
@@ -16,6 +24,7 @@
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
}
.messages-list-header {
@@ -58,8 +67,16 @@
}
.messages-conversations {
flex: 1;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.messages-conversations::-webkit-scrollbar {
width: 0;
height: 0;
}
.conversation-item {
@@ -176,6 +193,7 @@
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.messages-input-area input[type="text"] {
@@ -248,8 +266,7 @@
@media (max-width: 768px) {
.messages-layout {
grid-template-columns: 1fr;
height: auto;
min-height: calc(100dvh - var(--nav-height) - 3.5rem);
min-height: 0;
}
.messages-list {
display: flex;
+15 -1
View File
@@ -32,6 +32,8 @@
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
overflow-wrap: break-word;
word-break: break-word;
}
.profile-role {
@@ -135,7 +137,8 @@ a.profile-stat-value:hover {
.profile-info-row {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
gap: 0.75rem;
padding: 0.5rem 0;
font-size: 0.8125rem;
}
@@ -146,16 +149,27 @@ a.profile-stat-value:hover {
.profile-info-label {
color: var(--text-muted);
flex-shrink: 0;
}
.profile-info-row > .flex-center-gap {
min-width: 0;
align-items: flex-start;
}
.profile-info-value {
color: var(--text-secondary);
min-width: 0;
text-align: left;
overflow-wrap: break-word;
word-break: break-word;
}
.profile-info-edit {
color: var(--text-muted);
padding: 0.25rem;
font-size: 0.8125rem;
flex-shrink: 0;
}
.profile-info-edit:hover {
+8 -2
View File
@@ -2,15 +2,21 @@
import { ApiTester } from "./ApiTester.js";
import { CodeCopy } from "./CodeCopy.js";
import { DevRantTester, DevRantLogin } from "./DevRantTester.js";
export class ApiDocs {
constructor() {
this.copy = new CodeCopy();
const mounts = document.querySelectorAll("[data-api-tester]");
if (!mounts.length) return;
const ctx = window.DEVPLACE_DOCS || { loggedIn: false, username: "", apiKey: "", isAdmin: false };
ctx.base = location.origin;
const mounts = document.querySelectorAll("[data-api-tester]");
this.testers = [...mounts].map((mount) => new ApiTester(mount, ctx));
this.devrantLogins = [...document.querySelectorAll("[data-devrant-login]")].map(
(mount) => new DevRantLogin(mount),
);
this.devrantTesters = [...document.querySelectorAll("[data-devrant-tester]")].map(
(mount) => new DevRantTester(mount),
);
}
}
+2 -2
View File
@@ -26,7 +26,7 @@ import { Tabs } from "./Tabs.js";
import { DeviiTerminal } from "./DeviiTerminal.js";
import { ZipDownloader } from "./ZipDownloader.js";
import { ProjectForker } from "./ProjectForker.js";
import { BugReporter } from "./BugReporter.js";
import { IssueReporter } from "./IssueReporter.js";
import { MediaGallery } from "./MediaGallery.js";
import WindowManager from "./components/WindowManager.js";
import { ContainerTerminalManager } from "./ContainerTerminalManager.js";
@@ -65,7 +65,7 @@ class Application {
this.devii = new DeviiTerminal();
this.zipDownloader = new ZipDownloader();
this.projectForker = new ProjectForker();
this.bugReporter = new BugReporter();
this.issueReporter = new IssueReporter();
this.mediaGallery = new MediaGallery();
}
}
+86
View File
@@ -0,0 +1,86 @@
// retoor <retoor@molodetz.nl>
const STORAGE_KEY = "devrant-docs-auth";
class DevRantSession {
constructor() {
const docs = window.DEVPLACE_DOCS || {};
this.base = docs.base || location.origin;
this.auth = null;
this.username = "";
this.listeners = new Set();
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
this.auth = parsed.auth || null;
this.username = parsed.username || "";
}
} catch {
this.auth = null;
}
}
isLoggedIn() {
return Boolean(this.auth && this.auth.token_key);
}
triple() {
return this.isLoggedIn() ? { ...this.auth } : {};
}
onChange(callback) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
_emit() {
for (const callback of this.listeners) callback();
}
_persist() {
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({ auth: this.auth, username: this.username }),
);
} catch {
return;
}
}
setFromToken(token, username) {
this.auth = {
user_id: token.user_id,
token_id: token.id,
token_key: token.key,
};
if (username) this.username = username;
this._persist();
this._emit();
}
async login(username, password) {
const response = await fetch(`${this.base}/api/users/auth-token`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
},
body: new URLSearchParams({ username, password }).toString(),
});
const data = await response.json();
if (!data.success) throw new Error(data.error || "login failed");
this.setFromToken(data.auth_token, username);
return this.auth;
}
logout() {
this.auth = null;
this.username = "";
this._persist();
this._emit();
}
}
export const devrantSession = new DevRantSession();
+315
View File
@@ -0,0 +1,315 @@
// retoor <retoor@molodetz.nl>
import { CodeBlock } from "./CodeBlock.js";
import { devrantSession } from "./DevRantSession.js";
function el(tag, props = {}, children = []) {
const node = document.createElement(tag);
for (const [key, value] of Object.entries(props)) {
if (key === "class") node.className = value;
else if (key === "text") node.textContent = value;
else if (key === "html") node.innerHTML = value;
else if (value === true) node.setAttribute(key, "");
else if (value !== false && value !== null) node.setAttribute(key, value);
}
for (const child of children) {
if (child) node.appendChild(child);
}
return node;
}
export class DevRantLogin {
constructor(mount) {
this.mount = mount;
this.render();
devrantSession.onChange(() => this.render());
}
render() {
this.mount.innerHTML = "";
const bar = el("div", { class: "api-tester devrant-login" });
if (devrantSession.isLoggedIn()) {
bar.appendChild(
el("span", {
class: "try-note",
text: `Authenticated as ${devrantSession.username || "user " + devrantSession.auth.user_id} (token #${devrantSession.auth.token_id}).`,
}),
);
const out = el("button", { type: "button", class: "btn try-send", text: "Log out" });
out.addEventListener("click", () => devrantSession.logout());
bar.appendChild(out);
this.mount.appendChild(bar);
return;
}
const user = el("input", { class: "param-input", type: "text", placeholder: "username or email" });
const pass = el("input", { class: "param-input", type: "password", placeholder: "password" });
const docs = window.DEVPLACE_DOCS || {};
if (docs.username) user.value = docs.username;
const send = el("button", { type: "button", class: "btn btn-primary try-send", text: "Log in" });
const note = el("span", { class: "try-note", text: "Log in once to enable the authenticated widgets on every devRant page." });
send.addEventListener("click", async () => {
send.disabled = true;
note.textContent = "Logging in...";
try {
await devrantSession.login(user.value.trim(), pass.value);
} catch (error) {
note.textContent = error.message || "Login failed";
send.disabled = false;
}
});
bar.appendChild(el("div", { class: "auth-field" }, [el("label", { text: "Username" }), user]));
bar.appendChild(el("div", { class: "auth-field" }, [el("label", { text: "Password" }), pass]));
bar.appendChild(send);
bar.appendChild(note);
this.mount.appendChild(bar);
}
}
export class DevRantTester {
constructor(mount) {
this.mount = mount;
try {
this.config = JSON.parse(mount.dataset.config || "{}");
} catch {
return;
}
this.base = (window.DEVPLACE_DOCS || {}).base || location.origin;
this.inputs = [];
this.build();
this.renderExpected();
if (this.requiresAuth()) {
devrantSession.onChange(() => this.syncAuth());
this.syncAuth();
}
}
requiresAuth() {
return this.config.auth === "user";
}
build() {
const root = el("div", { class: "api-tester" });
if (this.config.params.length) root.appendChild(this.buildParams());
if (this.requiresAuth()) {
this.authNote = el("div", { class: "param-desc devrant-auth-note" });
root.appendChild(this.authNote);
}
root.appendChild(this.buildActions());
root.appendChild(this.buildResponse());
this.mount.appendChild(root);
}
buildParams() {
const table = el("div", { class: "param-table" });
for (const param of this.config.params) {
const control = this.buildControl(param);
this.inputs.push({ param, control });
const label = el("div", { class: "param-label" }, [
el("span", { class: "param-name", text: param.name }),
param.required ? el("span", { class: "param-required", text: "*" }) : null,
el("span", { class: "param-loc param-loc-" + param.location, text: param.location }),
]);
const meta = el("div", { class: "param-meta" }, [
control,
param.description ? el("div", { class: "param-desc", text: param.description }) : null,
]);
table.appendChild(el("div", { class: "param-row" }, [label, meta]));
}
return table;
}
buildControl(param) {
if (param.type === "enum") {
const select = el("select", { class: "param-input" });
for (const option of param.options || []) {
const opt = el("option", { value: option, text: option });
if (option === param.example) opt.selected = true;
select.appendChild(opt);
}
return select;
}
if (param.type === "textarea") {
const area = el("textarea", { class: "param-input", rows: "3" });
area.value = param.example || "";
return area;
}
const input = el("input", {
class: "param-input",
type: param.type === "int" ? "number" : param.type === "password" ? "password" : "text",
placeholder: param.example || "",
});
if (param.type !== "password") input.value = param.example || "";
return input;
}
buildActions() {
const bar = el("div", { class: "try-panel" });
this.sendBtn = el("button", { type: "button", class: "btn btn-primary try-send", text: "Send request" });
this.sendBtn.addEventListener("click", () => this.send());
bar.appendChild(this.sendBtn);
return bar;
}
syncAuth() {
if (!this.authNote) return;
if (devrantSession.isLoggedIn()) {
this.authNote.textContent = `Authenticated as ${devrantSession.username || "user " + devrantSession.auth.user_id}.`;
this.sendBtn.disabled = false;
} else {
this.authNote.textContent = "Requires login - use the Log in widget at the top of the page.";
this.sendBtn.disabled = true;
}
}
buildResponse() {
const wrap = el("div", { class: "response-viewer" });
const tabs = el("div", { class: "response-tabs" });
this.responseTabs = {};
for (const tab of [{ id: "expected", label: "Expected" }, { id: "live", label: "Live response" }]) {
const btn = el("button", { type: "button", class: "code-tab", text: tab.label });
btn.addEventListener("click", () => this.selectTab(tab.id));
this.responseTabs[tab.id] = btn;
tabs.appendChild(btn);
}
this.expectedEl = el("div", { class: "response-pane" });
this.liveEl = el("div", { class: "response-pane" });
this.liveEl.appendChild(el("p", { class: "response-placeholder", text: "Run the request to see the live response." }));
wrap.appendChild(tabs);
wrap.appendChild(this.expectedEl);
wrap.appendChild(this.liveEl);
this.selectTab("expected");
return wrap;
}
selectTab(id) {
for (const [name, btn] of Object.entries(this.responseTabs)) {
btn.classList.toggle("active", name === id);
}
this.expectedEl.classList.toggle("active", id === "expected");
this.liveEl.classList.toggle("active", id === "live");
}
renderExpected() {
const sample = this.config.sample_response;
if (sample === null || sample === undefined) {
this.expectedEl.appendChild(el("p", { class: "response-note", text: "No documented response body." }));
return;
}
this.expectedEl.appendChild(el("p", { class: "response-note", text: "Modeled JSON response:" }));
const pre = el("pre", { class: "response-body" });
pre.appendChild(el("code", { class: "language-json", text: JSON.stringify(sample, null, 2) }));
this.expectedEl.appendChild(pre);
CodeBlock.enhance(pre, { lineNumbers: false });
}
values() {
const out = { path: {}, query: {}, body: {} };
for (const { param, control } of this.inputs) {
const value = control.value;
if (value === "" && !param.required) continue;
out[param.location][param.name] = value;
}
return out;
}
buildUrl(values) {
let path = this.config.path;
for (const [name, value] of Object.entries(values.path)) {
path = path.replace("{" + name + "}", encodeURIComponent(value));
}
const query = { ...values.query };
const usesQuery = this.config.method === "GET" || this.config.method === "DELETE";
if (usesQuery && this.requiresAuth()) Object.assign(query, devrantSession.triple());
const entries = Object.entries(query);
const qs = entries.length
? "?" + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")
: "";
return this.base + path + qs;
}
async confirmDestructive() {
if (!(this.config.destructive || this.config.method === "DELETE")) return true;
if (window.app && window.app.dialog) {
return window.app.dialog.confirm({
title: "Run request",
message: "This performs a real action on your account. Continue?",
confirmLabel: "Run",
danger: true,
});
}
return window.confirm("This performs a real action on your account. Continue?");
}
async send() {
if (this.requiresAuth() && !devrantSession.isLoggedIn()) {
this.showError("Log in first using the widget at the top of the page.");
return;
}
if (!(await this.confirmDestructive())) return;
const values = this.values();
const url = this.buildUrl(values);
const headers = { Accept: "application/json" };
const options = { method: this.config.method, headers };
if (this.config.encoding === "form") {
const body = { ...values.body };
if (this.requiresAuth()) Object.assign(body, devrantSession.triple());
options.body = new URLSearchParams(body).toString();
headers["Content-Type"] = "application/x-www-form-urlencoded";
}
this.sendBtn.disabled = true;
const started = performance.now();
try {
const response = await fetch(url, options);
const ms = Math.round(performance.now() - started);
await this.renderResponse(response, ms, values);
} catch (error) {
this.showError(error.message || "Network error");
} finally {
this.sendBtn.disabled = false;
if (this.requiresAuth()) this.syncAuth();
}
}
async renderResponse(response, ms, values) {
const contentType = response.headers.get("content-type") || "";
this.liveEl.innerHTML = "";
this.selectTab("live");
const cls = "response-" + Math.floor(response.status / 100) + "xx";
const meta = el("div", { class: "response-meta" }, [
el("span", { class: "response-status " + cls, text: `${response.status} ${response.statusText}` }),
el("span", { text: `${ms} ms` }),
el("span", { text: contentType.split(";")[0] || "unknown" }),
]);
this.liveEl.appendChild(meta);
if (contentType.includes("application/json")) {
const data = await response.json();
this.maybeCaptureLogin(data, values);
const pre = el("pre", { class: "response-body" });
pre.appendChild(el("code", { class: "language-json", text: JSON.stringify(data, null, 2) }));
this.liveEl.appendChild(pre);
CodeBlock.enhance(pre, { lineNumbers: false });
return;
}
const text = await response.text();
const pre = el("pre", { class: "response-body" });
pre.appendChild(el("code", { text: text.slice(0, 2000) }));
this.liveEl.appendChild(pre);
CodeBlock.enhance(pre, { highlight: false, lineNumbers: false });
}
maybeCaptureLogin(data, values) {
const loginIds = ["devrant-login", "devrant-register"];
if (loginIds.includes(this.config.id) && data && data.success && data.auth_token) {
devrantSession.setFromToken(data.auth_token, values.body.username || "");
}
}
showError(message) {
this.liveEl.innerHTML = "";
this.selectTab("live");
this.liveEl.appendChild(el("div", { class: "response-meta" }, [
el("span", { class: "response-status response-5xx", text: "Error" }),
]));
this.liveEl.appendChild(el("p", { class: "response-redirect", text: message }));
}
}
+4
View File
@@ -2,6 +2,9 @@
import { TextInput } from "./TextInput.js";
import { DomUtils } from "./DomUtils.js";
import { assetUrl } from "./assetVersion.js";
const EMOJI_DATA_SOURCE = assetUrl("/static/vendor/emoji-picker-element/data.json");
export class EmojiPicker {
constructor(textarea) {
@@ -21,6 +24,7 @@ export class EmojiPicker {
this.wrapper.style.zIndex = "2000";
const picker = document.createElement("emoji-picker");
picker.setAttribute("data-source", EMOJI_DATA_SOURCE);
picker.style.width = "320px";
picker.style.height = "350px";
this.wrapper.appendChild(picker);
+1
View File
@@ -57,6 +57,7 @@ export class FormManager {
const btn = form.querySelector("button[type='submit']");
if (btn) {
btn.disabled = true;
btn.classList.add("is-loading");
}
});
});
@@ -3,11 +3,11 @@
import { Http } from "./Http.js";
import { JobPoller } from "./JobPoller.js";
export class BugReporter {
export class IssueReporter {
constructor() {
this.active = false;
document.addEventListener("submit", (event) => {
const form = event.target.closest("form[data-bug-create]");
const form = event.target.closest("form[data-issue-create]");
if (!form) return;
event.preventDefault();
this.submit(form);
@@ -24,7 +24,10 @@ export class BugReporter {
}
this.active = true;
const button = form.querySelector("button[type='submit']");
if (button) button.disabled = true;
if (button) {
button.disabled = true;
button.classList.add("is-loading");
}
window.app.toast.show("Filing your report and improving it with AI...", {
type: "info",
ms: 5000,
@@ -36,15 +39,18 @@ export class BugReporter {
window.app.toast.show("Could not submit the report", { type: "error" });
} finally {
this.active = false;
if (button) button.disabled = false;
if (button) {
button.disabled = false;
button.classList.remove("is-loading");
}
}
}
poll(statusUrl) {
return JobPoller.run(statusUrl, {
onDone: (status) => {
window.app.toast.show("Bug report filed", { type: "success" });
window.location.href = status.bug_url || "/bugs";
window.app.toast.show("Issue report filed", { type: "success" });
window.location.href = status.issue_url || "/issues";
},
onFailed: (status) => {
window.app.toast.show(
-19
View File
@@ -8,13 +8,6 @@ export class MessagesLayout {
}
this.thread = document.querySelector(".messages-thread");
this.input = document.querySelector('.messages-input-area input[name="content"]');
this.stackBreakpoint = 768;
this.minHeight = 320;
this.resize = this.resize.bind(this);
this.resize();
window.addEventListener("resize", this.resize);
window.addEventListener("orientationchange", this.resize);
this.scrollThreadToEnd();
if (this.input) {
@@ -22,18 +15,6 @@ export class MessagesLayout {
}
}
resize() {
if (window.innerWidth <= this.stackBreakpoint) {
this.layout.style.height = "";
return;
}
const top = this.layout.getBoundingClientRect().top;
const page = this.layout.closest(".page");
const bottomPadding = page ? parseFloat(getComputedStyle(page).paddingBottom) || 0 : 0;
const available = window.innerHeight - top - bottomPadding;
this.layout.style.height = `${Math.max(available, this.minHeight)}px`;
}
scrollThreadToEnd() {
if (this.thread) {
this.thread.scrollTop = this.thread.scrollHeight;
+14
View File
@@ -15,4 +15,18 @@ export class OptimisticAction {
return null;
}
}
async submitOptimistic(url, params, errorTarget, apply, revert, reconcile) {
apply();
try {
const result = await Http.sendForm(url, params);
if (reconcile) reconcile(result);
return result;
} catch (error) {
console.error("optimistic action failed", error);
revert();
if (errorTarget) Toast.flash(errorTarget, "Error", 1500);
return null;
}
}
}
+27 -2
View File
@@ -21,8 +21,33 @@ export class VoteManager extends OptimisticAction {
cast(form, button) {
const action = form.getAttribute("action");
const value = form.querySelector('input[name="value"]').value;
return this.submit(action, { value }, button, (result) => this.render(action, result));
const clickedValue = parseInt(form.querySelector('input[name="value"]').value, 10);
const targetUid = action.split("/").pop();
const prevValue = this.currentValue(action);
const prevNet = this.currentNet(targetUid);
const newValue = prevValue === clickedValue ? 0 : clickedValue;
const predictedNet = prevNet + (newValue - prevValue);
return this.submitOptimistic(
action,
{ value: clickedValue },
button,
() => this.render(action, { net: predictedNet, value: newValue }),
() => this.render(action, { net: prevNet, value: prevValue }),
(result) => this.render(action, result),
);
}
currentValue(action) {
const voted = document.querySelector(`form[action="${action}"] button.voted`);
if (!voted) return 0;
return parseInt(voted.closest("form").querySelector('input[name="value"]').value, 10);
}
currentNet(targetUid) {
const counter = document.querySelector(`[data-vote-count="${targetUid}"]`);
if (!counter) return 0;
const parsed = parseInt(counter.textContent, 10);
return Number.isNaN(parsed) ? 0 : parsed;
}
render(action, result) {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -30,7 +30,7 @@ const MODE_READWRITE = 'readwrite';
const INDEX_SKIN_UNICODE = 'skinUnicodes';
const FIELD_SKIN_UNICODE = 'skinUnicodes';
const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json';
const DEFAULT_DATA_SOURCE = '/static/vendor/emoji-picker-element/data.json';
const DEFAULT_LOCALE = 'en';
// like lodash's uniqBy but much smaller
+1 -1
View File
@@ -1646,7 +1646,7 @@ function createRoot (shadowRoot, props) {
}
}
const DEFAULT_DATA_SOURCE = 'https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json';
const DEFAULT_DATA_SOURCE = '/static/vendor/emoji-picker-element/data.json';
const DEFAULT_LOCALE = 'en';
var enI18n = {