Commit Graph
355 Commits
Author SHA1 Message Date
thePR0M3TH3AN 2df533a93d book(B1): drag-reorder + duplicate on Pages overview
- Tiles are now draggable; grabbing one and dropping on another
  swaps it into that position. Visual feedback: source tile fades
  to 50% opacity while dragging; the hovered drop target shows an
  accent-coloured outline.
- New `moveBookPage(from, to)` splices the manifest array and
  pushes a single history entry; nothing else changes (anchors
  reference entry ids, so reordering doesn't break refs).
- Per-tile hover overlay now has two actions instead of one:
  - `⎘` Duplicate — clones the entry with a fresh id, drops any
    anchor (the duplicate sits inserted-position by default), and
    appends "copy" to the label.
  - `×` Delete — unchanged, plus a soft danger hover state.
- `attachBookPagesDnd()` is idempotent (guards via
  `data-dnd-attached`) so re-rendering the grid doesn't pile up
  listeners.
2026-06-02 14:49:36 -04:00
thePR0M3TH3AN 382165d21b book(A5): Cover view with spec-derived spread preview
Cover view shows a live, proportional preview of the front / spine
/ back spread alongside a readout of every spec value that produced
it. Panel widths track the book.spec values exactly:

- Spine width derives via the existing bookSpineWidthInches()
  helper — page count × paper thickness when spineFromPages is on,
  otherwise the manual override. The summary readout marks the
  derived value in accent color with a "derived: N pages × T in"
  caption.
- The spread reflows automatically: change paper thickness in
  Document settings or add / remove pages, the spine column
  resizes, the readout updates. Same when toggling coverHasSpine
  or coverHasBack — the spread becomes back+spine+front /
  spine+front / back+front / front only.
- Bleed adds to each outer edge of the back and front panels; the
  inner edges between panels share so no double-bleed.
- A scaled px-per-inch factor fits the spread into ~720 px wide;
  scaling caps at 60 px/in so a tiny chapbook isn't huge.
- Cover view's renderBookCoverPreview() is guarded inside render()
  — it only repaints when the Cover sub-view is visible, so flipping
  between Pages and Cover stays cheap.

Phase A5 ships preview-only. Phase B mounts the existing cover
designer host inside #bookCoverView so users can actually paint the
panels; Phase D1 publishes the cover as its own casewrap-page event
with role:'cover-spread'. A "Replace cover from Nostr…" button is
stubbed disabled to anchor the future picker in the UI.
2026-06-02 14:45:21 -04:00
thePR0M3TH3AN 609ffbb6af book(A4): Pages overview view skeleton
The book mode now shows a dedicated Pages overview replacing the
empty cover canvas. Layout mirrors the deck-sorter pattern:

- New #bookView container in the canvas area, hidden by default and
  shown via `body.template-book`. Replaces #sheetWrap in book mode
  (same trick the deck sorter uses).
- View-toggle bar at the top: **Pages** / **Cover** with an active
  pill. Cover view is a placeholder pointing at Phase A5.
- Pages view has a header buttonRow with `+ Add page` and `+ Add 10
  pages`, plus a live page-and-reflow-segment counter readout.
- Tiles render as 2:3-aspect placeholders with the page number
  (driven by `book.spec.pageNumbering` — `enabled`, `startAt`,
  `firstNumberedPage`, `format` all honoured) and the entry's
  label. Reflow-segment tiles get a diagonal-stripe background so
  they're visually distinct from designed-page tiles (already
  positioned for Phase C.5 even though no reflow entries can be
  created yet).
- Per-tile delete button (visible on hover) removes the entry from
  the manifest; any other entry that anchored to the deleted one
  (`anchor.entryId` / `anchor.after`) gets its anchor reset to
  null so dangling refs don't break later phases.
- Empty state when `book.pages` is empty.
- `makeBookEntryId()` generates stable opaque ids
  (`entry:<random>`) so AnchorBlocks survive reorders, matching the
  data-model contract in docs/BOOK_DESIGNER_FEATURE.md.
- Hides the tool palette, canvas toolbar, and tool options panel
  in book mode (nothing to draw on yet — Phase B's per-page editor
  will surface them again when a page is open).
- `renderBookPagesGrid()` hooks into the main `render()` flow so
  the grid stays in sync with state across history undo / autosave
  restore / document-settings changes.
2026-06-02 14:42:11 -04:00
thePR0M3TH3AN 8fca55ff35 Merge branch 'main' into book-designer 2026-06-02 14:38:48 -04:00
thePR0M3TH3AN b829935103 fix: recreate <defs> in rebuildClipDefs when it's missing
Adam hit a hard crash every time he clicked a layout tab on the
deployed site:

  Uncaught TypeError: Cannot set properties of null (setting
  'innerHTML') at rebuildClipDefs (16403) → render (11689) →
  applyLayout (28513)

rebuildClipDefs() does defs.innerHTML = '' on an element found
via defsRoot.querySelector('defs'). If the SVG #globalClipDefs
container exists but its inner <defs> child has been removed (or
an early DOM-hook / browser extension injected the SVG without a
<defs> child), the query returns null and the assignment throws.
Because rebuildClipDefs is the first thing render() calls, the
crash aborts the entire render — body classes don't swap, the
canvas doesn't redraw, the layout panel doesn't update. The
top-level tab visually toggles but everything downstream stays
frozen. Exactly the "canvas stuck" report.

Guard the lookup: if no <defs> child exists, re-create one and
append it to the container, then proceed with the rebuild. The
ensureGlobalClipDefs() helper already creates a <defs> on first
call; this fix handles the rarer case where the container exists
in a malformed state.
2026-06-02 14:38:42 -04:00
thePR0M3TH3AN 7fcc8458be book: hide cover-mode canvas panels in book mode
The cover designer's BACK / SPINE / FRONT slot placeholders +
bleed / trim / fold-line / crop-mark guides were still rendering
on the canvas-host when book mode was active, because the existing
mode-switch only hides them for customart / slide / pixelart /
deck. Add a matching `body.template-book` block that hides every
`.coverOnly` element plus the disc / jewel artifacts and the
spine-text label.

Also hides the sidebar #exportFieldset / #layersFieldset /
#printExportRow until book pages exist (matches the deck-mode and
pixelart-mode treatment — they're not in the book WORKFLOW_ORDER
anyway, but the CSS gate keeps them from sneaking in).

The canvas-host now renders empty in book mode — the Pages
overview view (A4) and Cover view (A5) fill it in subsequent
commits.
2026-06-02 14:21:08 -04:00
thePR0M3TH3AN 57897a462b book(A3): Document settings modal
Wires the full Document settings modal for book mode. Every field
from `book.spec` is editable, applies live on Apply, and persists
via the existing autosave.

- New `#bookDocumentSettingsModal` markup:
  - Book size dropdown with five named presets (US Trade, US
    Letter, A5, A4, Square 8×8) plus Custom; selecting a preset
    locks the trim inputs to the right dimensions, Custom unlocks
    them.
  - Trim width / height (inches).
  - Bleed (default 0.125).
  - Paper thickness (in / page).
  - "Derive spine width from page count × paper thickness"
    checkbox; manual spine input shown only when unchecked. A
    live note below shows the computed spine ("Spine = N pages ×
    T in = D in").
  - Cover layout toggles: include spine, include back panel.
  - Page numbering: enabled toggle reveals start-at, first-
    numbered-page (manifest index), and a format string field
    with a `{n}` placeholder hint.
- New JS helpers:
  - `BOOK_SIZE_PRESETS` table (one source of truth for the five
    preset sizes).
  - `openBookDocumentSettings()` / `closeBookDocumentSettings()`
    matching the existing publish-confirm modal lifecycle.
  - `_syncBookSettingsSpineNote()` updates the derived-spine
    note on paper-thickness / from-pages toggle changes.
  - `_syncBookSettingsPresetLock()` locks / unlocks trim inputs
    when the preset dropdown changes.
  - `applyBookDocumentSettings()` writes every field back to
    `state.book.spec`, pushes a single history entry, then runs
    `syncInputs()` + `render()` so the sidebar summary refreshes.
- Sidebar "Document settings…" button is now enabled (was
  disabled placeholder in A2) and wired to the new modal.
- Esc-to-close hooked into the existing global Escape handler
  next to publishConfirm. Click-outside-to-close handled by the
  same delegation pattern other modals use.
2026-06-02 14:17:16 -04:00
thePR0M3TH3AN 7bed2a6a3f book(A2): Template-tab "Book" entry + mode switching
Adds the Book button to the Template tab's layout picker
(alongside Case cover / Disc labels / Slide deck) and wires the
applyLayout('book') path so the user can switch into book mode.

- `mode-book` button + click binding via the existing applyLayout
  loop (one-line list addition keeps every layout tab routed
  through the same handler).
- `applyLayout('book')` calls `ensureBookState()` to lazy-init the
  manifest the first time a user enters the mode.
- New `template-book` body class + CSS gating: the new
  `#bookFieldset` sidebar block shows only in book mode, and the
  cover / disc / deck / custom-art fieldsets hide in book mode so
  the sidebar stays focused on the spec + masters affordances.
- `WORKFLOW_ORDER` learns the book column.
- `sheetW` / `sheetH` fall back to trim + bleed when
  `templateMode === 'book'` so the canvas-host placeholder is
  sized correctly (it stays blank in this phase; the actual Pages
  overview + Cover view come in A4/A5).
- Sidebar fieldset shows a derived spec summary (trim, bleed,
  paper thickness, derived spine width, page + master count)
  rendered by a new `syncBookSidebarSummary()` hook into
  `syncInputs()`. Book-state helpers (`bookPageCount`,
  `bookSpineWidthInches`) are pure functions so future phases can
  reuse them.
- Two placeholder buttons (Document settings… / Manage masters…)
  are stubbed disabled with a hint pointing at the upcoming
  phases, so users can see where the controls will land.
2026-06-02 14:14:32 -04:00
thePR0M3TH3AN 314b582b0c book(A1): state shape + autosave round-trip for templateMode 'book'
First commit of the Book Designer feature. Pure plumbing — no UI
yet. Establishes the foundation for Phase A:

- New `templateMode: 'book'` recognised by `normalizeTemplateMode`
  (matches either `payload.templateMode === 'book'` or the presence
  of a `payload.book` block).
- `templateTypeForMode` returns `'book'` for the new mode.
- `primaryTargetForMode` returns `'book-page'`.
- `isLayerForCurrentMode` accepts every `book-*` target
  (`book-page`, `book-master`, `book-cover-front/back`, `book-spine`)
  in book mode.
- `state.book` initialised to `null`; the new `ensureBookState()`
  helper lazily fills it with `defaultBookState()` (empty spec,
  null cover, empty masters + pages arrays).
- `defaultBookSpec()` returns the v1 defaults documented in
  docs/BOOK_DESIGNER_FEATURE.md (US Trade 6×9, 0.125 bleed,
  paperThickness 0.0042, spineFromPages true, page numbering on,
  default master null).
- `defaultBookTextStyles()` returns the v1 markdown stylesheet
  (Georgia body, Helvetica headings, monospace code, accent-blue
  links, blockquote indented italic, etc.) — used by Phase C.5's
  pagination engine.
- SCHEMA_VERSION bumped to 6 with a comment explaining the
  additive `book` block. Existing autosave path serialises +
  restores `state.book` for free via Object.assign.

No behaviour change to existing modes — Book mode is unreachable
through the UI in this commit. The next commit (A2) adds the
Template-tab entry that switches into book mode.
2026-06-02 10:58:54 -04:00
thePR0M3TH3AN 2f5167054b docs(book): polish pass — schema consistency + MVP path + missing acceptance
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).
2026-06-02 10:53:53 -04:00
thePR0M3TH3AN 7383b98cf9 docs(book): fold reflowable text + markdown + NIP-23 into the spec
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.
2026-06-02 10:47:42 -04:00
thePR0M3TH3AN 33ee2b7ec5 docs(book): rewrite spec around Nostr-native sidecar architecture
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.
2026-06-02 10:36:40 -04:00
thePR0M3TH3AN 50eed8c017 docs: spec the Book Designer (unified cover + pages + PDF export)
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.
2026-06-02 10:29:01 -04:00
thePR0M3TH3AN 1baa917c9a free transform: keep pointer capture during a rotate drag
startLayerRotate and startLayerSelectionRotate called render() on
every pointermove, which tore down the .freeLayer wrapper that owns
the rotation handle. That dropped pointerCapture mid-drag, so after
the first move event the gesture "let go" — the handle stopped
following the cursor and only a few degrees of rotation registered
before the user had to grab again.

Fixed both paths the same way the resize handler does: mutate the
layer's rotate (and centre, for multi-layer) and apply the change
directly to the affected .freeLayer's transform / position styles
inside move; only call render() on pointerup so the rest of the UI
catches up. The sidebar Rotation field updates live via
syncLayerInputs() as before.
2026-06-02 10:20:16 -04:00
thePR0M3TH3AN 31df555ae7 shape tool: rename tool-panel inputs so they don't collide with layer-panel IDs
The Shape tool's fill / stroke colour pickers and stroke-width input
shared DOM ids (shapeFillColor, shapeStrokeColor, shapeStrokeWidth)
with the selected-shape-layer edit panel's equivalents.
document.getElementById returns only the first match, so the input
listeners that drive _shapeToolOpts attached to the layer-panel
inputs (which aren't visible while the Shape tool is active), not
the tool-panel inputs. Result: changing fill / stroke colour or
stroke width on the tool panel had no effect on the next shape drawn.

Renamed the three tool-panel inputs to shapeToolFillColor /
shapeToolStrokeColor / shapeToolStrokeWidth (and their <label for>
references and the syncToolOptionInputs + input-event listeners).
A full repo scan confirms no other duplicate DOM ids remain.
2026-06-02 10:17:03 -04:00
thePR0M3TH3AN e25180346b Merge branch 'pen-tool-phase-d': Illustrator-parity pen / select tools, Pathfinder, Align, Eyedropper, Free Transform, Marquee
Bundles the design-tool polish arc:

- Pen tool: Direct Selection (A) as a separate tool, Cmd/Ctrl temp
  toggle, hover cursor cues, Shift constrain, auto-add / auto-delete
  on segment / anchor, click-vs-drag convert, Alt-drag asymmetric
  pull, Alt-click curved ↔ pointed (drops / grows handles), multi-
  anchor select via Shift-click + marquee, arrow-nudge anchors,
  drag-segment to bow the curve, continue-an-open-path from its
  endpoint, Spacebar reposition + Alt mid-draw asymmetric during
  create-drag, Alt-drag handle breaks symmetry in DSel, Cmd+J join,
  Cmd+Alt+J average, Path Actions side panel with batch buttons.
- Eyedropper (I) via native window.EyeDropper.
- Align / distribute panel for layer selection.
- Free Transform rotation handle on single + multi-layer selection
  bounding boxes, Shift snaps to 15°.
- Pathfinder ops (Unite / Subtract / Intersect / Exclude), backed
  by vendor/polygon-clipping.min.js.
- Marquee box-select on the V tool with marching-ants animation.
- Double-click a vector shape with V to dive into Direct Selection;
  DSel empty-click deselects the layer entirely.
- Misc: pen draft stroke width now matches the panel value, anchor
  + handle glyphs shrunk closer to Illustrator size, Direct Select
  arrow tinted light blue.
- Docs: PEN_TOOL_FEATURE.md + TODO.md updated.
2026-06-02 09:53:28 -04:00
thePR0M3TH3AN 5b0eec1a1a docs: log the Illustrator parity arc + follow-ups
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.
2026-06-02 09:53:06 -04:00
thePR0M3TH3AN 404a6964a1 selection: dive-into-edit on shape dbl-click + DSel empty-click deselects
- Double-click a vector shape with the Select tool active and we
  flip to Direct Selection on that shape, auto-converting non-pen
  primitives (rect / circle / polygon / etc.) to editable pen paths
  first so anchors appear immediately — Illustrator's dive-into-
  isolation behavior. Double-click is ignored if any other tool is
  active or the click landed on a transform handle.
- Direct Selection on the canvas: a plain click in empty space
  (anywhere the overlay covers, including beyond the layer's bbox)
  now clears the layer selection entirely, not just the anchor
  sub-selection. The anchor-marquee path still kicks in for real
  drags. Off-canvas clicks already deselected via the previewShell
  handler, so this closes the last in-canvas gap.
2026-06-02 09:48:43 -04:00
thePR0M3TH3AN 8ef9b0cb34 pen tool: shrink anchor + handle glyphs closer to Illustrator size
The 0.0075 dot / 0.0018 line ratios still rendered as ~14px dots
with thick stems on a 1920px-wide preview, which dominated short
paths. Dropped the dot radius to 0.0028 and the line width to ~0.001
so the editing UI reads as a tiny on-canvas widget (~7px) rather
than a UI element fighting the artwork. Both renderPenEditHandles
and renderPenDraft use the new sizes so live drawing and post-commit
editing stay consistent.
2026-06-02 09:43:44 -04:00
thePR0M3TH3AN 5b1c038c27 tool palette: tint the Direct Selection arrow light blue
The hollow white arrow distinction was too subtle when both V and A
tools are side-by-side. Switched to a solid light blue (#60a5fa)
arrow so Direct Selection reads as visually distinct at a glance;
the regular Select arrow stays the default black.
2026-06-02 09:42:29 -04:00
thePR0M3TH3AN 2fc555a4ff pen tool: live draft stroke width matches the panel value
The pen draft path was using a canvas-relative thickness (lineW * 2,
derived from the canvas dimensions) for its preview stroke, so the
ribbon you saw while drawing looked different from the path you got
after committing. The committed path stores its stroke width in
pixel-space (1 unit = 1 px @ 96 dpi); the overlay renders in inch
space, so the live preview now divides _penToolOpts.penStrokeWidth
by 96 — same trick the pencil draft already uses — and adds round
line-cap / line-join so curves look consistent with the committed
path.
2026-06-02 09:40:59 -04:00
thePR0M3TH3AN cd54d8357f marquee box-select on canvas with the Select tool
Click-and-drag on an empty area of the canvas (with the V tool
active) now draws a marching-ants rectangle. On release, every
layer whose bounding box intersects the rectangle is selected;
Shift / Ctrl / Cmd held during the drag adds to the existing
selection instead of replacing it.

Plain clicks (no drag past the 4 px deadzone) still clear the
selection, preserving the previous "click empty area to deselect"
behaviour. Other tools own their own canvas drags (Pen creates
anchors, Hand pans, Shape drags out a primitive, Direct Select
marquees over anchors), so this only kicks in for V.

Shift+click on a layer to add or remove it from the current
selection was already wired — confirmed working through the
existing additive path in startLayerDrag.
2026-06-02 09:38:52 -04:00
thePR0M3TH3AN eb9aec19d8 pathfinder: Unite / Subtract / Intersect / Exclude on shape selection
Boolean ops on 2+ selected shape layers, backed by the vendored
polygon-clipping library (29 KB minified, MIT) loaded from
src/vendor/. Adds a new "Pathfinder" details block in the sidebar
under Align & distribute.

- Unite combines all selected shapes into one outline.
- Subtract removes upper shapes from the bottom-most shape.
- Intersect keeps only the overlapping region.
- Exclude keeps only the non-overlapping regions (XOR).

Pipeline: each selected layer's pen path is sampled to a polygon
(16 samples per Bézier segment, in world inches), the chosen op
runs, and the result is rebuilt as a new pen layer that inherits
the bottom-most input layer's z-order, target, fill and stroke —
matching Illustrator's Pathfinder convention. Non-pen primitives
(rect, circle, etc.) are auto-converted to pen paths first.

Limitations baked in:
- Rotated layers aren't supported yet (we'd need to rotate the
  sampled polygon before clipping). The op refuses with a toast.
- Curves are flattened — the result is a polygon approximation
  with `fill-rule: evenodd` so multi-ring outputs (donut from a
  subtraction) render correctly. The renderer learned to honor
  shape.fillRule for this commit.
- Multi-polygon results stay rendered correctly but only the
  first polygon's outer ring is preserved in pen.nodes, so
  Direct Select editing post-op is partial.
2026-06-02 08:54:26 -04:00
thePR0M3TH3AN 9bb401bea2 free transform: rotation handle on the selection bbox
A 9th handle (round, accent-colored, sitting above the bounding box
on a thin connector line) joins the existing 8 scale handles on both
the single-layer overlay and the multi-layer selection box.

- Single layer: drag rotates the layer about its own centre, with
  Shift snapping to 15° increments. The existing resize handles
  already account for layer.rotate in their local-frame math, so
  resizing keeps working at any rotation.
- Multi-layer selection: drag rotates each layer's centre around the
  selection bbox's centre AND advances each layer's own rotation by
  the same delta — the group spins as a rigid body. Pre-snapshotting
  the centres + rotations keeps the math stable across long drags.
- Locked / generated-disc layers are filtered out as everywhere else.
2026-06-02 08:45:51 -04:00
thePR0M3TH3AN ce1023a14e align + distribute panel for selected layers
A new "Align & distribute" details block in the layer-controls sidebar
surfaces eight ops: left / horizontal-centre / right and top / middle
/ bottom alignment, plus horizontal and vertical distribution.

- Multi-selection (2+ layers) aligns within the selection's own
  bounding box, so the outer layers stay put and the rest snap into
  line with them.
- Single-layer selection aligns to the layer's target bounds (page
  panel / disc face / custom canvas), making it easy to centre or
  pin a single asset without setting up a dummy second layer.
- Distribute requires three or more layers — the outermost two stay
  pinned and the inner layers slide so consecutive centres are
  evenly spaced along the chosen axis. Helpers toast a hint when the
  selection is too small.
- Locked / generated-disc layers are skipped automatically.

Flip H / V were prototyped here but pulled out — they need a renderer
extension to honour per-layer scaleX/scaleY, which is a separate change.
2026-06-02 08:43:39 -04:00
thePR0M3TH3AN cb130b82cd eyedropper tool (I) — sample colors via the native picker
A new ⊙ button in the tool palette and the I keyboard shortcut open
the browser's window.EyeDropper picker so the user can sample any
pixel anywhere on screen (not just our canvas) and apply the
sampled hex to a smart target:

- Selected shape layer → fill color (Shift = stroke color).
- Selected text layer  → text color.
- No layer selected    → pen / shape tool's default fill (or
  stroke), so the next thing drawn picks up the sampled hue.

The eyedropper is a transient action rather than a persistent tool —
invoking it doesn't change the active tool, so users can sample
mid-edit without losing their cursor. Falls back to a hint toast in
browsers that don't yet support window.EyeDropper (Safari).
2026-06-02 08:41:06 -04:00
thePR0M3TH3AN 0cb028c3f1 pen tool: side-panel "Path actions" block for anchor + path ops
A new "Path actions" block appears in the right-side tool options panel
whenever Pen (P) or Direct Selection (A) is active and an editable pen
layer is selected. The block surfaces the most useful anchor- / path-
level operations as buttons so they're discoverable without keyboard
shortcuts:

- Pointed corner / Curved smooth — flip the selected anchors (or all,
  if none are selected) between handle-less corner and symmetric-handle
  smooth, matching the Alt-click convert behavior.
- Delete anchor — bulk-delete the current anchor selection (Backspace
  equivalent).
- Average — move selected anchors to their centroid (Cmd+Alt+J).
- Join endpoints — close an open path when both its endpoints are
  selected (Cmd+J); button auto-disables otherwise.
- Close / Open path — toggle the path's closed flag.
- Reverse direction — flip the order anchors are drawn in.

A small hint at the top of the block reflects the current selection
size, so the user knows what an action will affect. The "Join" button
auto-disables when join would be a no-op; "Average" requires at least
two anchors. Direct Selection also gets a short panel reminding the
user how anchor editing works (multi-select via Shift / marquee, arrow
keys nudge selected anchors).
2026-06-01 21:46:45 -04:00
thePR0M3TH3AN d50284ecef pen tool: Alt-click anchor now truly converts curved ↔ pointed
The presence of handles is now the source of truth for whether a
node is pointed or curved, so Alt-click does a full flip in either
direction:

- Anchor with any handle → drops both handles and demotes to a real
  corner. The path renders straight segments through it.
- Anchor with no handles → grows symmetric Bézier handles aimed along
  the chord through this anchor (using neighbouring anchors when they
  exist, falling back to the single available neighbour for endpoints
  of an open path). Handle length is 1/3 of the chord, so the curve
  bends visibly without ballooning out.

Previously the toggle only flipped the kind flag without touching
handles, so a smooth-to-corner click left the curve looking unchanged
and a corner-with-no-handles click did nothing visible at all.
2026-06-01 21:42:48 -04:00
thePR0M3TH3AN 62f2a7592e pen tool: Join (Cmd+J) + Average (Cmd+Alt+J)
- Cmd+J closes an open path when both its endpoints are part of the
  current anchor selection. No-op if the path is already closed or
  the selection doesn't cover both endpoints; the user gets a hint
  toast in that case.
- Cmd+Alt+J averages every selected anchor to the centroid of the
  selection. Handles slide with their anchors so local curvature is
  preserved. Requires at least two anchors selected.
2026-06-01 21:37:11 -04:00
thePR0M3TH3AN c3c58e67f5 pen tool: Spacebar reposition + Alt mid-draw asymmetric (Illustrator parity)
While the pen tool's click-drag is still down (the gesture that pulls
Bézier handles out of a freshly-placed anchor), two modifiers now
match Illustrator's pen behavior:

- Hold Alt during the drag and only the outgoing handle is pulled.
  The incoming side stays at the anchor, giving an asymmetric corner
  that's perfect for tangent-then-corner transitions without lifting
  the pen.
- Hold Space and the drag stops growing handles and starts sliding
  the anchor itself, carrying any handles along. Release Space to
  resume handle-pulling from the anchor's new position. Mouse delta
  is tracked from the moment each mode is entered, so toggling Space
  on/off mid-drag doesn't introduce drift.
2026-06-01 21:36:05 -04:00
thePR0M3TH3AN 0fc45ae9fc pen tool: auto-add / auto-delete + convert + handle-break (Illustrator parity)
- Pen tool over an existing segment now auto-inserts an anchor at the
  click with no modifier (was Alt-only). Pen over an anchor deletes it
  on click — Illustrator's default "Auto Add/Delete Anchor" behavior.
- Drag (past a small threshold) on an existing anchor with the Pen tool
  converts the anchor on the fly: pulls symmetric handles from the
  drag, promoting corner → smooth. Alt-drag pulls only the outgoing
  handle for an asymmetric corner.
- Alt-click an anchor in Pen mode still toggles corner ↔ smooth (drops
  or pairs handles without a drag), preserving the existing modifier.
- Alt-drag a handle in Direct Selection breaks the smooth-anchor's
  handle symmetry: the node becomes a corner so the opposite handle
  stays put while the dragged one moves independently.
- Endpoint-of-open-path still wins over auto-delete: hovering the start
  or last anchor of an open path shows the "continue" pointer cue, and
  clicking resumes drawing rather than deleting.
- Hover cursors updated: − (delete) over a deletable anchor, + (add)
  over a segment in Pen mode, grab over a segment in Direct Select.
2026-06-01 21:35:21 -04:00
thePR0M3TH3AN 1d64306a33 pen tool: continue an open path from its endpoint (Illustrator parity)
With the Pen tool, clicking an endpoint of an open path resumes
drawing from that anchor. The source path is hidden for the duration
of the draft; on commit (Enter / click-back-to-start / Escape) the
old layer is dropped and the new (longer) path takes its place,
preserving the original z-order, target, fill and stroke. Clicking
the start anchor reverses the node order so the next click extends
in the natural direction. Cancelling the draft restores the source
path unchanged.
2026-06-01 21:29:03 -04:00
thePR0M3TH3AN 547ff5d1c6 pen tool: Illustrator parity polish (multi-anchor + drag-segment + marquee)
- Multi-anchor selection. Shift-click an anchor to add/remove it from
  the selection set; click without modifier selects just that anchor.
  Dragging any selected anchor moves the whole set rigidly (anchors +
  handles), so reshape operations affect groups in one gesture.
- Arrow keys nudge selected anchors. Direct Selection (or Pen) with at
  least one anchor in the selection: ←/→/↑/↓ moves the anchors and
  their handles by 1px (10px with Shift) in pixel-space; bbox refits
  automatically. Falls back to the existing layer-nudge when no anchor
  is selected.
- Marquee anchor box-select. In Direct Selection, drag from empty
  space inside the layer to draw a dashed rectangle; on release, every
  anchor whose center lies inside the box is selected. Shift+drag adds
  to the existing selection.
- Drag a curve segment to bow it. Equal-split delta distributed
  across the two inner control points so the cursor tracks the grabbed
  point; smooth anchors on either end keep their handles mirrored. Lines
  are auto-promoted to cubics on first bow.
- Alt-click a segment in Direct Selection now inserts an anchor at the
  click (mirrors the Pen tool's segment-insert), and Alt-click an
  anchor still toggles corner/smooth.
- Backspace deletes every selected anchor at once (bulk delete), with
  the "path needs at least two anchors" guard applied to the resulting
  size.
2026-06-01 21:24:24 -04:00
thePR0M3TH3AN c73aab2c20 pen tool: Illustrator parity foundation (Direct Select + cursor + Shift constrain)
- New Direct Selection tool (A) as a separate palette button, distinct
  from the regular Select tool. Anchors and handles only appear when
  Pen or Direct Select is the effective tool, matching Illustrator's
  V/A split.
- Cmd/Ctrl temporary toggle: holding the modifier from any non-Pen tool
  flips the effective tool to Direct Selection so a quick anchor tweak
  doesn't require dropping the current tool; release restores it.
- Hover cursor feedback on pen/direct-select: shows + when over the
  end of an open path, - over an existing anchor (delete-adjacent),
  ✕ near the start to close, default crosshair otherwise.
- Shift constrains pen segments to 0/45/90 degrees while drawing and
  while previewing the next click, matching Illustrator's constrain.

Foundation for the multi-anchor select + drag-segment + continue-path
endpoint work that follows.
2026-06-01 21:19:26 -04:00
thePR0M3TH3AN 8ca7a2468a Pen tool: select-mode anchor editing
Selecting a pen-shape layer with the Select tool now reveals its
anchors + handles for inline editing — no need to switch to the
Pen tool first. Drag anchors / handles to reshape; right-click
or Backspace to delete; Alt-click toggles corner ↔ smooth. Pen
mode keeps the richer affordances (segment-click to insert
anchor, click-to-create-new-path) since those would conflict with
the Select tool's normal layer-click behavior.

Three small changes:

- getEditablePenLayer drops the strict 'pen' tool requirement and
  also accepts 'select'.
- syncPenOverlay keeps the overlay alive in select mode when a
  pen-shape layer is selected; render() now re-syncs each frame
  so the overlay appears / disappears as selection changes
  (without requiring a tool change).
- renderPenEditHandles marks anchor + handle elements
  pointer-events: all (with `cursor: pointer`) while the overlay
  base stays pointer-events: none. Empty-space clicks fall
  through to the layer's normal selection behavior; only clicks
  on anchors / handles register as edit ops.

onPenPointerDown gains a select-mode branch that:
- Only handles anchor / handle hits; segment clicks pass through
  to the layer (so dragging the path body still selects / moves
  the layer as expected).
- preventDefault + stopPropagation on anchor / handle hits so
  the underlying layer-drag handler doesn't also fire.
2026-06-01 21:08:06 -04:00
thePR0M3TH3AN 9c638c0676 Pen tool: pixel-space viewBox so stroke widths match across shapes
Two pen paths with the same stored stroke number rendered at very
different on-screen thicknesses — strokes scaled with layer size
because nodes lived in a 0-100 normalized viewBox. The pencil tool
solved this exact problem (see commitPencilStroke ~line 9580 with
the inline comment about pixel-space viewBox = "true on-screen
thickness, identical no matter how long the stroke is"). Switching
pen paths to the same scheme.

Pen nodes now live in a pixel-space viewBox where 1 unit = 1 px
at 96 dpi: viewBox dims = (layer.w * 96, layer.h * 96). Because
the viewBox aspect matches the layer aspect, the renderer's
sx == sy (uniform scale) AND the same stroke-width number produces
the same device-pixel thickness on every pen path regardless of
size.

Five touchpoints updated:

- finishPenPath: node remap multiplies bbox-relative coords by 96
  instead of normalizing to 0-100; viewBox.w / viewBox.h written
  as bbox.w*96 / bbox.h*96.
- penNodeToInch / inchToPenNode: convert via /96 (was /100 of
  layer dim).
- regenPenLayerD: viewBox rewritten as (layer.w*96, layer.h*96)
  on every edit. Catches user-resizes via corner handles too.
- refitPenLayerIfNeeded: bounds checked against pixel-space
  viewBox; layer position/size translated via /96; node remap is
  now a pure translation (no rescale, since the units stay
  pixel-space).
- "Make editable path" conversion: 0-100 primitive nodes mapped
  to pixel-space relative to the existing layer's dimensions.

Strokes are now consistent across pen paths of any size, and the
in-progress draft (rendered in the pen overlay's inch-space) still
matches the final committed path's visual stroke weight at the
canvas's natural 96-DPI mapping.
2026-06-01 17:06:57 -04:00
thePR0M3TH3AN 3831b39b2e Pen tool: write viewBox.h to match layer aspect for uniform strokes
The previous "uniform-scale remap" commit fixed node coordinates to
land in 0-100 × 0-(100*h/w), but the renderer (buildShapeSvgInner
path branch) reads shape.viewBox and computes sx = 100/vb.w,
sy = vbH/vb.h. With shape.viewBox.h hardcoded to 100, sx ≠ sy for
non-square layers and the path got stretched again — strokes
distorted around curves after the path was committed, even though
the in-progress draft (which renders inside the pen overlay, not
the layer SVG) looked fine.

Fix is the matching half: also write shape.viewBox.h = 100*lh/lw
wherever we touch the pen data, so sx == sy and the renderer
applies a uniform scale.

- finishPenPath now sets viewBox.h = vbH from the bbox aspect.
- regenPenLayerD (called on every edit drag + insert/delete/toggle)
  recomputes vbH from current layer.w/h before writing the new d
  string. This catches the rare case where layer.w/h change between
  refit calls without my refit running.
- The "Make editable path" conversion remaps primitiveToPenNodes /
  parsePathDToNodes results (which always come in 0-100×0-100) into
  the layer's actual aspect space, and writes viewBox.h to match.
  Result: converting a rect → editable path on a non-square layer
  keeps its proportions, and the resulting pen path edits cleanly
  with uniform strokes.

Strokes now stay a constant width all the way around closed
shapes, even after the path is fully committed and re-rendered
through the standard shape-layer pipeline.
2026-06-01 16:46:58 -04:00
thePR0M3TH3AN e5c06b7415 Pen tool: uniform-scale remap (fixes stroke distortion) + smaller handles
Two visual issues on top of the Phase D close-out commit:

1. **Stroke width varied along curves.** The pre-existing pipeline
   stored nodes in 0-100 × 0-100 viewBox space and let the SVG
   renderer pick `vbH = 100 * layer.h / layer.w` with
   `preserveAspectRatio="none"`. For any non-square layer, the
   0-100×0-100 node range got stretched non-uniformly into a 100×vbH
   viewBox, distorting the path: strokes appeared wider where the
   curve traveled more in one axis. Phase D's refit made this much
   more likely because dragging anchors out of bounds routinely
   produced non-square layers.

   Fix in two places:
   - finishPenPath: uniform-scale the bbox by `100/bbox.w` so y
     range lands at exactly `100 * h/w = vbH`. The viewBox aspect
     matches the layer aspect, no stretching.
   - refitPenLayerIfNeeded: same uniform-scale invariant. Also
     respects the layer's current vbH when deciding whether nodes
     are "out of bounds" — was previously hard-coded at 100, which
     wrongly fired refit on every drag in already-non-square layers.

2. **Anchor + handle glyphs were too prominent.** On a 1920px-wide
   slide preview the old `dotR = 0.013 * unit` rendered ~25px
   dots. Reduced to `0.0075 * unit` (~12px) and stroke from
   `0.003 * unit` to `0.0018 * unit`. The in-progress draft
   renderer matches.

Strokes now stay constant width all the way around a curve, and
the editing UI reads as overlay rather than as part of the artwork.
2026-06-01 16:36:19 -04:00
thePR0M3TH3AN b825181356 Pen tool Phase D close-out: add/delete anchor, smooth/corner, bbox refit
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).
2026-06-01 16:25:38 -04:00
thePR0M3TH3AN 007ad1d1e2 Cleanup: drop superseded specs + refresh README
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.
2026-06-01 16:11:35 -04:00
thePR0M3TH3AN c1ac849219 External embed spec: mark shipped + drop Phase 8
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.
2026-06-01 15:58:05 -04:00
thePR0M3TH3AN 9fe574c41d External embed Phase 5: pixel-art play/pause + fps + reduced-motion
Animated pixel-art embeds (frames > 1) now respect three controls
per docs/EXTERNAL_EMBED_FEATURE.md §6.3 / §11.4:

- ?play=1 / ?play=0 — explicit auto-play toggle. If the param
  isn't set, autoplay follows the visitor's prefers-reduced-motion
  preference: motion-OK users get auto-play, reduced-motion users
  get start-paused with a play CTA. ?play=1 forces auto-play even
  in reduced-motion mode (the spec's "user opt-in" wording).
- ?fps=N — override frame rate, clamped to 1-60. Falls back to the
  payload's stored fps (default 8) when not set.
- A play/pause chip in the embed footer's left side that toggles
  on click. Re-renders the artwork with the new autoplay state;
  the renderer's RAF self-stops when the canvas leaves the DOM, so
  the swap is leak-free.

Renderer changes (renderPixelArtPreviewDOM):
- New opts.autoplay (default true) gates the requestAnimationFrame.
  When false, frame 0 is drawn but no animation loop starts.
- New opts.fps (number, optional) overrides pa.fps. Defaults
  unchanged for all existing callers (linked-design renderer,
  feed cards, modal preview) because they don't pass either opt.

Single-frame pixel art is unaffected — the play/pause chip only
appears for frames.length > 1, and the renderer ignores autoplay
for the still-image path.
2026-06-01 15:28:09 -04:00
thePR0M3TH3AN d29afaea8c External embed Phase 4: deck player with keyboard + fullscreen
Public multi-slide deck embeds get a paginated viewer instead of
the single-thumbnail-with-badge that was hitting before (the
shared renderDeckPreviewDOM compact path).

_paintEmbed now branches on mode === 'deck' && slides > 1 into a
new _paintEmbedDeck that:

- Reads ?slide=N (1-based) from the route params for the initial
  index; clamps to [0, slides.length-1].
- Renders the current slide via composeSlideForDeck +
  renderDesignPayloadPreview into a .designObjectFitWrap and
  fitMiniPreview-scales it.
- Adds prev / counter / next pill chips to the footer left side
  (only when slides > 1 and controls=1), and the existing author
  chip after them.
- Adds a fullscreen chip ⛶ + the Open-in-Artstr link to the
  footer right side.
- Wires keyboard nav on document.keydown:
    ArrowRight / PageDown / Space → next
    ArrowLeft  / PageUp           → prev
    Home → first; End → last
    F → toggle fullscreen
  Single handler is swapped via the _paintEmbedDeck._kbd sentinel
  so re-renders (e.g., the refresh-error path) don't stack
  listeners. Form-element targets (none today, future-proof) are
  ignored so the embed doesn't eat typing.
- New _toggleEmbedFullscreen helper calls
  documentElement.requestFullscreen() / exitFullscreen with a
  graceful catch.

Touch swipe, autoplay (?autoplay / ?loop / ?interval), and
prefers-reduced-motion respect are deferred — they fit naturally
on top of this scaffold whenever we want them.
2026-06-01 15:25:07 -04:00
thePR0M3TH3AN 425e734238 Template browser: drop redundant Preview button from cards
Both card builders (buildFeedCard + buildResultCard) had a
dedicated "Preview" button on the action row that just called
openDesignPreview(row). The whole card already has a click
handler doing exactly that, so the button was redundant — it
just added visual noise next to the Use/Fork (or premium-unlock)
CTA.

Removed the prevBtn from both builders. Existing card-click
behavior unchanged, so the preview still opens on click; the
action row now has only the reactions + the action button(s)
specific to that card state.
2026-06-01 15:20:15 -04:00
thePR0M3TH3AN 9673d7d39f External embed: subtle transparency checker in preview pane
Swapped the embed code generator's live-preview backdrop from
the dev-time bright magenta/yellow checker to the standard
Photoshop-style light-gray/white transparency indicator
(#e5e7eb / #ffffff, 14×14 tiles). Reads as "this is where
transparency shows through" without screaming.
2026-06-01 10:55:36 -04:00
thePR0M3TH3AN 02d47214f3 External embed: ship a minimal transparency baseline page
src/transparency-iframe.html is a tiny html+body=transparent
stub we serve at /transparency-iframe.html so dev hosts (and
future debug tools) can include it via iframe to confirm that
cross-origin iframe transparency is working in the target
browser without involving any of the Artstr embed JS.

Validated end-to-end:
- srcdoc iframe → magenta shows through ✓
- cross-origin minimal page → magenta shows through ✓
- cross-origin Artstr embed → magenta shows through the
  Computer_Icon's transparent ring ✓

What looked like opaque white on the stress-test host was the
artwork's natural fill + the host's pink/yellow checker reading
as noise rather than see-through. Magenta on a flat backdrop
made the transparency unambiguous.
2026-06-01 10:54:49 -04:00
thePR0M3TH3AN af39a4ec71 External embed: code generator uses ?embed= URL (universal)
The generator was emitting /embed/<id> URLs (the canonical clean
path from the spec). Vercel rewrites that to / in production —
but on hosts without SPA rewrites (including local dev with
python http.server, and many static hosts users will paste into
WITHOUT Vercel-style routing), it 404s. The Embed modal's live
preview showed a "404 — File not found" error on localhost.

Switched to the ?embed=<id> query form. Universal: works on any
host that serves the index.html at /, no rewrite dependency.
The published Vercel deploy supports both forms via the existing
rewrites, so the change is backward-compatible for production
consumers.

If anyone wants the clean /embed/<id> form they can hand-edit
the snippet after copying; the embed runtime accepts all three
forms (path, hash, query) on the receive side.
2026-06-01 10:45:00 -04:00
thePR0M3TH3AN 52c8539257 External embed: fix code-dialog modal visibility
openEmbedCodeDialog set modal.style.display = '' to show the
modal, which falls back to the CSS default `.modal { display:
none }` — so the dialog never appeared. Every other modal in the
app sets `'flex'` explicitly. Matching that pattern.

Also fixes the symptom "Embed button doesn't work / doesn't
copy anything" — the dialog was always present in the DOM but
invisible, so the copy buttons inside it couldn't be reached.
2026-06-01 10:38:33 -04:00
thePR0M3TH3AN 21725b35ff External embed: code generator dialog in preview pane
Per docs/EXTERNAL_EMBED_FEATURE.md §13 / §18 Phase 6 — the embed-
code generator now ships alongside Phase 1's viewer so authors
can grab iframe snippets directly from the community preview.

UX:
- New "Embed ↗" button in the preview pane's action row, between
  the share-link button and Use/Fork. Hidden for private rows
  (embed refuses them anyway); shown for locked-premium rows so
  designs can be embedded as watermarked-preview teasers pre-
  purchase.
- Click opens #embedCodeModal: a single-screen dialog with
  identifier selector, size preset, options checkboxes, live
  iframe preview, and the generated iframe HTML in a copy-ready
  textarea + Copy URL / Copy iframe code buttons.

Controls:
- Identifier: naddr (latest version, auto-updates) | nevent
  (snapshot, frozen to this exact event). naddr is the default
  but falls back to nevent when the row has no d-tag.
- Size: responsive 16:9 (recommended) | square 320 / 480 | fixed
  640×360 / 800×450 / 960×540 | custom WxH.
- Options: controls / author chip / "Open in Artstr" link /
  credits row — each toggled via a query param (controls=0,
  attr=0, open=0, credits=1).

The live-preview pane uses a pink/yellow checker background so
transparency in the embedded design shows through visually
(verifies that the iframe is actually transparent before the
user pastes the snippet on their own site).

Copy buttons use navigator.clipboard.writeText with a graceful
fallback toast on permission denial. The URL extracted from the
generated iframe's src=, so URL/iframe stay in sync.
2026-06-01 10:34:48 -04:00
thePR0M3TH3AN c9585c02be External embed: more aggressive html/body bg reset + debug log
Defensive: also setProperty('background-image', 'none', 'important')
and ('background-color', 'transparent', 'important') on both
documentElement and body. Catches any inherited background-image
that might be painting a pattern instead of a flat color.

Added a console.log('[embed] boot v3', { route }) at the top of
bootEmbedViewer so the user can confirm the new code is reaching
the iframe — if the log doesn't appear in the iframe's dev-tools
console, the browser cache hasn't refreshed yet.
2026-06-01 09:48:41 -04:00