Skip to content

Commit 52ef884

Browse files
author
DavidQ
committed
Unify tool UI/UX contract and enforce consistent lifecycle, selection, and workspace behavior - PR 10.7
1 parent f853410 commit 52ef884

16 files changed

Lines changed: 481 additions & 43 deletions

File tree

docs/dev/codex_commands.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
# Codex Commands — PR 10.6V
1+
# CODEX COMMANDS
22

3-
Model: GPT-5.4
4-
Reasoning: high
3+
model: gpt-5.3-codex
4+
reasoning: medium
55

6-
```powershell
7-
codex "Execute PR 10.6V from docs/pr/PR_10_6V_FINAL_DOD.md. Make the smallest scoped implementation changes required to satisfy the DoD. Do not broaden scope. Do not add fallback/demo data. Do not hardcode sample paths. Preserve manifest-driven input contracts. Create required reports under docs/dev/reports. Run npm run test:launch-smoke:games and npm run test:sample-standalone:data-flow. Package the result as <project>/tmp/PR_10_6V.zip."
8-
```
6+
Apply PR_10_7_UNIFIED_TOOL_UX_CONTRACT
7+
8+
- Implement shared layout zones across all tools
9+
- Enforce lifecycle states
10+
- Add first-item auto-selection
11+
- Enforce control enable/disable rules
12+
- Add explicit empty-state UI (no fallback data)
13+
- Ensure tools behave correctly inside workspace (no reset, no auto-close)
14+
- Do not modify data layer
15+
- Do not refactor unrelated code

docs/dev/commit_comment.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Enforce final tool UI DoD selection, enablement, lifecycle, and empty-state gates - PR 10.6V
1+
Unify tool UI/UX contract and enforce consistent lifecycle, selection, and workspace behavior - PR 10.7

docs/dev/reports/REPORT_PR_10_7.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# REPORT_PR_10_7
2+
3+
## Summary
4+
Establishes a single UI/UX + state contract before tool-by-tool UAT fixes.
5+
6+
## Why
7+
Current tools behave as one-offs, causing inconsistent UX and masking real issues.
8+
9+
## Result
10+
Creates a stable baseline so UAT fixes are deterministic and repeatable.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# BUILD_PR_10_7_UNIFIED_TOOL_UX_CONTRACT
2+
3+
## Definitions
4+
5+
### Layout Zones (MANDATORY)
6+
1. Left Panel: Data/Asset list
7+
2. Center Canvas: Main work area
8+
3. Right Panel: Properties / Controls
9+
4. Top Bar: Tool name + context actions
10+
11+
### State Lifecycle (MANDATORY)
12+
- INIT: tool mounted, no data
13+
- LOADING: fetching manifest/input
14+
- READY_EMPTY: no data → show empty state
15+
- READY_SELECTED: first item auto-selected
16+
- INTERACTING: user modifying
17+
18+
### Selection Rules
19+
- First valid item MUST auto-select
20+
- Selection MUST be visually highlighted
21+
- Selection MUST persist (no reset unless explicit)
22+
23+
### Control Enablement
24+
- Controls enabled ONLY when selection exists
25+
- Disabled state must be visually obvious (not silent)
26+
27+
### Empty State (NO FALLBACK DATA)
28+
- Show message: "No data loaded"
29+
- Show instruction: "Load or create asset"
30+
- No hidden defaults
31+
32+
### Workspace Contract
33+
- Tool must:
34+
- not auto-close
35+
- not reset on focus change
36+
- not reload without explicit action
37+
38+
## Deliverables
39+
- docs/pr/* (this bundle)
40+
- Codex applies contract across tools in-place
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# PLAN_PR_10_7_UNIFIED_TOOL_UX_CONTRACT
2+
3+
## Purpose
4+
Define a single, enforced UI/UX + state contract across all tools to eliminate one-off behavior before tool-level UAT fixes.
5+
6+
## Scope (STRICT)
7+
- Define shared UI layout regions
8+
- Define shared state lifecycle (init → load → select → ready)
9+
- Define selection rules (auto-select first item)
10+
- Define control enablement rules
11+
- Define empty-state UX (no silent data)
12+
- Define workspace embedding contract
13+
- No implementation code
14+
- No data changes
15+
16+
## Affected Tools
17+
- Asset Browser
18+
- Sprite Editor
19+
- Tilemap Studio
20+
- Vector Asset Studio
21+
- Vector Map Editor
22+
23+
## Non-Goals
24+
- No feature additions
25+
- No data schema changes
26+
- No rendering changes
27+
28+
## Acceptance
29+
- All tools follow same layout zones
30+
- All tools follow same selection + enablement rules
31+
- All tools show consistent empty state
32+
- All tools operate correctly inside Workspace container

tools/Asset Browser/index.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
</div>
1919
</details>
2020
<div class="asset-browser app-shell">
21+
<header class="panel asset-browser__panel asset-browser__topbar" data-tool-ux-zone="top-bar">
22+
<strong>Asset Browser / Import Hub</strong>
23+
<span class="asset-browser__hint">Shared manifest-driven asset workflow.</span>
24+
</header>
2125
<div class="asset-browser__layout tools-platform-layout-grid">
22-
<aside class="panel asset-browser__panel tools-platform-resize-panel" data-panel-side="left">
26+
<aside class="panel asset-browser__panel tools-platform-resize-panel" data-panel-side="left" data-tool-ux-zone="left-panel">
2327
<h3>Browse Approved Assets</h3>
2428
<div id="launchContextText" class="asset-browser__hint">Shared asset flow ready.</div>
2529
<label class="field">
@@ -34,19 +38,19 @@ <h3>Browse Approved Assets</h3>
3438
<div id="assetList" class="asset-browser__list" aria-label="Approved asset catalog"></div>
3539
</aside>
3640

37-
<main class="panel asset-browser__panel">
41+
<main class="panel asset-browser__panel" data-tool-ux-zone="center-canvas">
3842
<h3 id="assetPreviewTitle">Preview</h3>
3943
<div id="assetPreviewMeta" class="asset-browser__meta">Select an approved asset from the catalog.</div>
4044
<div id="assetPreviewCanvas" class="asset-browser__preview">
41-
<p class="asset-browser__empty">No asset selected.</p>
45+
<p class="asset-browser__empty">No data loaded. Load or create asset.</p>
4246
</div>
4347
<div class="asset-browser__actions tools-platform-control-row">
4448
<button id="useAssetInToolButton" type="button">Use In Active Tool</button>
4549
</div>
4650
<pre id="assetPreviewText" class="asset-browser__text-preview">Choose a file to inspect metadata and content.</pre>
4751
</main>
4852

49-
<aside class="panel asset-browser__panel tools-platform-resize-panel" data-panel-side="right">
53+
<aside class="panel asset-browser__panel tools-platform-resize-panel" data-panel-side="right" data-tool-ux-zone="right-panel">
5054
<h3>Import Plan</h3>
5155
<p class="asset-browser__hint">This tool does not write files into the repo. It validates a local file and generates an import plan that targets approved project folders.</p>
5256
<label class="field">

tools/Asset Browser/main.js

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import {
1919
logToolUiLifecycle
2020
} from "../shared/toolLoadDiagnostics.js";
2121
import { ACTIVE_PROJECT_STORAGE_KEY } from "../shared/projectManifestContract.js";
22+
import {
23+
TOOL_UX_LIFECYCLE,
24+
getUnifiedEmptyStateMessage,
25+
setToolUxLifecycleState
26+
} from "../shared/unifiedToolUxContract.js";
2227

2328
const APPROVED_DESTINATIONS = Object.freeze({
2429
"Vector Assets": "games/<project>/assets/vectors/",
@@ -104,6 +109,48 @@ const state = {
104109
}
105110
};
106111

112+
function setAssetBrowserLifecycle(stateName, details = {}) {
113+
setToolUxLifecycleState("asset-browser", stateName, details);
114+
}
115+
116+
function ensureFirstVisibleAssetSelection(entries) {
117+
const source = Array.isArray(entries) ? entries : [];
118+
if (source.length <= 0) {
119+
state.selectedAssetId = "";
120+
return false;
121+
}
122+
const hasCurrent = source.some((entry) => entry.id === state.selectedAssetId);
123+
if (hasCurrent) {
124+
return false;
125+
}
126+
state.selectedAssetId = source[0].id;
127+
return true;
128+
}
129+
130+
function syncAssetBrowserUxContract(options = {}) {
131+
const interactive = options.interacting === true;
132+
const approvedCount = Array.isArray(state.assetCatalog) ? state.assetCatalog.length : 0;
133+
const hasSelection = Boolean(getSelectedAsset());
134+
if (refs.useAssetInToolButton instanceof HTMLButtonElement) {
135+
refs.useAssetInToolButton.disabled = !hasSelection;
136+
}
137+
if (interactive) {
138+
setAssetBrowserLifecycle(TOOL_UX_LIFECYCLE.INTERACTING, {
139+
approvedCount,
140+
hasSelection
141+
});
142+
return;
143+
}
144+
if (approvedCount <= 0) {
145+
setAssetBrowserLifecycle(TOOL_UX_LIFECYCLE.READY_EMPTY, { approvedCount });
146+
return;
147+
}
148+
setAssetBrowserLifecycle(
149+
hasSelection ? TOOL_UX_LIFECYCLE.READY_SELECTED : TOOL_UX_LIFECYCLE.READY_EMPTY,
150+
{ approvedCount, hasSelection }
151+
);
152+
}
153+
107154
function sanitizeText(value) {
108155
return typeof value === "string" ? value.trim() : "";
109156
}
@@ -834,6 +881,7 @@ function populateDestinationOptions(category) {
834881

835882
function renderAssetList() {
836883
const entries = getVisibleAssets();
884+
ensureFirstVisibleAssetSelection(entries);
837885
refs.countText.textContent = buildApprovedAssetStatusText(entries.length, state.catalogLoadInfo);
838886
refs.assetList.innerHTML = entries.length > 0
839887
? entries.map((entry) => {
@@ -846,20 +894,16 @@ function renderAssetList() {
846894
</button>
847895
`;
848896
}).join("")
849-
: `<p class="asset-browser__empty">${buildApprovedAssetEmptyStateText(state.catalogLoadInfo)}</p>`;
850-
851-
if (!entries.some((entry) => entry.id === state.selectedAssetId)) {
852-
state.selectedAssetId = "";
853-
}
897+
: `<p class="asset-browser__empty">${getUnifiedEmptyStateMessage()} ${buildApprovedAssetEmptyStateText(state.catalogLoadInfo)}</p>`;
854898
}
855899

856900
async function renderPreview() {
857901
const selectedAsset = getSelectedAsset();
858902
if (!selectedAsset) {
859903
refs.previewTitle.textContent = "Preview";
860-
refs.previewMeta.textContent = "Select an approved asset from the catalog.";
861-
refs.previewCanvas.innerHTML = '<p class="asset-browser__empty">No asset selected.</p>';
862-
refs.previewText.textContent = "Choose a file to inspect metadata and content.";
904+
refs.previewMeta.textContent = getUnifiedEmptyStateMessage();
905+
refs.previewCanvas.innerHTML = `<p class="asset-browser__empty">${getUnifiedEmptyStateMessage()}</p>`;
906+
refs.previewText.textContent = "Load or create asset.";
863907
return;
864908
}
865909

@@ -1038,49 +1082,66 @@ function syncImportFormFromFile() {
10381082

10391083
function bindEvents() {
10401084
refs.categoryFilter.addEventListener("change", () => {
1085+
syncAssetBrowserUxContract({ interacting: true });
10411086
state.selectedCategory = refs.categoryFilter.value;
10421087
renderAssetList();
10431088
renderPreview();
10441089
emitAssetBrowserControlReadiness();
1090+
syncAssetBrowserUxContract();
10451091
});
10461092

10471093
refs.searchInput.addEventListener("input", () => {
1094+
syncAssetBrowserUxContract({ interacting: true });
10481095
state.search = refs.searchInput.value;
10491096
renderAssetList();
10501097
renderPreview();
10511098
emitAssetBrowserControlReadiness();
1099+
syncAssetBrowserUxContract();
10521100
});
10531101

10541102
refs.assetList.addEventListener("click", (event) => {
10551103
const button = event.target instanceof Element ? event.target.closest("[data-asset-id]") : null;
10561104
if (!(button instanceof HTMLElement)) {
10571105
return;
10581106
}
1107+
syncAssetBrowserUxContract({ interacting: true });
10591108
state.selectedAssetId = button.dataset.assetId || "";
10601109
renderAssetList();
10611110
renderPreview();
10621111
emitAssetBrowserControlReadiness();
1112+
syncAssetBrowserUxContract();
10631113
});
10641114

1065-
refs.importFileInput.addEventListener("change", syncImportFormFromFile);
1115+
refs.importFileInput.addEventListener("change", () => {
1116+
syncAssetBrowserUxContract({ interacting: true });
1117+
syncImportFormFromFile();
1118+
syncAssetBrowserUxContract();
1119+
});
10661120
refs.importCategorySelect.addEventListener("change", () => {
1121+
syncAssetBrowserUxContract({ interacting: true });
10671122
populateDestinationOptions(refs.importCategorySelect.value);
10681123
renderImportPlan();
10691124
emitAssetBrowserControlReadiness();
1125+
syncAssetBrowserUxContract();
10701126
});
10711127
refs.importNameInput.addEventListener("input", () => {
1128+
syncAssetBrowserUxContract({ interacting: true });
10721129
renderImportPlan();
10731130
emitAssetBrowserControlReadiness();
1131+
syncAssetBrowserUxContract();
10741132
});
10751133
refs.validateImportButton.addEventListener("click", () => {
1134+
syncAssetBrowserUxContract({ interacting: true });
10761135
renderImportPlan();
10771136
emitAssetBrowserControlReadiness();
1137+
syncAssetBrowserUxContract();
10781138
});
10791139
refs.downloadImportPlanButton.addEventListener("click", downloadImportPlan);
10801140
refs.useAssetInToolButton.addEventListener("click", useSelectedAssetInActiveTool);
10811141
}
10821142

10831143
async function init() {
1144+
setAssetBrowserLifecycle(TOOL_UX_LIFECYCLE.LOADING);
10841145
await hydrateApprovedAssetCatalog();
10851146
const approvedAssetsState = String(state.catalogLoadInfo?.status || APPROVED_ASSET_STATUS.sourceMissing);
10861147
const approvedClassification = approvedAssetsState === APPROVED_ASSET_STATUS.loadedEmpty
@@ -1114,6 +1175,7 @@ async function init() {
11141175
await renderPreview();
11151176
renderImportPlan();
11161177
emitAssetBrowserControlReadiness();
1178+
syncAssetBrowserUxContract();
11171179
bindEvents();
11181180
}
11191181

@@ -1150,6 +1212,7 @@ const assetBrowserApi = {
11501212

11511213
function bootAssetBrowser() {
11521214
if (!initialized) {
1215+
setAssetBrowserLifecycle(TOOL_UX_LIFECYCLE.INIT);
11531216
void init().then(() => tryLoadPresetFromQuery());
11541217
initialized = true;
11551218
}

tools/Sprite Editor/index.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
</div>
2626
</details>
2727
<div class="app-shell" id="appShell">
28-
<section class="toolbar-row">
28+
<section class="toolbar-row" data-tool-ux-zone="top-bar">
2929
<div class="toolbar-group tools-platform-control-cluster tools-platform-control-cluster--primary">
3030
<label>Width
3131
<input id="canvasWidthInput" type="number" min="1" max="256" value="32" />
@@ -64,7 +64,7 @@
6464
</section>
6565

6666
<section class="workspace tools-platform-layout-grid">
67-
<div id="leftSidebar" class="left-panel tools-platform-resize-panel" data-panel-side="left">
67+
<div id="leftSidebar" class="left-panel tools-platform-resize-panel" data-panel-side="left" data-tool-ux-zone="left-panel">
6868
<details class="panel-accordion" id="leftPanelColors" open>
6969
<summary class="panel-accordion__summary"><h2>Colors</h2></summary>
7070
<div class="panel-accordion__body">
@@ -143,7 +143,7 @@ <h3>Recent</h3>
143143
</details>
144144
</div>
145145

146-
<div class="center-panel">
146+
<div class="center-panel" data-tool-ux-zone="center-canvas">
147147
<div class="fullscreen-overlay-toggle-group fullscreen-overlay-toggle-group--left">
148148
<button type="button" class="fullscreen-overlay-button" data-overlay-toggle data-overlay-side="left" data-overlay-target="leftPanelColors" aria-expanded="false">
149149
<span class="overlay-toggle-symbol" aria-hidden="true">+</span>
@@ -173,7 +173,7 @@ <h3>Recent</h3>
173173
</div>
174174
</div>
175175

176-
<div id="rightSidebar" class="right-panel tools-platform-resize-panel" data-panel-side="right">
176+
<div id="rightSidebar" class="right-panel tools-platform-resize-panel" data-panel-side="right" data-tool-ux-zone="right-panel">
177177
<details class="panel-accordion" id="rightPanelPreview" open>
178178
<summary class="panel-accordion__summary"><h2>Preview</h2></summary>
179179
<div class="panel-accordion__body">

0 commit comments

Comments
 (0)