Reader Mode reuses renderBookDesignedPageToCanvas, which the J4.c.2
commit already updated to render threaded chapter text. So J4.c.3
is implicit — no code change needed beyond what J4.c.2 shipped.
Adds an 'Implementation status' table to the spec so anyone reading
the doc later can map phase labels to actual commits + see what
still pends.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Drafts the next architectural arc for the Book Designer around two
ideas:
- Events are linked files. Generalizes the pin-vs-live toggle
shipped for chapters to every external reference. A new
book.links[] collection becomes the project's 'Links panel'
(chapters, masters, pages, illustration sets, future asset kinds).
- Threaded text frames (InDesign-style). First-class text-frame
layers on designed pages, threaded port-to-port; a thread
declares a content source (a linked chapter event) and the
pagination engine pours text through the frame chain.
The current 'reflow entries in book.pages' model collapses into
links[] + threads[] + auto-generated designed pages — a one-time
migration on load. Pagination engine, PDF export, Reader Mode,
Linked Designs resolver, and NIP-23 chapter events all survive.
Phasing J1..J6 captured in the spec + TODO. No code yet — this is
just the design doc and arc placement.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Final spec polish after the markdown / NIP-23 revision.
Schema consistency:
- Manifest entries now carry a stable opaque `id` (generated at
insert time). Used for anchoring designed pages around reflow
segments — anchors reference `entryId` instead of fragile array
positions or human-readable strings.
- Unified `masterRef` across designed and reflow entries (was
`inheritsMasterRef` on designed, `masterRef` on reflow). Both
default to `spec.defaultMasterRef` when null.
- Promoted `AnchorBlock`, `GatingBlock`, `TextStylesBlock`,
`TypographyObject` to named definitions in the data-model block
so the schema is documented once and referenced from prose.
- Page event `d`-tag pinned to a stable opaque id, not derived from
the page title (titles change; d-tags shouldn't).
- Added uniqueness rules: a chapter or page event can appear in at
most one manifest entry per book; every entry has a unique `id`.
New sections:
- **Chapter event example** (kind-30023 NIP-23 article) so the
full event shape is documented alongside page / master / cover.
- **Schema + compatibility** — bump to SCHEMA_VERSION 6,
additive on the project payload, with the documented fallback
for older clients.
- **Privacy** — confirms PRIVATE_PUBLISHING works across all
constituent events of a book.
- **Compatibility notes** — Linked Designs inside pages works
via existing async resolver (worth a Phase B smoke test); all
existing layer types render on book targets.
- **MVP launch path** — two reasonable launch targets:
MVP-A (designed-only books, Phases A+B+D1+D2) and MVP-B
(books with prose, MVP-A + C+C.5+E). Phases F/G/H/I are strict
enhancements that don't block either MVP.
Tightened semantics:
- **Token substitution scope** explicitly limited to (a) text
layers and (b) reflow-entry display fields. Markdown bodies of
chapter events stay portable to other NIP-23 clients — no
book-specific tokens leak in.
- PDF section: removed outdated "may flatten text to vector
paths" notes — the font allowlist + per-export warning is now
the committed approach. Reflowed prose and designed-page text
emit as real PDF text uniformly.
- PDF outline / bookmarks moved out of "limitations" — they ship
in Phase I built from `chapterTitle` and `{toc}` targets.
Cleaned Open Questions: removed answered items (TOC token is now
Phase I, Linked Designs interaction moved to Compatibility notes).
Added missing acceptance criteria for Phase F (templates +
deck-to-book import) and Phase I (CodeMirror + extended subset +
auto-TOC + cross-references).
Third revision pass adds reflowable prose as a first-class part of
the Book Designer, without giving up the fixed-layout design pages.
Architecture:
- Prose chapters live as NIP-23 long-form articles (kind-30023,
markdown body). One source, two presentations: same article event
is readable standalone on Habla.news AND laid out as designed
pages inside a book on Artstr Studio.
- Book manifest's `pages` becomes a discriminated union:
`{ type: 'design', ref }` for fixed-layout pages, and
`{ type: 'reflow', textRef, masterRef, … }` for chapter segments
that paginate dynamically.
- New `text-frame` layer kind, lives on masters. The pagination
engine pours markdown tokens into copies of the master until the
chapter is exhausted.
- New `spec.textStyles` block — a book-level stylesheet mapping
each markdown element (h1-h6 / body / blockquote / code / list /
link / hr) to typography (font, size, weight, leading, alignment,
margins, plus keepWithNext / keepTogether flow controls).
- Anchored designed pages: designed entries on the manifest use
`anchor.mode: 'before-chapter' | 'after-chapter'` so their
semantic position survives reflow-driven page-count changes.
Tradeoff mitigations baked in:
- Pagination is fiddly → tight v1 markdown subset documented;
pure-function engine for testability; "Preview pagination" before
publish; overflow toasts for too-long words / wide code blocks.
- Text drift breaks pagination → chapters pin to a specific event
id by default (safe for print); opt-in "Live" mode for
always-latest with re-paginate notices showing page-count delta;
anchor-based positioning for designed pages.
- PDF text fidelity → reflowed prose exports as real PDF text via
pdf-lib font subsetting; font allowlist with vector-path
fallback + per-export warning.
- Editor scope → plain textarea + live preview for v1; CodeMirror
upgrade deferred to Phase I; WYSIWYG never in scope.
New tokens: `{chapter-title}`, `{chapter-number}` resolve per
chapter context (useful for running headers on masters).
Phases updated:
- Phase C grows `text-frame` layer kind (structural).
- New Phase C.5 — vendor `marked`, implement pagination engine,
markdown editor pane, anchoring, pin/live tracking. Slots in
before PDF export because PDF needs it.
- Phase D1 expanded to include chapter publish as kind-30023.
- Phase E updated for real PDF text on reflowed prose.
- New Phase I — extended markdown subset (tables, footnotes,
auto-TOC via `{toc}`, cross-references) + CodeMirror editor.
Pages overview tile UX expanded: designed-page tiles show a
rendered thumbnail; reflow tiles show a stack-of-pages icon with
chapter title, master name, paginated page count, and a tiny
first-page preview.
Acceptance criteria for Phases C, C.5, D1, E updated to match.
TODO.md summary rewritten to reflect the new shape.
Apply twelve Nostr-driven revisions to the Book Designer spec:
1. **Sidecar architecture.** Pages, masters, and the cover spread
are now separate addressable kind-30078 events
(`casewrap-page`, role-discriminated). The book is a thin
`casewrap-book` manifest that references them by
`{ kind, pubkey, dTag }` — the same shape Linked Designs
already speaks. Edit one page → that page event replaces; the
manifest only changes when order / list / spec moves.
2. **Masters as Nostr objects.** Each master is its own event; one
"Body pages" master can be referenced by many books and forked
by other authors. Default master lives on the manifest spec.
3. **Cover as a sidecar event** with `role: 'cover-spread'` so it
can iterate independently and be republished as standalone
marketing art.
4. **Linked Designs is the precedent.** The resolver IIFE,
progressive fetch, localStorage disk cache, batched relay
queries, upstream-update notifications, and attribution chain
all transfer verbatim. Call this out explicitly so we don't
reinvent the wheel.
5. **Addressable refs everywhere.** Manifest entries use the
existing `{ kind, pubkey, dTag }` form (convertible to naddr at
the UI layer), not local UUIDs.
6. **Premium gating + tips.** New "Premium, tipping, gating"
section covers whole-book paywall (gate the manifest),
per-chapter paywall (gate individual page events via
PREMIUM_DESIGNS), and NIP-57 zaps at any granularity.
7. **Reader Mode** added as Phase G — the Slide Decks' Presenter
Mode counterpart, with progressive page fetch, TOC sidebar,
page-flip nav, and inline zap-to-unlock.
8. **Embed integration.** EXTERNAL_EMBED learns `?book=<naddr>`
for a flip-book iframe player; same transport + CSP rules.
9. **Per-page reactions / comments** work for free because each
page is its own addressable event (NIP-25 + kind-1). One-liner
in product goals + Reader Mode acceptance.
10. **`{page-count}` semantics pinned.** Defaults to *numbered*
pages; new `{physical-page-count}` exposes the raw total.
Plus `{book-author}` for the manifest pubkey.
11. **NIP-23 distinction.** Explicit non-goal — Book mode is
fixed-layout design, not reflowable prose; for prose use
NIP-23 long-form articles directly.
12. **Manifest schema.** Pages now carry
`{ ref, label?, inheritsMasterRef, skipNumber, gating? }`;
page-side fields that vary per-book (master, skipNumber,
label) moved off the page event onto the manifest entry.
Phase D split into D1 (publish individual page / master / cover
events) and D2 (publish the manifest); new Phase G (Reader Mode +
embed); new Phase H (premium gating). Acceptance summary,
cross-surface reactivity, and risks updated to match.
TODO.md's Book Designer summary rewritten to reflect the
sidecar plan.
New BOOK_DESIGNER_FEATURE.md drafts a top-level templateMode: 'book'
that orchestrates the existing cover designer, deck overview, and
custom-art canvas around a shared document spec (trim size, page
count, paper thickness, bleed). Spine width derives from page count
× paper thickness; the cover view goes read-only on the spine
column when in derived mode. Pages reuse the deck-sorter UI for
reordering; the per-page editor is the custom-art canvas bound to
a book-page:<id> target with prev / next page nav. Masters render
underneath each inheriting page and support {page-number} /
{page-count} / {book-title} token substitution.
PDF export ships in its own phase (E), backed by a vendored
pdf-lib (~80 KB). Single-file (cover + interior) and split (cover
PDF + interior PDF) output modes; bleed + crop-mark options;
v1 flattens text to vector for font-fidelity, with vector PDF
text as a follow-up.
Phased: A (spec + skeleton) → B (pages + editor) → C (masters) →
D (publish) → E (PDF export) → F (templates + deck-to-book
import). Non-goals: reflowable text, footnotes, automatic TOC,
CMYK / ICC, imposition, mixed page sizes in one book.
Linked from README's "Drafted-but-unbuilt" list and TODO.md.
PEN_TOOL_FEATURE.md gets a 2026-06-02 status block documenting
everything that landed on top of Phase D — Direct Selection split,
auto-add/auto-delete, click-vs-drag convert, Alt-click curved ↔
pointed, multi-anchor select / marquee / arrow nudge, drag-segment
bow, continue-endpoint, spacebar reposition, Alt mid-draw, handle
break, Cmd+J / Cmd+Alt+J, Path Actions side panel — plus the
adjacent design-tool features that shipped in the same arc
(Eyedropper, Align & distribute, Free Transform rotation,
Pathfinder, marquee box-select, dive-into-edit on dbl-click).
TODO.md gets a "Design-tool follow-ups" section listing the gaps:
Smart Guides, Gradients + shadows, Scissors / Outline text /
Command palette, Flip H / V (needs renderer scaleX/scaleY),
Pathfinder rotation support, pixel-art frame timeline.
Finished the deferred Phase D pieces from PEN_TOOL_FEATURE.md.
New affordances on the editable pen layer:
- Click a segment between anchors → inserts a new anchor at the
click point via de Casteljau cubic split. Curve shape is
preserved exactly: a's hOut, b's hIn, and the new mid-anchor's
handles all derive from the split. Lines (no handles either
side) get a plain midpoint anchor with no handles. The new
anchor is selected and immediately drag-able.
- Right-click an anchor → delete it. Suppresses the browser
context menu on the pen overlay always.
- Backspace / Delete with an anchor selected → delete it.
Layer-delete still wins when no anchor is selected. The pen
tool's selected-anchor sentinel `_penSelectedAnchor` is reset
on tool change.
- Alt-click an anchor → toggle between `kind: 'corner'` (handles
move independently) and `kind: 'smooth'` (handles stay mirrored
across the anchor). Promoting to smooth re-derives the missing
handle (or re-mirrors both if independent) so the curve
actually becomes continuous. New nodes from pen click-drag
default to smooth; new nodes from a single click are corners.
- Drag an anchor or handle past the original 0–100 viewBox →
the layer's bounding rect auto-refits. New `refitPenLayerIfNeeded`
computes the bbox over all nodes + handles, translates
layer.x/y by the bbox offset (scaled to current w/h), updates
layer.w/h, then remaps every node into the new 0–100 space.
Path stays visually anchored to the canvas — no more clipping
at the layer edge.
Anchor glyphs now distinguish kind: smooth = circle (existing
look), corner = square. The selected anchor renders with an amber
stroke (#f97316) so the Backspace-to-delete target is unmistakable.
Spec updated to mark Phase D complete and the deferred items
shipped. Rotated paths are still not node-editable in v1 (the
deferral remains).
Removed three superseded spec files now that the work they
described is either shipped (under newer specs) or formally
replaced:
docs/ZAP_GATED_TEMPLATES_FEATURE.md
docs/ZAPS_NWC_FEATURE.md
docs/ZAP_GATED_PREMIUM_FEATURE.md
The history is preserved in git. The current canonical premium
plan is docs/PREMIUM_DESIGNS_FEATURE.md (which absorbed the
zap-gated+NWC infrastructure plus added the soft-gate KDF +
encrypted envelopes + purchase vault).
README's docs paragraph now lists every active spec by status —
shipped / partial / drafted — and adds the transparency-iframe
baseline stub that landed alongside the embed feature.
The embed feature is essentially complete — phases 0-6 are live
on main, Phase 7 hardening shipped alongside Phase 1. Updated
the spec to reflect that:
- Status block flipped from "Nothing in this spec is implemented
yet" to "Shipped (2026-06-01)" with date.
- Every shipped phase gets a ✅ marker on its heading.
- Phase 7 (Hardening + headers) body rewritten to record what
actually shipped (CSP frame-ancestors, SPA rewrites, error
handling, sanitization reuse) plus what's still optional
(?debug=1 surface, hard relay-count cap).
- Phase 8 (Polish) deliberately dropped — the previously-drafted
Credits overlay, postMessage handshake, and explicit
light/dark/auto theming weren't needed for the use cases the
feature is actually solving. Replaced with a clear "dropped"
note listing each item and why.
- Phase 9 (Future) reframed as the natural follow-up arcs if we
revisit (oEmbed endpoint, render-to-image, OG images,
lightweight embed.html bundle).
The Acceptance Criteria for Embed v1 stays valid as-is — every
bullet is true.
Updated rewrite of the earlier embed plan covering every Artstr
feature shipped since the original sketch:
- Linked sub-design resolution inside embeds (reuses
linkedDesigns.prefetchBatch + resolveLinked + disk cache, full
cycle/depth/byte safety).
- Premium boundary: watermarked preview + "Open in Artstr to
unlock" CTA; no in-iframe zap.
- Private boundary: hard refuse with 🔒 placeholder; no decrypt
attempts.
- Pixel-art animation playback (auto-play, fps override,
prefers-reduced-motion respected).
- Deck slide picker — `?slide=N` deep-links; linked-deck layers
carrying design.slideIndex project to a single-slide payload.
- Credits overlay (opt-in via `?credits=1`), mirroring the
in-app Credits panel.
- PostMessage handshake for parent-page integration (deck
control, fullscreen request, ready/error events).
- Layer-group handling falls out naturally — groups are flat
layers with groupId; the existing renderers already paint them.
- Open Graph metadata on the embed route URL itself.
- Per-event + per-tree byte caps inherited from linked-designs.
- Phase plan + acceptance criteria + risks updated.
No code; this is the planning artifact. Implementation phases
0-9 are scoped inside the doc.
Second-pass review surfaced eight issues worth pinning down before
we start coding. Each addressed in-place rather than as a TODO so
the implementation has a clear target:
1. §5.1 — Persisted vs in-memory fields. The host's serialized
`content` was implicitly inflating with `fallbackPreview`,
`lastResolvedAt`, `lastResolvedSize` from the initial schema.
Now explicit: only `kind` / `ref` / `mode` / `pinnedEventId` /
`pinnedSnapshot` persist; the rest are runtime-only and stripped
at publish time. A linked layer footprint is ~110 bytes serialized.
2. §6.1 — Pinned + source deletion. New section covering the case
where the source author kind-5-deletes a pinned event. Adds a
`pinnedSnapshot` field that the renderer prefers-live-falls-back-
to-snapshot, plus the existing `fallbackPreview` for visual-only
fallback. Default = pin without snapshot, user opts in for
permanence.
3. §8.1 — Sync vs async render decision. The renderer is synchronous
today; this section explicitly picks "sync placeholder + async
upgrade" over "block render" or "pre-fetch all". DOM tagging +
abort-on-detach behaviour documented so the implementation
doesn't end up re-deciding mid-build.
4. §8.2 — Autosave + cold start. localStorage restore loses in-memory
resolver state, so on `restoreAutosave` we kick off the resolver
for every linked layer in the background.
5. §10.3 — Embedded → linked attribution: corrected an inaccurate
claim. Embedded layers don't carry source-author metadata, so
"Publish + link" credits the CURRENT user, not whoever originally
authored the embedded content. Action copy should warn explicitly.
6. §12.1 — Premium-link access check: clarified that the gate is the
host viewer's purchase-vault entry (not technical decryption
capability, since the soft-gate KDF is deterministic).
7. §13 — Added a per-tree cap (1 MB total resolved payload per host
render) on top of the per-event cap. Depth-3 fan-out could
theoretically multiply fetches; the tree cap stops a coordinated
DOS at the host level.
8. §11.5 — Picking initial layer dimensions for a new linked layer
(sync resolver lookup with a 1.5 s timeout, falls back to 16:9
default). Plus a link-creation modal preview so the user can
confirm what they're linking before commit.
Also tightened the Phase 1 acceptance criteria around serialized
size: explicit budgets for 10-link host events and verification that
in-memory fields are stripped at publish.
Spec for the next big arc: reference-based cross-design imports.
Adds a `kind: 'linked'` flavor of the existing design layer that
stores only a NIP-01 addressable-event coordinate
(30078:<pubkey>:<dTag>) and resolves to the latest source on
render. Sits alongside the existing embedded import path; user
picks at import time.
Key decisions baked in (per scoping conversation):
- Versioning: always-latest by default, with an opt-in "Pin to
this version" toggle exposed per linked layer.
- Nesting: up to 3 levels deep with cycle detection + visible
depth-exceeded placeholders.
- Encrypted: Phase 1 ships public-only; Phase 2 adds premium
(renders if viewer's unlocked) and private (share-list aware).
- Attribution chain is first-class — host publish emits one
['p', '<contributor pubkey>', '<relay-hint>', 'design-contributor']
tag per distinct linked-tree creator, plus an in-app Credits
panel surfacing co-creators. Lets larger collaborative projects
carry honest attribution natively.
Architectural shape:
- Resolver IIFE (linkedDesigns) with session cache, multi-relay
union (mirrors _vaultRelayUnion), and a manual Refresh button
to bypass cache.
- renderDesignObjectIntoElement gets a linked branch that calls
the resolver before the existing render path.
- Layer-options panel exposes pin / refresh / unlink / replace.
- "Publish + link" action converts an embedded layer to a freshly-
published source + linked reference (Phase 3 polish).
- Convert-back-to-embed action snapshots the current resolution.
Phased implementation:
- Phase 1: Public-only linking, schema, resolver, rendering, UI.
- Phase 2: Premium + private link resolution with access control.
- Phase 3: Disk cache, attribution `p`-tag emission, Credits panel,
Publish + link workflow.
Acceptance criterion for Phase 1: a public cover design linked into
a public slide deck renders correctly; editing the source updates
the host's render on next refresh. 10-link host event stays under
typical relay size limits.
Outstanding open questions to revisit during implementation:
- Whether the depth limit should be 3 forever or configurable.
- Whether the 256 KB per-source payload cap is the right number.
- Whether to surface "last resolved" times in the layer name itself
or only on hover.
Spec for the next big arc: replacing the current chart-placeholder
behaviour (single image layer at PPTX_IMAGE_PLACEHOLDER_URL) with
native rendering — bars / wedges / lines as Artstr shape layers,
labels as text layers. Branch pptx-chart-spec.
Scope decisions baked in:
- Phase 1: clustered column + bar charts only. Bars as rect shape
layers, category labels as text. Plot area derived via a fixed
inset on the chart's graphicFrame bounding box (no
c:manualLayout reading until Phase 4).
- Phase 2: pie / doughnut. Slice geometry emitted as kind:'path'
with SVG arc d-attributes.
- Phase 3: line charts with optional markers.
- Phase 4: title / legend / data labels / value-axis ticks /
gridlines.
- Phase 5: stacked + percent-stacked variants.
- Deferred to placeholder: scatter, bubble, area, radar, 3D,
combo charts, trendlines, error bars, per-point overrides.
Architecture:
- New src/pptx-importer-charts.js with the per-type builders,
loaded as an addendum to pptx-importer.js (or inlined — TBD
during implementation; the dependency budget is small enough
that a single file might be cleaner).
- convertGraphicFrame in pptx-importer.js gets a "try native
before placeholder" branch: parse chart XML, dispatch on type,
fall through to the existing placeholder + warning when the
builder returns null.
- All chart data read from c:strCache / c:numCache. The embedded
ppt/embeddings/*.xlsx workbook is ignored — caches are reliable
in OOXML output and reading XLSX is its own rabbit hole.
Acceptance criteria for Phase 1: gallery_template_layout_set.pptx
slide 10 imports with 8 native bar layers (#5E735D + #B05A44
clustered across Q1-Q4) plus 4 category-label text layers,
matching what PowerPoint shows. Scatter / 3D / etc. still
placeholder cleanly without crash.
Adds docs/PPTX_IMPORT_FEATURE.md as the canonical plan for the next
big arc: a phased editable .pptx importer that lands PowerPoint decks
as editable Artstr deck JSON.
Key decisions baked in:
- Images and unsupported objects (charts / SmartArt / tables / video)
import as placeholder image layers pointing at a hardcoded URL
(postimg.cc for now). Original PowerPoint media filenames carry
over into the layer name so the user can correlate placeholders
with their source deck.
- fflate (~10 KB) vendored as src/vendor/fflate.min.js. JSZip is too
heavy for the single-file ethos.
- Importer code lives in src/pptx-importer.js as an IIFE that
exposes window.ArtstrPptxImporter. The Import PPTX button
dynamically injects both scripts on first click, so the main
index.html bundle does not grow for the 90 % of users who never
import a PPTX.
- Notes folded into Phase 2 alongside text (single extra relationship
lookup, Presenter Mode already renders slide.notes).
- No Blossom / NIP-96 prereq, no base64 bloat, no publish-size guard.
Imported decks are normal Artstr deck payloads with remote URLs.
- Import-only for this arc. PPTX export (Artstr -> .pptx) is a
separate later arc with its own complexity.
TODO.md gets a Drafted-but-unbuilt entry pointing at the new spec.
Pre-merge docs sweep before zap-gated lands on main:
- TODO.md: move Premium out of "Drafted-but-unbuilt" into "Shipped
(recent)" with a full summary of what landed (soft-gate, watermarked
previews, NWC split-zap with 30% platform fee, purchase vault with
auto-split, profile menu, login picker, Settings, mobile polish).
Leave Phase 4 E2E + a few confirm()-modal replacements as a new
"Premium-templates polish" section.
- docs/PREMIUM_DESIGNS_FEATURE.md: flip the status banner to
"Shipped on zap-gated, merged into main 2026-05-29" and map each
spec step (0-4) to where it lives in src/.
- README.md: add a Premium encrypted designs section; mention NWC,
vault, login picker, profile dropdown, and Settings modal in the
Nostr / Lightning sections; drop "NWC zaps / zap-gated templates"
from the unbuilt list in the Files section.
In live testing, the tag-only gate shipped in Phases 0–3 turned out
to be too leaky: the design payload was published in plaintext, three
separate import paths (feed-card Use/Fork, preview-pane Use/Fork,
preview-pane Save JSON) bypassed the lock, and any non-Artstr client
could ignore the tag entirely. Adam pulled back the original detailed
spec he wrote earlier — the static soft-gate + buyer purchase vault
model — which solves the protection gap WITHOUT requiring a creator-
online bot (the route I was about to build).
Key idea: the AES key derives deterministically from
HKDF-SHA256(softGatePepperForEpoch(epoch), publicDesignSalt, info =
"artstr-softgate-v1" + epoch + creatorPubkey + premiumAddress +
schemaVersion). The pepper lives in the static client (obfuscated,
not secret). The client refuses to derive the key until valid kind-
9735 receipts confirm the split zap. Same key for every buyer; no
creator-online requirement; cross-device unlock via a NIP-44 self-
encrypted purchase vault (kind-30078, d=artstr:purchase-vault:v1).
Files:
- docs/PREMIUM_DESIGNS_FEATURE.md (new, canonical) — the full
soft-gate + split-zap + purchase-vault spec. Sections 1–17 are
the original spec verbatim. Section 18 maps the shipped Phases
0–3 to the spec's implementation-plan items (10.0–10.6) so the
next contributor sees what's done. Section 19 lists the
remaining work in build order: soft-gate crypto primitive →
watermarked preview → premium publish event → premium consume
→ receipt fast-path by address → purchase vault → My Purchases
UI → hardening + copy.
- docs/ZAP_GATED_PREMIUM_FEATURE.md — banner now points at
PREMIUM_DESIGNS_FEATURE.md as the canonical going-forward doc.
The obsolete Phases 5/6 (DM-delivery key flow) I sketched
earlier today are removed; the "Content model (CORRECTED)"
section is trimmed and points at the new spec. The file is
kept for Phases 0–3 history, the NWC/tip integration notes,
and the threat model that still applies.
- docs/ZAP_GATED_TEMPLATES_FEATURE.md and ZAPS_NWC_FEATURE.md
keep their "superseded" banners pointing at
ZAP_GATED_PREMIUM_FEATURE.md (which itself now points at
PREMIUM_DESIGNS_FEATURE.md) — chain of provenance preserved.
- TODO.md: drafted-but-unbuilt entry rewritten to describe the
final shape (encrypted + vault + honest soft-gate framing) and
point at the new spec; references the older docs for the
shipped foundation.
No code changes yet — the spec adoption is the gate. Next commits
implement the soft-gate crypto primitive, the watermarked preview,
and the premium publish path.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Critical additions:
1. Relay fast-path. Zap receipts live on relays forever — querying
for matching receipts on feed load means a cache-wiped user (or
a new device) restores their unlocks for free. Also auto-detects
a half-failed unlock that eventually settled.
2. NIP-47 capability discovery via kind-13194. We subscribe to the
wallet's info event on connect and verify pay_invoice is
supported before allowing the user to proceed.
3. Wallet-budget-exhausted handling. Special-case QUOTA_EXCEEDED /
BUDGET_EXCEEDED so we say "raise your wallet's NWC budget" not
a generic failure. Also surface this guidance in the CoinOS
setup flow itself ("set a budget that comfortably covers the
unlocks you expect").
4. Preimage = proof of payment. Decouple unlock UX from kind-9735
receipt arrival. The preimage hashes to the bolt11's payment
hash; that's all we need to mark unlocked locally. Receipts
become async verification.
5. nwcLookupInvoice for double-spend protection. Before paying any
leg on retry, check whether the wallet already settled it.
Combined with PARTIAL_UNLOCK_KEY persistence (now storing bolt11
+ paymentHash, not just receipt id), this means cross-session
retries never double-charge.
6. LNURL minSendable checks. Block at publish time if the creator's
LNURL won't accept their split share (with a corrective hint
showing the minimum viable zap-min). Same defensive check on
the platform side.
7. Parallelize the dual-zap. Sign both zap requests in Promise.all,
fetch both bolt11s in parallel, then pay sequentially with
creator first. Roughly halves the user-facing latency.
8. Partial-unlock persistence + banner. If the user closes the tab
mid-unlock, on next visit a top-bar banner offers Resume or
Discard for unfinished unlocks.
Polish:
- Phase 0 bumped 2-3h → 3-5h (noble subset inlining is genuinely
more work than that estimate).
- NWC URI parser now handles multiple relay params, URL-encoded
secrets, optional lud16 param.
- "Wallet alias from get_info" instead of misleading "walletDomain".
- CoinOS deep link to coinos.io/settings/connect.
- Mobile copy-paste note.
- Three new threat-model rows: platform-pubkey exposure (intentional),
budget-exhausted-mid-flow (precise retry prompt), and forged-
receipt fast-path (signature verification).
- Two new open-questions: pay-above-min unlocks (creator-only
bonus), currency display.
- Phase 4 ship gate: "documented in commit message" since we push
straight to main, no PR workflow.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The original ZAP_GATED_TEMPLATES_FEATURE.md said "no platform fee, no
backend" — that's exactly the opposite of what we actually want to
ship. ZAPS_NWC_FEATURE.md was scoped as a general tip upgrade,
listing splits as a "future enhancement." For our purposes the two
are a single arc, with NWC being the only thing that gives the
mandatory creator/platform split a tolerable UX.
ZAP_GATED_PREMIUM_FEATURE.md is the new canonical plan:
- 5 phases (0 crypto bundle, 1 NWC, 2 platform-fee plumbing,
3 zap-gated UI, 4 E2E + edge cases). ~16-23h total.
- 30% platform fee, single-constant for easy adjustment later.
- PLATFORM_NPUB hard-coded; lud16 resolved dynamically from the
npub's kind-0 each session (so the lightning address can change
later without a code update).
- CoinOS recommended via a guided "Set up with CoinOS" path in
the wallet-connection modal; generic "paste any NIP-47 URI"
fallback for Alby Hub, Mutiny, Phoenix, Cashu.me, etc.
- Idempotent retry of partial unlocks via localStorage receipt
tracking — creator-leg pays first so a half-failed unlock at
least pays the creator.
- Explicit threat model + non-goals (no creator-only fallback,
no refunds, no bolt11-QR fallback path).
The two old specs are marked superseded with a banner at the top so
historical context is preserved without future contributors getting
crossed wires. TODO.md drops them from the drafted-but-unbuilt list
and points to the merged doc; it also fixes the stale Private
Publishing entry — Phases 1 & 2 already shipped in 3e6e89e, only
Phases 3 & 4 remain.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
PIXEL_ART_FEATURE.md: rewrite Implementation status to reflect that
Phases A–D all shipped (embedded layer, standalone design, raster
export + editor extras, animation, image → pixelize), with the
backlog (WebP/APNG, Blossom offload, animated pixel layers driving
Stack actions) called out as deferred.
TODO.md: drop stale items now that they're resolved —
- Pixel Art (moved to a "Shipped (recent)" section pointing at the
spec doc).
- Pixel editor layout expansion (subsumed by the collapsible
sidebar — both the pixel editor and vector canvas can now use
the full viewport width).
- Print / Save event ID wrapping (the `<dd>` already uses
`overflow-wrap: anywhere; word-break: break-word`).
- TMDB auto-fill cleanup (the doc was already deleted).
The Color picker fixes item is kept but narrowed to the vector
tools — the pixel editor's in-app HSV picker shipped in Phase A.
README.md: add Pixel Art everywhere it's relevant —
- One-liner mention in the intro paragraph.
- New "Pixel art" Designer-tab layout entry alongside Slide / Disc
design.
- `pixelart` layer type added to the Layers list.
- New dedicated "Pixel art" section before Editor canvas, covering
tools / transforms / animation / export / import / undo /
animated previews.
- Editor canvas section mentions the new sidebar collapse toggle.
- Schema section lists `pixelart` template mode and layer type,
documents the `pixelArt` data-model shape, and notes that the
inline GIF89a encoder lives in `index.html` (not a vendored dep).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
A new `pixelart` layer type: a palette-indexed, RLE-encoded grid stored
in the project JSON, rendered crisp (image-rendering: pixelated) across
the editor, preview-DOM renderers, and the export canvas.
A dedicated #pixelEditorView with pencil / eraser / fill / eyedropper
(drag-paint with line interpolation), a palette panel (pick / add
colours), grid toggle, mirror X/Y, zoom, and per-stroke in-editor undo.
Reachable via a tool-palette button (adds a 32x32 layer + opens the
editor) and an "Edit pixels" button / double-click on a pixel layer.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Phase 1 — publish a design encrypted, readable only by the author:
- Crypto helpers: encryptProjectEnvelope / decryptProjectEnvelope —
AES-256-GCM over gzipped JSON, AES key NIP-44 key-wrapped to the
author (NIP-04 fallback), via a nostrEncryptCapability() probe. All
in-browser, no new dependencies.
- publishEncryptedDesign: a feed-invisible kind:30078 event — random
persistent d-tag, only d / alt / encrypted tags, no casewrap tag.
- Publish-confirm modal gains a Public / Private control with the
privacy warning and a capability gate; wired into the template and
disc-design publish paths.
- My Designs decrypts the author's own private events and shows a
lock badge; buildRowsFromEvents skips any stray encrypted event so it
can never surface in the public feed.
- state.visibility / state.privateDTag — restored on load, cleared on
Close project, autosaved.
Phase 2 — flip a published design's visibility in place:
- Owner-only Make Public / Make Private button on the preview page;
republishDesignVisibility re-publishes under the same d-tag with the
opposite wrapper (NIP-33 replace), behind a confirm that states the
public-to-private caveat.
Spec status documented in docs/PRIVATE_PUBLISHING_FEATURE.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for a Nostr-native pixel-art editor: an editable `pixelart` layer
type (palette-indexed + RLE grid stored in JSON, raster as export-only),
a dedicated Pixel Editor canvas-area view, a standalone casewrap-pixelart
Designer-tab design importable into any design as a layer, PNG export,
and a phased path to animation frames + GIF.
Includes a Size limits section: relays cap event size (commonly 64 KB),
so publishing is gated on a measured event-size budget derived from the
relays' NIP-11 max_event_size (min across the user's set, 64 KB
fallback) — soft warning at ~50%, hard block at ~90%, with a live size
readout in the editor. Encoder stores the smaller of RLE / base64 to
bound the worst case; rough size table per grid dimension included.
Also index the spec in TODO.md under drafted-but-unbuilt features.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replace the thin single-action-per-layer model with real interactive
logic:
- Cards gain onEnter/onExit chains; layers gain an onClick chain. Each is
an ordered action chain, not a single link.
- v1 action set: goto-card, go-back, open-url, show-layer, hide-layer,
toggle-layer — reveals, secret panels, multi-step buttons, branching.
- New action-builder surface: a no-JSON panel with reorderable rows,
id-based target pickers, and authoring warnings.
- Runtime tracks a per-card layer-visibility map (reset on re-entry) and
executes chains; action overlay regions are focusable buttons.
- Safety: onEnter/onExit cannot navigate (no enter->goto->enter loops);
chains capped and non-recursive; unknown action types ignored.
- Re-phased A (format) -> B (behaviour authoring) -> C (runtime) ->
D (route) -> E (stack logic: variables/conditionals, future tier).
Interactivity stays a declarative, interpreted attribute of existing
layers — no new layer types, no HTML/JS, no eval.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Aspect-ratio changes now re-fit cards via the existing import-letterbox
remap instead of stranding layers off-canvas; imports target the
stack's aspect, not a hardcoded 16:9.
- Clarify how the shared deck renderer is fed for stacks (dims from
stack, background from card) and startCard deletion fallback.
- Viewer: action regions are focusable buttons (Tab/Enter, screen
readers); empty-space clicks are no-ops; goto-card keywords clamp at
the ends; touch taps act as clicks; persist the edited card before
launching; encrypted-stack locked state on the /p/ route.
- Fix the hit-testing risk note (rotation is exact; only clip-masked
layers fall back to a bounding box) and add an accessibility row.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for "HyperCard for Nostr" — fullscreen interactive card stacks built
as a focused extension of the Slide Deck: a new templateMode 'stack'
reusing the deck card-container engine, with a per-stack aspect ratio,
optional per-layer actions (goto-card / open-url), transparent hotspots,
and a fullscreen interactive viewer with click routing. Declarative
safe schema only — no HTML/JS. Phased A-D plus a /p/<naddr> viewer route.
Also index both PRIVATE_PUBLISHING_FEATURE.md and STACKS_FEATURE.md under
the drafted-but-unbuilt features in TODO.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for publishing any design to Nostr encrypted, readable only by the
author (Phase 1 "private to me"). Covers the AES-256-GCM + NIP-44/NIP-04
key-wrap envelope, leak-free tagging (no casewrap feed tag, random d-tag),
progressive decryption in My Designs, an explicit threat model, and a
Phase 0 validation spike. Phases 2-4 (public/private toggle, recipients,
NIP-59/Blossom) sketched as future work.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Build a Nostr-native presentation system on top of the existing canvas
editor:
- Slide (templateMode 'slide') — a 16:9 Custom Art canvas with speaker
notes; publishes/forks/loads as casewrap-slide.
- Deck Builder (templateMode 'deck') — a Template-tab container with an
inline ordered slide list. Slide-sorter view: add blank, import from
Nostr or file (non-16:9 imports letterboxed), duplicate, delete,
drag-reorder, inline-rename; edit a slide in the canvas editor with
write-back. Self-contained save/load/publish/fork as casewrap-deck;
deck feed-card and publish previews show the first slide + count badge.
- Deck theme — deck-level font + background, render-time composition,
per-slide ignoreDeckTheme opt-out.
- Presenter Mode — full-screen runtime: themed slide, notes, next
thumbnail, counter; keyboard/click/on-screen nav; Presenter ⇄ Audience
view toggle (slide-only full-screen with auto-hiding chrome). Launches
from the deck tool palette and a deck's community-browser preview.
Supporting changes:
- New categories: slide ("Slide design") and slide-deck ("Slide deck");
both use the free-form Custom tag field, not media identifiers.
- Swap the Template/Designer tab icons; 50% larger.
- Remove the IMDb/OMDb metadata import (button, handler, omdbApiKey);
the imdbId field stays.
- Update README, SLIDE_DECK_FEATURE.md status, and TODO.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- docs/SLIDE_DECK_FEATURE.md: phased spec for Slide Designer (16:9
canvas + speaker notes), Deck Builder (embedded ordered slides,
slide-sorter view, deck theme), and Presenter Mode (single-window
v1, dual-window deferred to Phase E). Phases A-D with ship gates.
- Select tool icon changed to a rotated arrowhead so it reads as a
cursor, not a play button (distinct from the planned Present icon).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- New left-palette tools: Hand (pan), Shape (drag to draw a primitive),
Text (click to place). Removed the redundant sidebar add-buttons.
- Pencil strokes now keep a constant on-screen thickness (pixel-space
viewBox) instead of scaling with stroke length/aspect.
- Restructured tabs: "Disc Designer" -> "Designer", which now hosts a
Layout panel with Custom Art (default) + Disc Design; Custom Art moved
off the Template tab. Underlying appMode/save-format unchanged.
- Renamed the app from CaseWrap Studio to Artstr Studio (brand text only;
Nostr tags and localStorage keys kept for compatibility); smaller
one-line logo wordmark.
- README updated for the new tabs, tools, and keyboard shortcuts.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Pen/pencil tools (Phases A-E): left tool palette, contextual options
panel, freehand pencil strokes, Bézier pen paths, anchor/handle editing,
per-layer hidden flag with eye toggle in the layer list.
- Unified vector editing: the pen panel edits any selected shape's
fill/stroke; a "Make editable path" action converts primitives and
single-path SVGs into editable pen paths (SVG d-string parser + arcs).
- Stroke no longer clips at the shape's bounding box (overflow:visible).
- Toolbars anchored outside the scroll area so they stay locked on screen.
- Keyboard shortcuts: Delete, arrow-nudge, [/], Ctrl+D, Ctrl+C/X/V.
- Collapsible sidebar panels, collapsed by default (Layout stays open);
selecting a layer auto-opens the Layers panel.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
docs/PEN_TOOL_FEATURE.md: pen and pencil tools that author
shape.kind:'path' layers rather than a new raster type, so drawn
paths inherit the existing fill / gradient / stroke / clip / opacity
/ transform / undo / export / Nostr plumbing for free.
Decisions locked with the project owner:
- Pencil strokes are raw polylines, no smoothing (early-web look).
- Only pen paths are node-editable; pencil paths are not.
- A closed pen path auto-fills with a solid accent color on close.
- Pen + pencil work in Disc Designer mode too — the overlay attaches
to #designerDiscWrap and coordinate capture branches on
isDesignerLayer, mirroring startLayerDrag.
Plan: a state.editorTool tool-mode system (select / pen / pencil), a
left floating tool palette and a right contextual options toolbar
matching the zoom-toolbar styling, a pointer-capturing pen overlay,
shape.pen.nodes metadata for re-editable pen paths, and a new
layer.hidden flag for mask-source shapes. Five phases: A tool-mode +
toolbars, B pencil, C pen create, D pen node editing, E visibility
flag + mask polish.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
QR code layer (Phase A):
- New `qr` layer type. Add QR button alongside Image / Text / Color /
Shape in every layer panel. Spawns 1:1 with a placeholder URL.
- Controls: data textarea, error-correction level (L/M/Q/H), quiet
zone, module + background colors, transparent-background toggle.
- Renders as crisp SVG on the editor and all five preview surfaces
(dark modules merged into one path); canvas/PDF/JPEG export draws
it cell-by-cell. Round-trips through JSON and Nostr publish/fork;
old clients skip the unknown type. Works with drag / resize /
rotate / opacity / aspect-lock / clip / clipboard / undo-redo.
Combined-wrap layer panel:
- In cover mode with Artwork mode "One combined wrap image", the
layer panel collapses the separate Back / Spine / Front panels
into a single "Cover trim area" panel — the wrap is one open
canvas, so the sub-regions aren't meaningful. The combined panel
still lists any pre-existing back/spine/front layers via a
matchKeys set so they aren't orphaned from the UI.
Profile modal close fix:
- closeProfilePage used history.back(), which navigated the browser
out of the app entirely when the profile was cold-loaded from a
shared /u/<npub> link (that entry being first in the tab's
history). Switched to history.replaceState to "/" — cleans the
URL in place without navigating or firing popstate. Matches how
closeNostrPreview already handles the /share/ URL.
Docs:
- Add TODO.md tracking deferred work: QR Phase B/C, IMDb-import
removal, profile-page polish, edit-flow polish, and the unbuilt
badges / collections / zap-gated / NWC-zaps features.
- Remove docs/UNDO_REDO_FEATURE.md and docs/QR_CODE_LAYER_FEATURE.md
(shipped) and docs/TMDB_AUTOFILL_FEATURE.md (declined).
- README: document QR layers, undo/redo, fit-to-view + zoom toolbar,
and the off-canvas veil; refresh the schema and docs references.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Rewrite docs/UNDO_REDO_FEATURE.md to match what the codebase actually
contains today. Explicit snapshotted-vs-not field list covering
savedMetaByMode, customArt canvas, editing-in-place context, the full
layer schema with shape/fill/stroke/clip sub-objects, and discMeta /
spine labels. Trigger list organized by category (layer lifecycle,
geometry, paint, SVG per-element, clip, mode/structural, artwork
loads, metadata, custom-art canvas) with debounce windows per field
type. Switches storage from localStorage to memory-only for v1 to
avoid quota issues with our richer state. Adds an _isRestoring guard,
focused-input bypass for the keyboard shortcut, and 11 acceptance
criteria covering cross-mode meta swap, edit-in-place, clipboard
interaction, and the stack cap. Four implementation slices.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Custom Art project metadata:
- Hide the "Add Media Identifiers" panel when in Custom Art mode —
IMDb / UPC / TheTVDB / MusicBrainz don't fit wallpaper / banner /
logo work.
- Show a single optional Custom tag field in its place. Stored as
state.meta.customTag, round-trips through save/load, and emits to
Nostr as both an NIP-73-style 'i custom:<tag>' identifier and a
topical 't <tag>' tag (lowercased, hyphenated, capped at 64 chars).
- Hide the IMDb import button + hint in Custom Art mode.
- Auto-select the "Custom art" category the first time the user
enters customart mode without one set.
JPEG export mask fix:
- body.export-print > *:not(#exportPrintContainer) { display: none }
was hiding the global SVG <defs> container that holds <clipPath>
definitions for layer masks. With the defs invisible, every
clip-path: url(#…) reference resolved to nothing during print,
so the PDF that the JPEG flow re-rasterizes had unclipped layers.
Carve #globalClipDefs out of the exclusion selector and force
it to zero-size + overflow:hidden so it stays in the DOM but
takes no space. Direct "Print / Save PDF" was unaffected because
it doesn't use the export-print mode.
README rewrite:
- Brings the README in line with what the app actually does today:
Custom Art mode, vector shape primitives + gradients + strokes +
custom SVG upload with per-element editor, clipping/masking,
layer clipboard, profile pages at /u/:npub, Lightning tips with
WebLN auto-pay + NIP-57 zap tracking + Zapped tab, edit-in-place
via NIP-33 replaceable semantics, the publish-confirm modal, the
current schema version (5 / min 4), and the vendored qrcode lib.
Remove docs/VECTOR_SHAPES_FEATURE.md — the feature shipped.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
VECTOR_SHAPES_FEATURE.md plans a new shape layer type that subsumes
primitives (rect / rounded-rect / circle / ellipse / polygon / star /
line / custom SVG path) under a single discriminated kind, plus
solid / linear-gradient / radial-gradient fills, strokes, and clip
support that any layer type can use. JSON storage is purely additive,
backwards compatible with existing payloads, and forward compatible
for unknown shape kinds. Phased delivery: A primitives + solid fill,
B gradients, C strokes, D custom paths, E masking.
Pruned docs for features that have already shipped in this codebase:
- COLOR_AND_FONT_LAYERS_FEATURE.md — color layers + font picker
- CUSTOM_ART_MODE_FEATURE.md — custom-art template mode
- EDIT_DESIGN_FEATURE.md — replaceable-event edit-in-place
- LIGHTNING_ZAPS_FEATURE.md — LNURL-pay tip flow
- PROFILE_PAGES_FEATURE.md — /u/:npub profile pages
- STREAMING_RELAY_SYNC_FEATURE.md — progressive feed loading
- TEMPLATE_FORKING_FEATURE.md — Fork off! flow
- TRUST_AND_BLOCK_FEATURE.md — block / mute-sync / trust tiers / report
- WOT_SOFT_MODERATION_FEATURE.md — hop-1 mute/report blur
- ZAP_TRACKING_FEATURE.md — NIP-57 zap requests and receipt sums
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Uses NIP-33 parameterized-replaceable semantics: republishing kind-30078
with the same d-tag from the same pubkey supersedes the old event on
compliant relays. No NIP-09 deletion required for the replace itself.
Plan covers state/banner (Phase A), publish-flow rewire (Phase B),
owner-only Edit buttons on My Designs, profile, and preview (Phase C),
and polish (Phase D). Surfaces the honest tradeoff that reactions and
zap receipts orphan to the old event id, while naddr share links keep
working since they're d-tag based.
Co-Authored-By: Claude Opus 4.7 <[email protected]>