Open the work again / original
DEVELOPMENT JOURNAL / 19
Photoshot
CODE × UI
Building a free Photoshop alternative

Open the work again

This Photoshot journal examines how editing remains editable. Using the same AI portrait as Part 18, we changed a layer name and opacity, then separately checked reopening a project file and restoring the workspace in the same browser.

PHOTOSHOT DEVELOPMENT JOURNAL / 19

AI-generated study photograph. Editor captures and outputs show Photoshot in use.

PhotoshotDEVELOPMENT JOURNAL / 19FOUNDATION

From the source.

An exported image and a project make different promises

PNG delivers the visible composition as one image. Editing layer names, opacity or masks later requires document structure. In the production editor, saveProject serializes the current document as a downloaded JSON .layerstudio file.

It contains the current document, not every open tab or the complete undo history. PSD interchange uses a separate path. Preservation in .layerstudio does not imply that every property survives PSD interchange identically.

Auto-save retains the workspace and one predecessor

SavedWorkspace in workspace-store.ts includes activeId, document tabs and savedAt. It writes workspace to the IndexedDB documents store, first copying an existing value to workspace-recovery. That is one previous workspace, not permanent version history.

A Promise queue serializes writes. Loading validates the version, tab array, a 20-tab limit and the active tab, then validates every document. Named history snapshots, if present, are limited to five per document and checked for valid names, timestamps and documents.

app/studio/workspace-store.ts · SHA-256 8617c3f508c4 · L3–6Photoshot implementation ↗
export type SmartParent={tabId:string;linkedId:string;sourceKey?:string;sourceHash?:string};
export type HistorySnapshot={id:string;name:string;createdAt:number;doc:StudioDoc};
export type SavedTab={id:string;doc:StudioDoc;smartParent?:SmartParent;artboardParent?:ArtboardParent;snapshots?:HistorySnapshot[]};
export type SavedWorkspace={version:1;activeId:string;tabs:SavedTab[];savedAt:number};
PhotoshotDEVELOPMENT JOURNAL / 19IMPLEMENTATION

Inside the
calculation.

Restoring work does not restore the entire undo stack

editor.tsx tries the recovery copy if reading the normal workspace throws. When rebuilding sessions it initializes undo and redo as empty arrays. Recovering the current document differs from replaying every earlier edit.

The UI distinguishes Saving from Saved on this device and advises saving a project file when auto-save fails. Auto-save is local browser data. A downloaded project is needed to move the work elsewhere or retain it after browser data is removed.

The layer name and 87% opacity returned

On October 22, 2026, we reused Part 18’s 1536×1024 portrait, renamed its layer Journal 19 recovery check and set opacity to 87%. The saved file contained that name and opacity 0.87. File → Open Project created another tab with the same name and 87% opacity, while Undo was disabled.

After Saved on this device appeared, a separate editor window restored five document tabs, the active document and the same layer properties. Undo was again disabled. We closed that verification window afterward. These screenshots demonstrate restored document state, not a photographic color adjustment.

PHOTOSHOT / SOURCE NOTES19

Read the code.

app/studio/workspace-store.ts · SHA-256 8617c3f508c4 · L9–10Photoshot implementation ↗
export function saveWorkspace(value:SavedWorkspace){const snapshot=value;const job=queue.catch(()=>{}).then(async()=>{const db=await database();try{await new Promise<void>((resolve,reject)=>{const t=db.transaction('documents','readwrite'),s=t.objectStore('documents'),r=s.get('workspace');r.onsuccess=()=>{try{if(r.result)s.put(r.result,'workspace-recovery');s.put(snapshot,'workspace');const current=snapshot.tabs.find(t=>t.id===snapshot.activeId);if(current)s.put(current.doc,'current');}catch(error){try{t.abort();}catch{/* Already aborted. */}reject(error);}};t.oncomplete=()=>resolve();t.onabort=()=>reject(t.error??Error('Saving was cancelled.'));t.onerror=()=>reject(t.error);});}finally{db.close();}});queue=job;return job;}
export async function loadWorkspace(recovery=false):Promise<SavedWorkspace|null>{const db=await database();try{const v=await new Promise<SavedWorkspace|undefined>((resolve,reject)=>{const r=db.transaction('documents').objectStore('documents').get(recovery?'workspace-recovery':'workspace');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});if(!v)return null;if(v.version!==1||!Array.isArray(v.tabs)||v.tabs.length>20||!v.tabs.some(t=>t.id===v.activeId))throw Error('Invalid recovery workspace.');for(const t of v.tabs){validateDoc(t.doc);if(t.snapshots){if(!Array.isArray(t.snapshots)||t.snapshots.length>5)throw Error('Invalid history snapshots.');for(const s of t.snapshots){if(typeof s.id!=='string'||typeof s.name!=='string'||s.name.length>80||!Number.isFinite(s.createdAt))throw Error('Invalid history snapshot.');validateDoc(s.doc);}}}return v;}finally{db.close();}}
app/studio/editor.tsx · SHA-256 8cebf076ec90 · L248–248Photoshot implementation ↗
  useEffect(()=>{let live=true;(async()=>{let saved:SavedWorkspace|null=null;try{saved=await loadWorkspace();}catch{saved=await loadWorkspace(true);}if(!live)return;if(saved){sessions.current=new Map(saved.tabs.map(t=>[t.id,{doc:t.doc,undo:[],redo:[],selected:t.doc.layers.at(-1)?.id??null,zoom:.65,smartParent:t.smartParent,artboardParent:t.artboardParent,snapshots:t.snapshots}]));const current=sessions.current.get(saved.activeId)!;tabIdRef.current=saved.activeId;setTabId(saved.activeId);setTabs(saved.tabs.map(t=>({id:t.id,name:t.doc.name})));setDoc(current.doc);docRef.current=current.doc;setSelected(current.selected);}else{const d=await localLoad();if(live&&d){setDoc(d);docRef.current=d;setSelected(d.layers.at(-1)?.id??null);}}if(live){setReady(true);setTimeout(()=>fit(),30);}})().catch(()=>{setReady(true);notify('Recovery is unavailable. Save a project file.');});return()=>{live=false;aiAbort.current?.abort();};},[]);
app/studio/editor.tsx · SHA-256 8cebf076ec90 · L268–269Photoshot implementation ↗
  function saveProject(){download(new Blob([JSON.stringify(docRef.current)],{type:'application/json'}),`${docRef.current.name}.layerstudio`);notify('Project saved.');}
  async function openProject(file?:File){if(!file)return;try{if(file.size>100*1024*1024)throw Error('Project files must be 100 MB or smaller.');const next=validateDoc(JSON.parse(await file.text()));await Promise.all(next.layers.filter(l=>l.src).map(l=>loadImage(l.src!)));openDocument(next);}catch(e){notify((e as Error).message);}}
PhotoshotDEVELOPMENT JOURNAL / 19IN PRACTICE

In the editor.

Open the work again / workspace
Actual Photoshot workspace. Settings and the image are shown together.

Keep artboards and historical evidence in scope

Multiple document tabs differ from multiple artboards inside one document. The model includes artboards and artboardWorkspace; the artboard UI creates, moves and resizes regions within the document. What a project can restore depends on its document data and serialization path.

The stored workspace-structure-ui report records passing artboard grouping, moving contents, duplicate Undo/Redo and recovery checks. Its file date is October 12, 2026; it was not rerun today. Today’s direct checks covered project reopening and auto-recovery in another window, not every storage-quota failure, forced shutdown moment or cross-browser transfer. Part 20 examines PSD and PSB preservation separately.

PhotoshotDEVELOPMENT JOURNAL / 19BEFORE / AFTER

Original and restored work.

Open the work again / original
01 / AI-generated demonstration original
Open the work again / result
02 / Project reopened · layer name and 87% opacity retained

AI-generated study photograph. Editor captures and outputs show Photoshot in use.