Skip to content

Commit e5cc63e

Browse files
author
DavidQ
committed
Working through Workspace
1 parent 12976be commit e5cc63e

8 files changed

Lines changed: 208 additions & 13 deletions

File tree

tools/Palette Browser/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,14 @@ <h3 id="paletteTitle">Palette Preview</h3>
6969

7070
<aside class="panel palette-browser__panel tools-platform-resize-panel" data-panel-side="right">
7171
<h3>Actions &amp; Validation</h3>
72+
<pre id="paletteJsonPreview" class="palette-browser__json-preview">Palette JSON preview will appear here.</pre>
7273
<div id="paletteValidationText" class="palette-browser__hint">Validation summary will appear here.</div>
73-
<div class="palette-browser__actions palette-browser__actions--stacked tools-platform-control-row">
74+
<div class="palette-browser__actions tools-platform-control-row">
7475
<button id="copyPaletteJsonButton" type="button">Copy Palette JSON</button>
7576
<button id="exportPaletteJsonButton" type="button">Export Palette JSON</button>
76-
<button id="usePaletteButton" type="button">Use In Active Tools</button>
77+
<button id="usePaletteButton" type="button">Use in Workspace Manager</button>
7778
</div>
7879
<div id="paletteSelectionText" class="palette-browser__hint">No handoff recorded yet.</div>
79-
<pre id="paletteJsonPreview" class="palette-browser__json-preview">Palette JSON preview will appear here.</pre>
8080
</aside>
8181
</div>
8282
</div>

tools/Palette Browser/main.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,31 @@ const state = {
4444
hiddenBuiltInPaletteIds: loadHiddenBuiltInPaletteIds()
4545
};
4646

47+
function isWorkspaceContext() {
48+
if (typeof window === "undefined") {
49+
return false;
50+
}
51+
const searchParams = new URLSearchParams(window.location.search);
52+
const currentPath = window.location.pathname || "";
53+
const isHostedWorkspaceView = searchParams.get("hosted") === "1"
54+
|| searchParams.has("hostToolId")
55+
|| searchParams.has("hostContextId");
56+
const isWorkspaceManagerReferrer = /\/tools\/Workspace(?:%20| )Manager\//i.test(document.referrer || "");
57+
const isWorkspaceManagerParent = (() => {
58+
try {
59+
return window.top !== window
60+
&& /\/tools\/Workspace(?:%20| )Manager\//i.test(window.top.location.pathname || "");
61+
} catch {
62+
return false;
63+
}
64+
})();
65+
return isHostedWorkspaceView
66+
|| isWorkspaceManagerReferrer
67+
|| isWorkspaceManagerParent
68+
|| /\/tools\/Workspace%20Manager\//i.test(currentPath)
69+
|| /\/tools\/Workspace Manager\//i.test(currentPath);
70+
}
71+
4772
function hasDeleteOverrideParam() {
4873
const params = new URLSearchParams(window.location.search);
4974
const raw = params.get("overridReserveWorkBlock")
@@ -208,6 +233,7 @@ function renderPaletteList() {
208233

209234
function renderSelectedPalette() {
210235
const palette = getSelectedPalette();
236+
const canUseInActiveTools = isWorkspaceContext();
211237
if (!palette) {
212238
refs.paletteTitle.textContent = "Palette Preview";
213239
refs.paletteSummaryText.textContent = "Select a palette to inspect its swatches.";
@@ -221,6 +247,7 @@ function renderSelectedPalette() {
221247
refs.deletePaletteButton.disabled = true;
222248
refs.addSwatchButton.disabled = true;
223249
refs.deleteSwatchButton.disabled = true;
250+
refs.usePaletteButton.disabled = true;
224251
refs.jsonPreview.textContent = "Palette JSON preview will appear here.";
225252
refs.validationText.textContent = "Validation summary will appear here.";
226253
return;
@@ -239,6 +266,7 @@ function renderSelectedPalette() {
239266
refs.deletePaletteButton.disabled = canOverrideDeleteGuard ? false : readOnly;
240267
refs.addSwatchButton.disabled = readOnly;
241268
refs.deleteSwatchButton.disabled = readOnly;
269+
refs.usePaletteButton.disabled = !canUseInActiveTools;
242270

243271
refs.paletteSwatches.innerHTML = palette.entries.map((entry, index) => {
244272
const currentClass = index === state.selectedSwatchIndex ? " is-current" : "";
@@ -440,6 +468,10 @@ function usePaletteInActiveTools() {
440468
if (!palette) {
441469
return;
442470
}
471+
if (!isWorkspaceContext()) {
472+
refs.selectionText.textContent = "Use in Workspace Manager is available only in Workspace Manager context.";
473+
return;
474+
}
443475
const context = getSharedLaunchContext();
444476
const handoff = createPaletteHandoff({
445477
paletteId: palette.id,

tools/Palette Browser/paletteBrowser.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,6 @@
137137
flex-wrap: nowrap;
138138
}
139139

140-
.palette-browser__actions--stacked {
141-
display: grid;
142-
}
143-
144140
.palette-browser__json-preview {
145141
margin: 0;
146142
min-height: 0;

tools/shared/platformShell.css

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,57 @@ body.tools-platform-surface {
174174

175175
.tools-platform-frame__nav-bucket-links {
176176
display: flex;
177-
align-items: center;
177+
align-items: stretch;
178178
gap: 10px;
179+
flex-wrap: nowrap;
180+
flex-direction: column;
181+
}
182+
183+
.tools-platform-frame__nav-tool-row {
184+
display: grid;
185+
gap: 6px;
186+
align-items: start;
187+
padding-bottom: 10px;
188+
margin-bottom: 10px;
189+
border-bottom: 1px solid rgba(221, 214, 254, 0.18);
190+
}
191+
192+
.tools-platform-frame__nav-tool-row:last-child {
193+
padding-bottom: 0;
194+
margin-bottom: 0;
195+
border-bottom: 0;
196+
}
197+
198+
.tools-platform-frame__binding-badges {
199+
display: flex;
179200
flex-wrap: wrap;
201+
gap: 6px;
202+
}
203+
204+
.tools-platform-frame__binding-badge {
205+
display: inline-flex;
206+
align-items: center;
207+
min-height: 22px;
208+
padding: 0 8px;
209+
border: 1px solid var(--line, rgba(221, 214, 254, 0.26));
210+
border-radius: 999px;
211+
font-size: 0.74rem;
212+
color: var(--muted, #e9ddff);
213+
background: rgba(43, 16, 91, 0.75);
214+
}
215+
216+
.tools-platform-frame__binding-badge.is-active {
217+
color: #ffffff;
218+
border-color: var(--tools-shell-border-strong);
219+
background: linear-gradient(180deg, rgba(167, 139, 250, 0.34) 0%, rgba(91, 33, 182, 0.78) 100%);
220+
}
221+
222+
.tools-platform-frame__binding-badge.is-muted {
223+
color: #b3a7c9;
224+
}
225+
226+
.tools-platform-frame__binding-badge.is-project {
227+
color: #ffffff;
180228
}
181229

182230
.tools-platform-frame__nav {

tools/shared/platformShell.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { createRuntimeMonitoringHooks } from "../../src/engine/runtime/index.js"
1414
let workspaceController = null;
1515
let headerExpandedState = null;
1616
let runtimeMonitoringHooks = null;
17+
let bindingRefreshHandlersBound = false;
1718

1819
const HEADER_EXPANDED_STORAGE_KEY = "toolboxaid.toolsPlatform.headerExpanded";
1920
const HEADER_EXPANDED_FALLBACK_TOOL = "tool-host";
@@ -74,6 +75,62 @@ function getManifest() {
7475
return workspaceController ? workspaceController.getManifest() : null;
7576
}
7677

78+
function formatBindingValue(label, value, fallback = "none") {
79+
const safe = String(value || "").trim();
80+
return `${label}: ${safe || fallback}`;
81+
}
82+
83+
function resolveProjectBindingLabel() {
84+
const manifest = getManifest();
85+
if (!manifest) {
86+
return "Workspace: none";
87+
}
88+
return manifest.dirty === true ? "Workspace: Unsaved" : "Workspace: Loaded";
89+
}
90+
91+
function getToolBindingCompatibility(toolId) {
92+
const paletteCapable = new Set([
93+
"palette-browser",
94+
"sprite-editor",
95+
"vector-asset-studio"
96+
]);
97+
const assetCapable = new Set([
98+
"asset-browser",
99+
"sprite-editor"
100+
]);
101+
return {
102+
palette: paletteCapable.has(toolId),
103+
asset: assetCapable.has(toolId)
104+
};
105+
}
106+
107+
function renderToolBindingBadges(tool) {
108+
const palette = readSharedPaletteHandoff();
109+
const asset = readSharedAssetHandoff();
110+
const compatibility = getToolBindingCompatibility(tool.id);
111+
const paletteLabel = compatibility.palette
112+
? formatBindingValue("Palette", palette?.displayName, "none")
113+
: "Palette: Not used";
114+
const assetLabel = compatibility.asset
115+
? formatBindingValue("Asset", asset?.displayName, "none")
116+
: "Asset: Not used";
117+
const projectLabel = resolveProjectBindingLabel();
118+
const paletteTitle = compatibility.palette
119+
? `Updated: ${escapeHtml(palette?.selectedAt || "not-set")}`
120+
: "Not used by this tool";
121+
const assetTitle = compatibility.asset
122+
? `Updated: ${escapeHtml(asset?.selectedAt || "not-set")}`
123+
: "Not used by this tool";
124+
125+
return `
126+
<div class="tools-platform-frame__binding-badges" aria-label="Tool data bindings">
127+
<span class="tools-platform-frame__binding-badge${compatibility.palette ? " is-active" : " is-muted"}" title="${paletteTitle}">${escapeHtml(paletteLabel)}</span>
128+
<span class="tools-platform-frame__binding-badge${compatibility.asset ? " is-active" : " is-muted"}" title="${assetTitle}">${escapeHtml(assetLabel)}</span>
129+
<span class="tools-platform-frame__binding-badge is-project" title="Workspace manifest dirty state">${escapeHtml(projectLabel)}</span>
130+
</div>
131+
`;
132+
}
133+
77134
function renderToolLinks(currentToolId) {
78135
const tools = getToolRegistry()
79136
.filter((entry) => entry.active === true)
@@ -98,7 +155,12 @@ function renderToolLinks(currentToolId) {
98155
${bucketTools
99156
.map((tool) => {
100157
const currentClass = tool.id === currentToolId ? " is-current" : "";
101-
return `<a class="tools-platform-frame__nav-link${currentClass}" href="${escapeHtml(getRegistryEntryHref(tool.entryPoint))}">${escapeHtml(tool.displayName)}</a>`;
158+
return `
159+
<div class="tools-platform-frame__nav-tool-row">
160+
<a class="tools-platform-frame__nav-link${currentClass}" href="${escapeHtml(getRegistryEntryHref(tool.entryPoint))}">${escapeHtml(tool.displayName)}</a>
161+
${renderToolBindingBadges(tool)}
162+
</div>
163+
`;
102164
})
103165
.join("")}
104166
</div>
@@ -376,6 +438,25 @@ function renderShell(currentTool) {
376438
bindWorkspaceShellEvents(currentTool);
377439
}
378440

441+
function bindLiveBindingRefresh(currentTool) {
442+
if (bindingRefreshHandlersBound || typeof window === "undefined") {
443+
return;
444+
}
445+
bindingRefreshHandlersBound = true;
446+
447+
const rerender = () => {
448+
renderShell(currentTool);
449+
};
450+
451+
window.addEventListener("storage", rerender);
452+
window.addEventListener("focus", rerender);
453+
document.addEventListener("visibilitychange", () => {
454+
if (document.visibilityState === "visible") {
455+
rerender();
456+
}
457+
});
458+
}
459+
379460
function ensureRuntimeMonitoring() {
380461
if (runtimeMonitoringHooks) {
381462
return;
@@ -482,6 +563,7 @@ function initPlatformShell() {
482563
}
483564

484565
renderShell(currentTool);
566+
bindLiveBindingRefresh(currentTool);
485567
}
486568

487569
initPlatformShell();

tools/shared/projectManifestContract.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
export const PROJECT_MANIFEST_SCHEMA = "html-js-gaming.project";
1111
export const PROJECT_MANIFEST_VERSION = 1;
1212
export const ACTIVE_PROJECT_STORAGE_KEY = "toolboxaid.projectSystem.activeManifest";
13+
export const PROJECT_DOCUMENT_KIND = "workspace-manifest";
1314

1415
const sanitizeString = safeString;
1516

@@ -97,6 +98,7 @@ export function createEmptyProjectManifest(options = {}) {
9798
const tools = sanitizeToolsBlock(options.tools);
9899
const name = sanitizeString(options.name, "Untitled Project");
99100
const manifest = {
101+
documentKind: PROJECT_DOCUMENT_KIND,
100102
schema: PROJECT_MANIFEST_SCHEMA,
101103
version: PROJECT_MANIFEST_VERSION,
102104
id: sanitizeString(options.id, createProjectId()),
@@ -153,6 +155,10 @@ export function migrateProjectManifest(rawManifest) {
153155
migrated.migration.applied.push("normalize-legacy-project-shape");
154156
}
155157

158+
if (sanitizeString(rawManifest.documentKind, "") !== PROJECT_DOCUMENT_KIND) {
159+
migrated.migration.applied.push("project-document-kind-added");
160+
}
161+
156162
if (sourceVersion > PROJECT_MANIFEST_VERSION) {
157163
migrated.migration.applied.push("forward-version-opened-as-compatible");
158164
}
@@ -175,6 +181,10 @@ export function validateProjectManifest(rawManifest) {
175181
issues.push(`Expected schema ${PROJECT_MANIFEST_SCHEMA} but received ${manifest.schema || "unknown"}.`);
176182
}
177183

184+
if (sanitizeString(manifest.documentKind, "") !== PROJECT_DOCUMENT_KIND) {
185+
warnings.push(`Project documentKind expected ${PROJECT_DOCUMENT_KIND} but received ${manifest.documentKind || "unknown"}.`);
186+
}
187+
178188
if (!Number.isInteger(manifest.version) || manifest.version < 1) {
179189
issues.push("Project manifest version must be a positive integer.");
180190
}

tools/shared/projectSystem.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ export function createWorkspaceSystemController(options = {}) {
8181
appliedInitialState: false
8282
};
8383

84+
function serializeForDirtyComparison(manifest) {
85+
const normalized = cloneValue(manifest);
86+
normalized.dirty = false;
87+
return serializeProjectManifest(normalized);
88+
}
89+
8490
function computeObservedManifest() {
8591
const toolAdapter = adapter();
8692
const currentManifest = state.manifest
@@ -92,7 +98,7 @@ export function createWorkspaceSystemController(options = {}) {
9298

9399
currentManifest.activeToolId = toolId;
94100
currentManifest.workspace.lastOpenTool = toolId;
95-
currentManifest.updatedAt = new Date().toISOString();
101+
currentManifest.updatedAt = safeString(currentManifest.updatedAt, new Date().toISOString());
96102
currentManifest.sharedReferences = captureSharedReferenceSnapshot();
97103
currentManifest.tools = currentManifest.tools && typeof currentManifest.tools === "object"
98104
? currentManifest.tools
@@ -116,7 +122,7 @@ export function createWorkspaceSystemController(options = {}) {
116122

117123
function updateDirtyState(reason = "") {
118124
const observed = computeObservedManifest();
119-
const observedHash = serializeProjectManifest(observed);
125+
const observedHash = serializeForDirtyComparison(observed);
120126
state.manifest = observed;
121127
state.lastObservedHash = observedHash;
122128
state.adapterReady = adapter().ready;
@@ -133,7 +139,9 @@ export function createWorkspaceSystemController(options = {}) {
133139

134140
function markSaved(reason = "") {
135141
const observed = computeObservedManifest();
136-
const serialized = serializeProjectManifest(observed);
142+
observed.updatedAt = new Date().toISOString();
143+
observed.dirty = false;
144+
const serialized = serializeForDirtyComparison(observed);
137145
state.manifest = observed;
138146
state.baselineHash = serialized;
139147
state.lastObservedHash = serialized;

0 commit comments

Comments
 (0)