Graphical work order item picker

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 19:32:27 +01:00
parent 1488692463
commit 16f0384483
2 changed files with 65 additions and 16 deletions

View file

@ -1243,15 +1243,33 @@ function orderCard(o) {
</div>`;
}
function orderPicker(list) {
const d = draft, actions = Object.entries(list).map(([id, action]) => [id, C.actions.find((x) => x.id === action)]);
const skills = [...new Set(actions.map(([, a]) => a.skill))];
const picked = d.item && actions.find(([id]) => id === d.item)?.[1];
d.skill = skills.includes(d.skill) ? d.skill : picked?.skill || skills[0];
const inSkill = actions.filter(([, a]) => a.skill === d.skill);
const groups = [...new Set(inSkill.map(([, a]) => a.group).filter(Boolean))];
if (!groups.includes(d.group)) d.group = picked?.skill === d.skill && picked.group || groups[0];
const q = (d.search || "").trim().toLowerCase();
const shown = q ? actions.filter(([id]) => item(id).name.toLowerCase().includes(q))
: inSkill.filter(([, a]) => !groups.length || a.group === d.group);
const others = S.players.filter((p) => p.id !== S.uid);
const tiles = shown.sort((x, y) => x[1].level - y[1].level).map(([id, a]) => {
const makers = others.filter((p) => p.levels[a.skill] >= a.level).length;
const tip = `${item(id).name} · ${skillOf(a.skill).name} ${a.level}${d.kind === "craft" ? ` · 👥 ${makers}` : ""}`;
return `<button class="doll-slot swap pick ${d.item === id ? "on" : ""} ${d.kind === "craft" && !makers ? "nobody" : ""}" data-draft-item="${id}" title="${esc(tip)}">
${item(id).icon}<span class="pick-lvl">${a.level}</span></button>`;
}).join("");
return `<div class="picker">
<div class="pick-skills">${skills.map((k) => `<button class="pick-skill ${!q && k === d.skill ? "on" : ""}" data-draft-skill="${k}" title="${esc(skillOf(k).name)}">${skillOf(k).icon}</button>`).join("")}
<input type="text" data-keep="order-search" placeholder="🔍" value="${esc(d.search || "")}"></div>
${groups.length && !q ? `<div class="tabs sm">${groups.map((g) => `<button class="tab ${g === d.group ? "on" : ""}" data-draft-group="${esc(g)}">${esc(g)}</button>`).join("")}</div>` : ""}
<div class="swap-grid pick-grid">${tiles || `<span class="muted small">—</span>`}</div></div>`;
}
function orderComposer() {
const d = draft, list = d.kind === "craft" ? C.order_recipes : C.order_raw;
const bySkill = {};
for (const [id, action] of Object.entries(list)) {
const a = C.actions.find((x) => x.id === action);
(bySkill[a.skill] ||= []).push([id, a.level]);
}
const options = Object.entries(bySkill).map(([skill, items]) => `<optgroup label="${esc(skillOf(skill).name)}">${items
.sort((x, y) => x[1] - y[1]).map(([id, level]) => `<option value="${id}" ${d.item === id ? "selected" : ""}>${esc(item(id).name)} (${level})</option>`).join("")}</optgroup>`).join("");
let slots = "", maker = "";
const a = d.item && C.actions.find((x) => x.id === list[d.item]);
if (a) {
@ -1274,9 +1292,10 @@ function orderComposer() {
return `<div class="panel compose">
<div class="tabs" style="margin:0 0 14px"><button class="tab ${d.kind === "craft" ? "on" : ""}" data-draft-kind="craft">Craft</button>
<button class="tab ${d.kind === "gather" ? "on" : ""}" data-draft-kind="gather">Materials</button></div>
${orderPicker(list)}
<div class="compose-grid">
<div><select data-draft="item"><option value="">Item…</option>${options}</select>
${a ? `<div class="ro-pick"><span class="doll-slot on">${item(d.item).icon}</span><div><b>${esc(item(d.item).name)}</b>${maker}</div></div>` : ""}
<div>${a ? `<div class="ro-pick"><span class="doll-slot on">${item(d.item).icon}</span><div><b>${esc(item(d.item).name)}</b>${maker}</div></div>`
: `<div class="ro-pick"><span class="doll-slot empty-req"></span><span class="muted">—</span></div>`}
<div class="stepper">×<input type="number" min="1" max="${C.order_max[d.kind]}" value="${d.qty}" data-draft="qty">
<span class="muted small">max ${fmt(C.order_max[d.kind])}</span></div></div>
<div>${slots ? `<div class="ro-slots big">${slots}</div>` : ""}</div>
@ -1604,7 +1623,10 @@ document.addEventListener("click", (e) => {
draft = draft ? null : { kind: "craft", item: "", qty: 1, provide: [], reward: 0 };
return render();
}
if (d.draftKind) { draft = { kind: d.draftKind, item: "", qty: 1, provide: [], reward: 0 }; return render(); }
if (d.draftKind) { draft = { kind: d.draftKind, item: "", qty: 1, provide: [], reward: 0 }; clearSearch(); return render(); }
if (d.draftSkill) { draft.skill = d.draftSkill; draft.search = ""; clearSearch(); return render(); }
if (d.draftGroup) { draft.group = d.draftGroup; return render(); }
if (d.draftItem) { pickOrderItem(d.draftItem); return render(); }
if (d.provide) {
draft.provide = draft.provide.includes(d.provide) ? draft.provide.filter((x) => x !== d.provide) : [...draft.provide, d.provide];
return render();
@ -1664,16 +1686,30 @@ document.addEventListener("click", (e) => {
if ("devEvent" in d) return act("/api/dev/event", {});
});
function clearSearch() {
const field = $("[data-keep=order-search]");
if (field) field.value = "";
}
function pickOrderItem(id) {
const list = draft.kind === "craft" ? C.order_recipes : C.order_raw;
draft.item = list[id] ? id : "";
draft.provide = [];
// A starting price: what the item's worth, each.
draft.reward = draft.item ? item(draft.item).value * (draft.kind === "craft" ? draft.qty : 1) : 0;
}
document.addEventListener("input", (e) => {
if (e.target.dataset.keep !== "order-search" || !draft) return;
draft.search = e.target.value;
render();
});
document.addEventListener("change", (e) => {
const key = e.target.dataset.draft;
if (!key || !draft) return;
if (key === "item") {
const list = draft.kind === "craft" ? C.order_recipes : C.order_raw;
draft.item = e.target.value;
draft.provide = [];
// A starting price: what the item's worth, each.
draft.reward = draft.item ? item(draft.item).value * (draft.kind === "craft" ? draft.qty : 1) : 0;
if (!list[draft.item]) draft.item = "";
pickOrderItem(e.target.value);
} else {
const before = draft.qty;
draft[key] = Math.max(key === "qty" ? 1 : 0, Math.floor(Number(e.target.value) || 0));

View file

@ -603,3 +603,16 @@ button.sheet-skill:hover { background: var(--panel-2); }
.reward-input input { width: 110px; }
@media (max-width: 900px) { .compose-grid { grid-template-columns: 1fr; gap: 14px; } }
.compose .why { color: var(--warn); font-weight: 600; font-size: 13px; }
.picker { display: grid; gap: 10px; margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid var(--line); }
.pick-skills { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pick-skill { width: 40px; height: 40px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel); display: grid; place-items: center; }
.pick-skill .gi { width: 24px; height: 24px; }
.pick-skill.on { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); }
.pick-skills input { margin-left: auto; width: 160px; }
.tabs.sm { margin: 0; gap: 4px; }
.tabs.sm .tab { padding: 3px 10px; font-size: 13px; }
.pick-grid { gap: 9px; }
.doll-slot.pick.nobody > .gi { opacity: .35; }
.pick-lvl { position: absolute; bottom: -5px; right: -5px; font-size: 10px; font-weight: 700; line-height: 1; padding: 2px 4px; border-radius: 6px; background: var(--panel); border: 1px solid var(--line); color: var(--muted); }
.doll-slot.pick.on .pick-lvl { border-color: var(--gold); }
.doll-slot.pick.on .pick-lvl { color: var(--gold); }