SOURCE / PINNED RELEASE
Made of little things.
Body Journal
- Release
- e8049f8da4e2…
- Author-recorded commit
- 61c04bf0e8aa…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6c4554844c7
Archive hash verified: 4369d778da046a8b…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { storage } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import { DRAFT_KEY } from './case-record.js';
import { parseSession, type SessionRecord } from './session-record.js';
import { SessionStore } from './session-store.js';
import { escape as esc } from './case.js';
export class DraftWorkspace {
private opened = false;
private supported = runtimeHasDomain('storage');
private store: SessionStore<SessionRecord>;
constructor(private ui: { modal: (content: string) => void; snapshot: () => string; restore: (record: SessionRecord) => void; reset: () => void }) {
this.store = new SessionStore({ read: () => storage.getItem(DRAFT_KEY), write: raw => storage.setItem(DRAFT_KEY, raw), remove: () => storage.removeItem(DRAFT_KEY), parse: parseSession, snapshot: ui.snapshot, restore: ui.restore, reset: ui.reset, changed: () => this.refresh() });
document.addEventListener('click', this.onClick);
document.querySelector('#modal')?.addEventListener('close', () => { this.opened = false; });
}
async init(): Promise<void> { if (this.supported) await this.store.init(); this.refresh(); }
change(): void { if (this.supported) this.store.change(); }
open(): void { this.opened = true; this.render(); }
private refresh(): void {
const status = document.querySelector<HTMLElement>('#sessionStatus');
if (status) {
status.textContent = !this.supported ? 'Session only' : this.store.blocked ? 'Not saved · retry' : this.store.loading ? 'Restoring…' : this.store.clearing ? 'Clearing…' : this.store.dirty || this.store.message === 'Saving session…' ? 'Saving…' : this.store.message === 'Autosave on' || this.store.message.startsWith('Session cleared') ? 'Autosave on' : 'Session saved';
status.title = this.supported ? this.store.message : 'This host has no storage. Changes last for this session only.';
}
if (this.opened) this.render();
}
private render(): void {
const busy = this.store.loading || this.store.clearing;
this.ui.modal(`<span class="section-caption">YOUR WORK, KEPT TOGETHER</span><h2>Your current session.</h2><p class="muted">Your forms, groups, body markings, poses, resized photos and completed report are saved automatically and restored when you return.</p><div class="quiet-note"><span>◇</span><p>Saved in this host’s browser storage, without an encryption guarantee. Another device, account or app build may have a separate storage scope. AI keys are managed in Your connections.</p></div><div class="draft-status" role="${this.store.blocked ? 'alert' : 'status'}">${esc(this.supported ? this.store.message : 'Storage is unavailable. Your work lasts for this session only.')}</div><div class="dialog-actions"><button class="primary-button" data-draft="save" ${!this.supported || busy ? 'disabled' : ''}>Save now</button>${this.store.blocked ? '<button class="outline-button" data-draft="restore">Restore saved session</button>' : ''}</div><button class="text-button" data-draft="confirm-forget" ${!this.supported || busy ? 'disabled' : ''}>Clear current session…</button><p class="field-hint">Wait for “Session saved” before closing. Clearing removes this session’s health details, photos and report; your AI connections are kept.</p>`);
}
private onClick = (event: Event): void => {
const button = (event.target as Element).closest<HTMLElement>('[data-draft]');
if (!button || !this.opened || !this.supported || this.store.loading || this.store.clearing) return;
if (button.dataset.draft === 'save') void this.store.flush(true);
if (button.dataset.draft === 'restore') void this.store.init();
if (button.dataset.draft === 'confirm-forget') this.ui.modal('<h2>Clear this session?</h2><p>This removes your entered health details, body markings, photos and AI report from this session and its saved copy. Your AI connections are kept.</p><button class="primary-button" data-draft="forget">Clear session and start fresh</button>');
if (button.dataset.draft === 'forget') void this.store.clear();
};
dispose(): void { this.store.stop(); document.removeEventListener('click', this.onClick); }
}
