Skip to content

Commit d03c061

Browse files
author
DavidQ
committed
Hide State Inspector from tools index; keep workspace access, and remove “Open with viewer” from import mismatch options.
1 parent b0ea36e commit d03c061

12 files changed

Lines changed: 591 additions & 34 deletions

File tree

tools/Palette Browser/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ <h3>Actions &amp; Validation</h3>
7272
<pre id="paletteJsonPreview" class="palette-browser__json-preview">Palette JSON preview will appear here.</pre>
7373
<div id="paletteValidationText" class="palette-browser__hint">Validation summary will appear here.</div>
7474
<div class="palette-browser__actions tools-platform-control-row">
75-
<button id="copyPaletteJsonButton" type="button">Copy Palette JSON</button>
75+
<button id="importPaletteJsonButton" type="button">Import Palette JSON</button>
76+
<input id="importPaletteJsonInput" type="file" accept=".json,application/json" hidden />
7677
<button id="exportPaletteJsonButton" type="button">Export Palette JSON</button>
78+
<button id="copyPaletteJsonButton" type="button">Copy Palette JSON</button>
7779
<button id="usePaletteButton" type="button">Use in Workspace Manager</button>
7880
</div>
7981
<div id="paletteSelectionText" class="palette-browser__hint">No handoff recorded yet.</div>

tools/Palette Browser/main.js

Lines changed: 136 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
writeSharedPaletteHandoff
77
} from "../shared/assetUsageIntegration.js";
88
import { registerToolBootContract } from "../shared/toolBootContract.js";
9+
import { addToolModeMetadata, assertStandaloneToolDocument, offerImportMismatchOptions } from "../shared/documentModeGuards.js";
910

1011
const CUSTOM_PALETTES_STORAGE_KEY = "toolboxaid.paletteBrowser.customPalettes";
1112
const HIDDEN_BUILTIN_PALETTES_STORAGE_KEY = "toolboxaid.paletteBrowser.hiddenBuiltins";
@@ -31,6 +32,8 @@ const refs = {
3132
validationText: document.getElementById("paletteValidationText"),
3233
selectionText: document.getElementById("paletteSelectionText"),
3334
jsonPreview: document.getElementById("paletteJsonPreview"),
35+
importPaletteJsonButton: document.getElementById("importPaletteJsonButton"),
36+
importPaletteJsonInput: document.getElementById("importPaletteJsonInput"),
3437
copyPaletteJsonButton: document.getElementById("copyPaletteJsonButton"),
3538
exportPaletteJsonButton: document.getElementById("exportPaletteJsonButton"),
3639
usePaletteButton: document.getElementById("usePaletteButton")
@@ -44,6 +47,12 @@ const state = {
4447
hiddenBuiltInPaletteIds: loadHiddenBuiltInPaletteIds()
4548
};
4649

50+
function setSelectionText(text, options = {}) {
51+
const muted = options?.muted === true;
52+
refs.selectionText.textContent = String(text || "");
53+
refs.selectionText.classList.toggle("is-context-muted", muted);
54+
}
55+
4756
function isWorkspaceContext() {
4857
if (typeof window === "undefined") {
4958
return false;
@@ -317,6 +326,99 @@ function createCustomPalette(name, entries) {
317326
};
318327
}
319328

329+
function makeUniquePaletteName(baseName) {
330+
const fallback = "imported-palette";
331+
const seed = String(baseName || "").trim() || fallback;
332+
const normalizedSeed = seed.toLowerCase();
333+
const existing = new Set(getAllPalettes().map((palette) => String(palette.name || "").trim().toLowerCase()));
334+
if (!existing.has(normalizedSeed)) {
335+
return seed;
336+
}
337+
let index = 2;
338+
while (index < 1000) {
339+
const candidate = `${seed}-${index}`;
340+
if (!existing.has(candidate.toLowerCase())) {
341+
return candidate;
342+
}
343+
index += 1;
344+
}
345+
return `${seed}-${Date.now().toString(36)}`;
346+
}
347+
348+
function normalizeImportedPalette(rawPayload) {
349+
if (!rawPayload || typeof rawPayload !== "object") {
350+
throw new Error("Palette JSON must be an object.");
351+
}
352+
const payload = rawPayload.palette && typeof rawPayload.palette === "object"
353+
? rawPayload.palette
354+
: rawPayload;
355+
const baseName = String(payload.name || "").trim();
356+
const entries = Array.isArray(payload.entries) ? payload.entries : [];
357+
if (!entries.length) {
358+
throw new Error("Palette JSON must include at least one swatch entry.");
359+
}
360+
const normalizedEntries = entries.map((entry, index) => ({
361+
symbol: String(entry?.symbol || "").trim().slice(0, 2),
362+
hex: String(entry?.hex || "").trim(),
363+
name: String(entry?.name || `Swatch ${index + 1}`).trim() || `Swatch ${index + 1}`
364+
}));
365+
return {
366+
name: baseName || "imported-palette",
367+
entries: normalizedEntries
368+
};
369+
}
370+
371+
async function importPaletteJsonFromFile(file) {
372+
if (!file) {
373+
return;
374+
}
375+
const text = await file.text();
376+
const parsed = JSON.parse(text);
377+
const guard = assertStandaloneToolDocument(parsed, {
378+
expectedLabel: "Palette Browser palette",
379+
requiredToolId: "palette-browser"
380+
});
381+
if (!guard.ok) {
382+
const handled = offerImportMismatchOptions(guard, {
383+
viewerToolId: "state-inspector",
384+
viewerPayload: parsed,
385+
sourceToolId: "palette-browser"
386+
});
387+
if (handled) {
388+
return;
389+
}
390+
throw new Error(guard.reason);
391+
}
392+
const imported = normalizeImportedPalette(parsed);
393+
let nextName = imported.name;
394+
if (hasReservedPaletteKeyword(nextName)) {
395+
nextName = `${nextName}-copy`;
396+
}
397+
if (hasReservedPaletteKeyword(nextName)) {
398+
while (true) {
399+
const requested = window.prompt(
400+
"Imported palette name contains reserved terms. Enter a new name:",
401+
"imported-palette"
402+
);
403+
if (requested === null) {
404+
setSelectionText("Palette import canceled.");
405+
return;
406+
}
407+
const trimmed = requested.trim() || "imported-palette";
408+
if (!hasReservedPaletteKeyword(trimmed)) {
409+
nextName = trimmed;
410+
break;
411+
}
412+
}
413+
}
414+
nextName = makeUniquePaletteName(nextName);
415+
const importedPalette = createCustomPalette(nextName, imported.entries);
416+
state.customPalettes.unshift(importedPalette);
417+
saveCustomPalettes();
418+
setSelectedPalette(importedPalette.id);
419+
setSelectionText(`Imported palette: ${importedPalette.name}.`);
420+
}
421+
320422
function createNewPalette() {
321423
const requestedName = window.prompt("Name for new palette:", "new-palette");
322424
if (requestedName === null) {
@@ -440,9 +542,9 @@ async function copyPaletteJson() {
440542
}, null, 2);
441543
try {
442544
await navigator.clipboard.writeText(payload);
443-
refs.selectionText.textContent = "Palette JSON copied to clipboard.";
545+
setSelectionText("Palette JSON copied to clipboard.");
444546
} catch {
445-
refs.selectionText.textContent = "Clipboard copy unavailable in this environment.";
547+
setSelectionText("Clipboard copy unavailable in this environment.");
446548
}
447549
}
448550

@@ -451,10 +553,10 @@ function exportPaletteJson() {
451553
if (!palette) {
452554
return;
453555
}
454-
const payload = JSON.stringify({
556+
const payload = JSON.stringify(addToolModeMetadata({
455557
name: palette.name,
456558
entries: palette.entries
457-
}, null, 2);
559+
}, { toolId: "palette-browser" }), null, 2);
458560
const blob = new Blob([payload], { type: "application/json" });
459561
const objectUrl = URL.createObjectURL(blob);
460562
const link = document.createElement("a");
@@ -470,12 +572,12 @@ function usePaletteInActiveTools() {
470572
return;
471573
}
472574
if (!isWorkspaceContext()) {
473-
refs.selectionText.textContent = "Use in Workspace Manager is available only in Workspace Manager context.";
575+
setSelectionText("Use in Workspace Manager is available only in Workspace Manager context.");
474576
return;
475577
}
476578
if (hasReservedPaletteKeyword(palette.name)) {
477579
const message = "Reserved palette names cannot be used for workspace shared palette. Duplicate and rename first.";
478-
refs.selectionText.textContent = message;
580+
setSelectionText(message);
479581
window.alert(message);
480582
return;
481583
}
@@ -486,7 +588,7 @@ function usePaletteInActiveTools() {
486588
&& existingSharedPalette.paletteId !== palette.id
487589
) {
488590
const message = `Shared palette is locked to ${existingSharedPalette.displayName}. Edit swatches instead.`;
489-
refs.selectionText.textContent = message;
591+
setSelectionText(message);
490592
window.alert(message);
491593
return;
492594
}
@@ -496,7 +598,7 @@ function usePaletteInActiveTools() {
496598
&& existingSharedPalette.paletteId === palette.id
497599
) {
498600
const message = "Shared palette is locked. Edit swatches instead.";
499-
refs.selectionText.textContent = message;
601+
setSelectionText(message);
500602
window.alert(message);
501603
return;
502604
}
@@ -511,9 +613,9 @@ function usePaletteInActiveTools() {
511613
sourceToolId: context.sourceToolId || "palette-browser"
512614
});
513615
const stored = writeSharedPaletteHandoff(handoff);
514-
refs.selectionText.textContent = stored
616+
setSelectionText(stored
515617
? `Shared palette handoff updated for ${getToolDisplayName(context.sourceToolId, "active tool")}: ${palette.name}`
516-
: "Shared palette handoff was not updated because the payload was invalid.";
618+
: "Shared palette handoff was not updated because the payload was invalid.");
517619
}
518620

519621
function deleteSelectedPalette() {
@@ -543,12 +645,17 @@ function deleteSelectedPalette() {
543645
}
544646

545647
function renderStoredSelection() {
648+
const workspaceMode = isWorkspaceContext();
546649
const handoff = readSharedPaletteHandoff();
547650
if (!handoff) {
548-
refs.selectionText.textContent = "No handoff recorded yet.";
651+
setSelectionText(workspaceMode
652+
? "No handoff recorded yet."
653+
: "No workspace handoff recorded yet.", { muted: !workspaceMode });
549654
return;
550655
}
551-
refs.selectionText.textContent = `Active handoff: ${handoff.displayName} (${handoff.selectedAt})`;
656+
setSelectionText(workspaceMode
657+
? `Active handoff: ${handoff.displayName} (${handoff.selectedAt})`
658+
: `Last workspace handoff: ${handoff.displayName} (${handoff.selectedAt})`, { muted: !workspaceMode });
552659
}
553660

554661
function bindEvents() {
@@ -584,6 +691,23 @@ function bindEvents() {
584691
refs.swatchColorInput.addEventListener("input", updateSelectedSwatchFromInputs);
585692
refs.swatchNameInput.addEventListener("input", updateSelectedSwatchFromInputs);
586693
refs.swatchSymbolInput.addEventListener("input", updateSelectedSwatchFromInputs);
694+
refs.importPaletteJsonButton.addEventListener("click", () => {
695+
refs.importPaletteJsonInput?.click();
696+
});
697+
refs.importPaletteJsonInput?.addEventListener("change", async () => {
698+
const file = refs.importPaletteJsonInput.files?.[0];
699+
refs.importPaletteJsonInput.value = "";
700+
if (!file) {
701+
return;
702+
}
703+
try {
704+
await importPaletteJsonFromFile(file);
705+
} catch (error) {
706+
const message = `Import failed: ${error instanceof Error ? error.message : "invalid JSON"}`;
707+
setSelectionText(message);
708+
window.alert(message);
709+
}
710+
});
587711
refs.copyPaletteJsonButton.addEventListener("click", copyPaletteJson);
588712
refs.exportPaletteJsonButton.addEventListener("click", exportPaletteJson);
589713
refs.usePaletteButton.addEventListener("click", usePaletteInActiveTools);

tools/Palette Browser/paletteBrowser.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
font-size: 0.95rem;
4444
}
4545

46+
.palette-browser__hint.is-context-muted {
47+
color: #b3a7c9;
48+
}
49+
4650
.palette-browser__list {
4751
display: grid;
4852
gap: 0.18rem;

tools/Parallax Scene Studio/main.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { buildDebugVisualizationLayer, summarizeDebugVisualizationLayer } from "
3333
import { registerToolBootContract } from "../shared/toolBootContract.js";
3434
import { createLivePreviewSyncBridge, validateStateBindingPayload } from "../shared/livePreviewSyncChannel.js";
3535
import { normalizeToolSamplePath, toToolSampleLabel } from "../shared/toolSampleCatalog.js";
36+
import { addToolModeMetadata, assertStandaloneToolDocument, offerImportMismatchOptions } from "../shared/documentModeGuards.js";
3637

3738
const SAMPLE_DIRECTORY_PATH = "./samples/";
3839
const SAMPLE_MANIFEST_PATH = "./samples/sample-manifest.json";
@@ -1133,7 +1134,7 @@ class ParallaxEditorApp {
11331134
this.updateStatus(`${getBlockingAssetValidationMessage("Save Project", validation)} (${summarizeAssetValidation(validation)}).`);
11341135
return;
11351136
}
1136-
const output = createRegistryManagedParallaxSaveDocument(this.documentModel);
1137+
const output = addToolModeMetadata(createRegistryManagedParallaxSaveDocument(this.documentModel), { toolId: "parallax-editor" });
11371138
const { graph, findings } = buildAssetDependencyGraph(this.assetRegistry);
11381139
this.assetDependencyGraphSnapshot = graph;
11391140
output.project = {
@@ -1219,6 +1220,22 @@ class ParallaxEditorApp {
12191220
reader.onload = () => {
12201221
try {
12211222
const raw = JSON.parse(String(reader.result));
1223+
const guard = assertStandaloneToolDocument(raw, {
1224+
expectedLabel: "Parallax project",
1225+
acceptedSchemas: ["toolbox.parallax/1", "toolbox.tilemap/1"],
1226+
requiredToolId: "parallax-editor"
1227+
});
1228+
if (!guard.ok) {
1229+
const handled = offerImportMismatchOptions(guard, {
1230+
viewerToolId: "state-inspector",
1231+
viewerPayload: raw,
1232+
sourceToolId: "parallax-editor"
1233+
});
1234+
if (handled) {
1235+
return;
1236+
}
1237+
throw new Error(guard.reason);
1238+
}
12221239
this.documentModel = extractParallaxDocument(raw);
12231240
this.assetDependencyGraphSnapshot = raw?.project?.assetDependencyGraph || null;
12241241
const resolution = this.resolveAssetRefsFromRegistry();

tools/Sprite Editor/modules/spriteEditorApp.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
MAX_RECENT_COLORS,
1414
NO_PALETTE_ID,
1515
PALETTE_SOURCE_ID,
16+
PROJECT_FORMAT,
1617
TOOL_IDS
1718
} from "./constants.js";
1819
import { colorToPickerValue, dedupeColors, isTransparent, normalizeColor, rgbaToHex } from "./colorUtils.js";
@@ -51,6 +52,7 @@ import {
5152
import { buildProjectPackage, summarizeProjectPackaging } from "../../shared/projectPackaging.js";
5253
import { buildEditorExperienceLayer, summarizeEditorExperienceLayer } from "../../shared/editorExperienceLayer.js";
5354
import { buildDebugVisualizationLayer, summarizeDebugVisualizationLayer } from "../../shared/debugVisualizationLayer.js";
55+
import { addToolModeMetadata, assertStandaloneToolDocument, offerImportMismatchOptions } from "../../shared/documentModeGuards.js";
5456

5557
function getRequiredElement(id) {
5658
const element = document.getElementById(id);
@@ -1321,7 +1323,7 @@ async function saveProjectJson(state) {
13211323
}
13221324
const { graph, findings } = buildAssetDependencyGraph(state.assetRegistry);
13231325
state.assetDependencyGraphSnapshot = graph;
1324-
const payload = serializeProject(state.project);
1326+
const payload = addToolModeMetadata(serializeProject(state.project), { toolId: "sprite-editor" });
13251327
payload.project = {
13261328
...(payload.project && typeof payload.project === "object" ? payload.project : {}),
13271329
assetDependencyGraph: graph
@@ -1338,6 +1340,22 @@ async function saveProjectJson(state) {
13381340
async function loadProjectJson(state, file) {
13391341
const text = await fileToText(file);
13401342
const parsed = JSON.parse(text);
1343+
const guard = assertStandaloneToolDocument(parsed, {
1344+
expectedLabel: "Sprite Editor project",
1345+
acceptedFormats: [PROJECT_FORMAT],
1346+
requiredToolId: "sprite-editor"
1347+
});
1348+
if (!guard.ok) {
1349+
const handled = offerImportMismatchOptions(guard, {
1350+
viewerToolId: "state-inspector",
1351+
viewerPayload: parsed,
1352+
sourceToolId: "sprite-editor"
1353+
});
1354+
if (handled) {
1355+
return;
1356+
}
1357+
throw new Error(guard.reason);
1358+
}
13411359
const nextProject = ensureProjectShape(parsed);
13421360
state.project = nextProject;
13431361
state.assetDependencyGraphSnapshot = parsed?.project?.assetDependencyGraph || null;

0 commit comments

Comments
 (0)