Spaces:
Running
Running
File size: 10,839 Bytes
b5f4479 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HFBA v2 — Name Editor</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=Source+Sans+3:ital,wght@0,400;0,600;1,400&display=swap" rel="stylesheet" />
<style>
:root { --bluishwhite:#F3F7FA; --bluishdark:#4D5862; --blue:#3B82F6; --border:#D2DAE1; --changed:#FEF3C7; --changedborder:#F59E0B; }
* { box-sizing: border-box; }
body { margin:0; background:var(--bluishwhite); color:var(--bluishdark); font-family:'Source Sans 3',sans-serif; }
header { position:sticky; top:0; z-index:20; background:rgba(243,247,250,.95); backdrop-filter:blur(6px); border-bottom:1px solid var(--border); padding:14px 20px; }
.bar { max-width:1280px; margin:0 auto; display:flex; flex-wrap:wrap; gap:12px 18px; align-items:center; justify-content:space-between; }
h1 { font-family:'IBM Plex Mono',monospace; font-size:20px; margin:0; }
h1 .pill { background:var(--blue); color:#fff; border-radius:999px; padding:3px 12px; font-size:15px; }
.hint { font-size:13px; max-width:560px; line-height:1.4; }
.actions { display:flex; gap:10px; align-items:center; }
button { font-family:'IBM Plex Mono',monospace; font-size:13px; cursor:pointer; border:1px solid var(--border); background:#fff; color:var(--bluishdark); padding:9px 16px; border-radius:999px; transition:transform .12s, background .12s; }
button:active { transform:scale(.94); }
button.primary { background:var(--blue); color:#fff; border-color:var(--blue); }
button:disabled { opacity:.45; cursor:not-allowed; }
.count { font-family:'IBM Plex Mono',monospace; font-size:13px; }
.count b { color:var(--changedborder); }
main { max-width:1280px; margin:0 auto; padding:24px 20px 80px; }
.sec-title { font-family:'IBM Plex Mono',monospace; font-size:16px; background:var(--blue); color:#fff; display:inline-block; padding:8px 18px; border-radius:999px; margin:30px 0 16px; }
.grid { display:grid; grid-template-columns:repeat(2,1fr); gap:18px; }
@media (min-width:720px){ .grid{ grid-template-columns:repeat(3,1fr); } }
@media (min-width:1080px){ .grid{ grid-template-columns:repeat(4,1fr); } }
.card { background:#fff; border:1px solid var(--border); border-radius:18px; padding:12px; display:flex; flex-direction:column; gap:8px; }
.card.changed { border-color:var(--changedborder); box-shadow:0 0 0 2px var(--changed); }
.thumb { width:100%; aspect-ratio:1/1; object-fit:contain; background:#fff; border-radius:12px; }
.meta { font-family:'IBM Plex Mono',monospace; font-size:11px; color:#9aa6b1; display:flex; justify-content:space-between; gap:6px; }
input.name { width:100%; font-family:'Source Sans 3',sans-serif; font-size:15px; color:var(--bluishdark); border:1px solid var(--border); border-radius:10px; padding:8px 10px; outline:none; }
input.name:focus { border-color:var(--blue); }
.dupwarn { color:#dc2626; font-size:11px; min-height:13px; font-family:'IBM Plex Mono',monospace; }
dialog { border:none; border-radius:16px; padding:0; max-width:680px; width:92%; box-shadow:0 20px 60px rgba(0,0,0,.25); }
.modal-in { padding:22px; }
.modal-in h2 { font-family:'IBM Plex Mono',monospace; font-size:17px; margin:0 0 8px; }
.modal-in p { font-size:13px; line-height:1.5; margin:0 0 12px; }
textarea { width:100%; height:240px; font-family:'IBM Plex Mono',monospace; font-size:12px; border:1px solid var(--border); border-radius:10px; padding:10px; resize:vertical; }
.modal-actions { display:flex; gap:10px; justify-content:flex-end; margin-top:14px; }
</style>
</head>
<body>
<header>
<div class="bar">
<div>
<h1>HFBA <span class="pill">Name Editor</span></h1>
<div class="hint">Edit any name below. The value you type becomes <b>both the file name and the display name</b>. When finished, click <b>Export renames</b> — a small JSON downloads and shows on screen. Then come back to chat and say you're done.</div>
</div>
<div class="actions">
<span class="count" id="count">0 changed</span>
<button id="resetBtn">Reset all</button>
<button id="exportBtn" class="primary" disabled>Export renames</button>
</div>
</div>
</header>
<main id="main">Loading assets…</main>
<dialog id="exportDlg">
<div class="modal-in">
<h2>Rename plan (<span id="planCount">0</span>)</h2>
<p>This file just downloaded to your <b>Downloads</b> folder as <code>hfba-renames.json</code>. You can also copy it from here. Return to chat and tell me you're set — I'll apply these to the real files and metadata, then push.</p>
<textarea id="planText" readonly></textarea>
<div class="modal-actions">
<button id="copyBtn">Copy JSON</button>
<button class="primary" id="closeBtn">Close</button>
</div>
</div>
</dialog>
<script>
const SECTIONS = [
{ folder: "Huggy Collection 2026", label: "Modern Huggies — 2026 batch" },
{ folder: "modern Huggies", label: "Modern Huggies — original" },
{ folder: "Outlined Huggies", label: "Outlined Huggies" },
];
const stripExt = (fn) => fn.replace(/\.[^.]+$/, "");
const extOf = (fn) => (fn.match(/\.[^.]+$/) || [""])[0];
let records = [];
function render() {
const main = document.getElementById("main");
main.innerHTML = "";
for (const sec of SECTIONS) {
const rows = records.filter((r) => r.folder === sec.folder);
if (!rows.length) continue;
const h = document.createElement("div");
h.className = "sec-title";
h.textContent = `${sec.label} (${rows.length})`;
main.appendChild(h);
const grid = document.createElement("div");
grid.className = "grid";
for (const r of rows) {
const card = document.createElement("div");
card.className = "card";
card.dataset.folder = r.folder;
card.dataset.filename = r.filename;
const orig = stripExt(r.filename);
card.innerHTML = `
<img class="thumb" loading="lazy" src="images/${r.folder}/${r.filename}" alt="">
<input class="name" type="text" value="${orig.replace(/"/g, """)}"
data-orig="${orig.replace(/"/g, """)}" data-ext="${extOf(r.filename)}">
<div class="dupwarn"></div>
<div class="meta"><span>${extOf(r.filename)}</span><span>${sec.folder}</span></div>`;
grid.appendChild(card);
}
main.appendChild(grid);
}
main.querySelectorAll("input.name").forEach((inp) => {
inp.addEventListener("input", onEdit);
});
recompute();
}
function onEdit(e) {
const inp = e.target;
const card = inp.closest(".card");
card.classList.toggle("changed", inp.value.trim() !== inp.dataset.orig);
recompute();
}
function recompute() {
const inputs = [...document.querySelectorAll("input.name")];
// duplicate detection within each folder
const seen = {};
inputs.forEach((i) => {
const folder = i.closest(".card").dataset.folder;
const key = folder + "||" + i.value.trim().toLowerCase();
(seen[key] = seen[key] || []).push(i);
});
inputs.forEach((i) => {
const folder = i.closest(".card").dataset.folder;
const key = folder + "||" + i.value.trim().toLowerCase();
const warnEl = i.closest(".card").querySelector(".dupwarn");
if (!i.value.trim()) warnEl.textContent = "name can't be empty";
else if (seen[key].length > 1) warnEl.textContent = "duplicate name in this section";
else warnEl.textContent = "";
});
const changed = inputs.filter((i) => i.value.trim() !== i.dataset.orig);
document.getElementById("count").innerHTML = `<b>${changed.length}</b> changed`;
const hasProblem = inputs.some((i) => {
const w = i.closest(".card").querySelector(".dupwarn").textContent;
return w !== "";
});
document.getElementById("exportBtn").disabled = changed.length === 0 || hasProblem;
}
function buildPlan() {
const inputs = [...document.querySelectorAll("input.name")];
return inputs
.filter((i) => i.value.trim() !== i.dataset.orig)
.map((i) => ({
folder: i.closest(".card").dataset.folder,
old_filename: i.closest(".card").dataset.filename,
new_name: i.value.trim(),
ext: i.dataset.ext,
}));
}
document.getElementById("exportBtn").addEventListener("click", () => {
const plan = buildPlan();
const payload = { tool: "hfba-name-editor", count: plan.length, renames: plan };
const json = JSON.stringify(payload, null, 2);
const blob = new Blob([json], { type: "application/json" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "hfba-renames.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
document.getElementById("planCount").textContent = plan.length;
document.getElementById("planText").value = json;
document.getElementById("exportDlg").showModal();
});
document.getElementById("copyBtn").addEventListener("click", async () => {
try { await navigator.clipboard.writeText(document.getElementById("planText").value); document.getElementById("copyBtn").textContent = "Copied!"; }
catch { document.getElementById("planText").select(); document.execCommand("copy"); }
});
document.getElementById("closeBtn").addEventListener("click", () => document.getElementById("exportDlg").close());
document.getElementById("resetBtn").addEventListener("click", () => {
document.querySelectorAll("input.name").forEach((i) => { i.value = i.dataset.orig; i.closest(".card").classList.remove("changed"); });
recompute();
});
fetch("metadata.json", { cache: "no-store" })
.then((r) => r.json())
.then((data) => { records = data; render(); })
.catch((e) => { document.getElementById("main").textContent = "Failed to load metadata.json: " + e; });
</script>
</body>
</html>
|