drafts: persistent scratchpad in Compose sidebar (v0.8.0) (#4)
* docs: drafts feature design
Approved design for a Compose-tab drafts sidebar: SQLite-backed
drafts + draft_media tables, /api/drafts CRUD + translate, publish
consumes via optional draft_id on /api/post, free-form tags with
AND filter, manual save with localStorage recovery autosave.
Also adds .superpowers/ to .gitignore for visual-brainstorm scratch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: drafts implementation plan
21-task TDD plan covering the store layer (drafts + draft_media),
media-pipeline sha256 dedup, /api/drafts CRUD + translate, the
/api/post draft_id consume hook, and the Compose-tab sidebar UI with
tags, filters, recovery banner, and mobile toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* drafts: add drafts and draft_media schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: group new indexes with existing ones for consistency
* drafts: Draft model + tag/title normalization helpers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: CreateDraft and GetDraft
* drafts: UpdateDraft and DeleteDraft with media cascade
* drafts: ListDraftsFiltered with query and tag-AND filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* media: dedup uploads via sha256 lookup across posts and drafts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: GET/POST /api/drafts handlers
Wire six draft routes in Routes(), implement handleListDrafts and
handleCreateDraft (with multipart+media-pipeline buildDraftFromRequest),
and stub the remaining four handlers as 501. Tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: GET/PUT/DELETE /api/drafts/{id}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: POST /api/drafts/{id}/translate via DeepL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: /api/post consumes draft via draft_id on success
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: state.activeDraftId, dirty flag, localStorage recovery
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* web: drafts sidebar + toolbar markup and CSS
Wraps <section id="compose"> in a .compose-layout flex container with a
.drafts-sidebar <aside> and .compose-main div; prepends compose-toolbar,
recovery-banner, and tags-input-row inside .compose-main. Adds all
supporting CSS including a new .primary/.primary.sm button variant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: sidebar list rendering and load-into-Compose
Wire the drafts sidebar to the backend: installDraftsSidebar() fetches and
renders the draft list with search/tag filters and click-to-load; loadDraft()
generalised to accept both the legacy {text,lang} translate flow and the full
draft spec shape; buildSpec() emits dual image entries (ref for new uploads,
blossom_url for already-uploaded) and includes draft_id when active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: save/new actions with dirty tracking and Ctrl+S
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: delete-active button with confirm
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: tag chip input in editor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: translate-to-new-draft action
Add populateTranslateMenu() to drafts.js that wires the #draft-translate
dropdown; POSTs to /api/drafts/{id}/translate and loads the returned draft
into Compose. Called from installDraftsSidebar() and again explicitly in
main.js init() after /api/config resolves (idempotent).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: recovery banner on boot when localStorage has unsaved work
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: publish consumes active draft on success
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* drafts: mobile sidebar toggle
* docs: document /api/drafts endpoints
- list/get/create/update/delete + translate
- multipart shape with ref/blossom_url image entries
- /api/post draft_id consume hook
- sha256-based Blossom dedup across posts and drafts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* drafts: fix recovery banner + langs round-trip from saved spec
Two issues surfaced during iPad smoke testing:
1. .recovery-banner { display: flex } in CSS overrode the [hidden] HTML
attribute (same specificity, class rule loaded later) — so the banner
was always visible. Added .recovery-banner[hidden] { display: none }.
2. buildSpec serializes bluesky.langs as an array, but the editor expects
it as a comma-separated string. Round-tripping a saved draft through
loadDraft → state.ov → buildSpec broke at ovFor's .split() call.
Convert back to string in loadDraft after Object.assign, and make
ovFor defensive (accept either type) so a stale stored spec doesn't
crash.
Also switched the recovery banner's Restore/Discard wiring from .onclick=
to addEventListener("click", ...) for consistency with the rest of the
file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* drafts: move save/tags into sidebar, fix mobile overlay
iPad smoke test feedback:
- Move Save / Translate / Delete toolbar and Draft tags input from the
Compose form into the sidebar (just under the "Drafts" title), so all
draft-related controls live together. The 📋 Drafts mobile toggle stays
in the Compose pane so the sidebar can be opened from mobile.
- Rename "Tags" label to "Draft tags" to disambiguate from per-post
hashtags.
- On mobile, the sidebar overlay now starts 75px below the viewport top
so it sits below the sticky topbar (was hidden behind it). Bumped
z-index to 15 (still below the topbar's 20).
- Add a ✕ close button to the sidebar header (mobile-only) and
auto-close the overlay when a draft row is tapped — otherwise there
was no way back to the composer on mobile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>