Fix renderer scope collisions, editor bugs; add welcome screen

Bug fixes from code review:
- Wrap renderer modules (canvas/dialogs/editor/app) in IIFEs: duplicate
  top-level 'const api' across plain scripts threw a SyntaxError that
  prevented app.js from ever running (blank window), and dialogs.js/
  editor.js silently overrode each other's labeledRow/makeSelect
- Focused-view toggle now writes step.focusedView.enabled instead of a
  nonexistent flat field that the schema dropped on save
- Annotation property edits no longer rebuild the panel on every
  keystroke (focus was stolen mid-typing); debounced save instead
- flushStep/undo/redo keep this.steps in sync with stepMap so the step
  list stops going stale after the first save
- Escape now deselects the annotation; Delete remains the delete key

Welcome screen (per spec): app opens to a title at top and three
buttons at the bottom — New Capture (creates a guide, opens the editor,
starts a capture session), Existing Workspace (library), Settings.
Brand click returns to the welcome screen.

Adds an env-gated dev screenshot hook (STEPFORGE_SCREENSHOT[_JS]) used
to visually verify welcome/library/editor views under WSLg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Iisyourdad
2026-06-10 21:29:14 -05:00
parent 4ab18080ce
commit 03bd9b0e2b
6 changed files with 198 additions and 20 deletions
+43 -13
View File
@@ -1,5 +1,7 @@
'use strict';
(() => {
const api = window.stepforge;
const dialogs = window.StepForgeDialogs || {};
@@ -317,7 +319,20 @@ class GuideEditor {
bindCheckbox(this.dom.hiddenToggle.querySelector('input'), 'hidden');
bindCheckbox(this.dom.skippedToggle.querySelector('input'), 'skipped');
bindCheckbox(this.dom.forceNewPageToggle.querySelector('input'), 'forceNewPage');
bindCheckbox(this.dom.focusedViewToggle.querySelector('input'), 'focusedViewDefault');
// Focused view lives under step.focusedView.enabled, not a flat field.
const focusedInput = this.dom.focusedViewToggle.querySelector('input');
focusedInput.addEventListener('change', () => {
if (!this.currentStep) return;
this.currentStep.focusedView = {
zoom: 1.5, panX: 0.5, panY: 0.5,
...(this.currentStep.focusedView || {}),
enabled: focusedInput.checked,
};
this.pendingSave = true;
this.saveStepDebounced();
this.emitMeta();
});
this.dom.descEditor.addEventListener('focus', () => {
if (this.currentStep) this.pushCanvasHistory('description');
@@ -511,17 +526,20 @@ class GuideEditor {
{ value: 'right', label: 'Right' },
]);
const apply = async (patch) => {
// Light-weight apply: mutate the selected annotation, redraw, and let the
// debounced save flush. Re-rendering the panel here would rebuild the
// inputs and steal focus mid-keystroke, so only structural changes
// (type/tail) pass rerender: true.
const apply = (patch, { rerender = false } = {}) => {
const ann = this.canvas.selected();
if (!ann) return;
Object.assign(ann, patch);
this.beforeCanvasSnapshot = null;
step.annotations = clone(this.canvas.annotations || []);
this.pendingSave = true;
this.canvas.setAnnotations(step.annotations || []);
this.canvas.select(ann.id);
await this.flushStep();
this.renderAnnotationPanel();
this.renderStepList();
this.canvas.render();
this.saveStepDebounced();
if (rerender) this.renderAnnotationPanel();
this.emitMeta();
};
@@ -548,10 +566,10 @@ class GuideEditor {
);
this.dom.annotationEditor.append(annSection);
typeSelect.addEventListener('change', async () => {
typeSelect.addEventListener('change', () => {
const ann = this.canvas.selected();
if (!ann) return;
await apply({ type: typeSelect.value });
apply({ type: typeSelect.value }, { rerender: true });
if (ann.type === 'tooltip') this.editAnnotationText(ann);
});
textInput.addEventListener('focus', () => this.pushCanvasHistory('annotation-text'));
@@ -671,6 +689,8 @@ class GuideEditor {
this.canvasFuture.push(clone(this.currentStep));
const previous = this.canvasHistory.pop();
this.stepMap.set(previous.stepId, previous);
const prevIdx = this.steps.findIndex((s) => s.stepId === previous.stepId);
if (prevIdx >= 0) this.steps[prevIdx] = previous;
this.selectedStepId = previous.stepId;
await this.flushStep(previous);
this.renderAll();
@@ -685,6 +705,8 @@ class GuideEditor {
this.canvasHistory.push(clone(this.currentStep));
const next = this.canvasFuture.pop();
this.stepMap.set(next.stepId, next);
const nextIdx = this.steps.findIndex((s) => s.stepId === next.stepId);
if (nextIdx >= 0) this.steps[nextIdx] = next;
this.selectedStepId = next.stepId;
await this.flushStep(next);
this.renderAll();
@@ -695,12 +717,18 @@ class GuideEditor {
this.pendingSave = false;
const saved = await api.step.save({ guideId: this.guideId, step });
this.stepMap.set(saved.stepId, saved);
// Keep the steps array in sync — it holds the objects the list renders.
const idx = this.steps.findIndex((s) => s.stepId === saved.stepId);
if (idx >= 0) this.steps[idx] = saved;
if (this.selectedStepId === saved.stepId) {
this.stepMap.set(saved.stepId, saved);
this.renderStepList();
this.syncStepFields();
this.canvas.setAnnotations(saved.annotations || []);
this.renderAnnotationPanel();
// Rebuilding the annotation editor while the user is typing in one of
// its inputs would steal focus, so skip it in that case.
if (!this.dom.annotationEditor.contains(document.activeElement)) {
this.renderAnnotationPanel();
}
this.emitMeta();
}
return saved;
@@ -1140,9 +1168,10 @@ class GuideEditor {
return;
}
if (e.key === 'Escape' && !isEditableTarget(e.target)) {
if (this.selectedAnnotationId && this.canvas.deleteSelected()) {
// Escape deselects; Delete is the destructive key.
if (this.selectedAnnotationId) {
e.preventDefault();
this.saveStepDebounced();
this.canvas.select(null);
return;
}
}
@@ -1206,3 +1235,4 @@ function loadImage(src) {
}
window.GuideEditor = GuideEditor;
})();