Commit Graph
141 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 4.7 27074b9fb6 QR layer: add Insert npub + Insert share link buttons (Phase B)
Two convenience buttons under the QR data textarea:

- Insert npub — async; pulls the signed-in pubkey via the Nostr
  extension (window.nostr.getPublicKey), encodes it to bech32
  npub, writes it into the QR data field. Toasts an error if no
  extension is present.
- Insert share link — async; only valid once the design has been
  published (checks state.editingDTag). Builds the same naddr /
  nevent shape that shareIdForRow produces for the community-
  browser Copy-share-link button:
    * naddr for replaceable events (kind 30000-39999) with a
      mode-prefixed d-tag (cover: / disc: / jewel: / customart: /
      disc-design: / slide: / deck: / pixelart:)
    * nevent fallback otherwise, using state.editingEventId
  Then wraps it as `${location.origin}/#/share/<id>` and writes
  it into the data field.

Both writes go through updateQrField('data', value) so undo /
history / the live preview pick them up the same way typing in
the textarea does.

TODO.md: mark QR Phase B as shipped.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 11:38:34 -04:00
thePR0M3TH3ANandClaude Opus 4.7 e7e3039748 Add collapsible sidebar with a chunky early-web toggle tab
A vertical handle tab on the seam between the sidebar and the canvas
toggles the side panel open / closed. State persists to localStorage
so the panel stays where the user left it across reloads.

Style choices to match the existing chunky early-web aesthetic:

- Hard right edge with a small 4px corner, no left border (visually
  fuses with the sidebar edge).
- Manual two-tone bevel via inset box-shadow — light top, dark
  bottom — for the old-school extruded-tab look, without using
  border-style: ridge/outset (which modern browsers render
  inconsistently).
- Monospace bold guillemet glyph: « when open, » when collapsed.
- Hover paints accent-blue; active state nudges 1px right (chunky
  click feedback).
- Dark-mode-aware bevel colours.

Positioning:

- Fixed at top: 24px so it aligns with the logo row instead of
  floating mid-screen on top of editor toolbars (the original
  mid-screen position obscured the pixel and vector editor
  toolbars).
- Height 44px — compact enough not to dominate the header.
- Slides smoothly with the sidebar via `left` transition matched
  to the grid-template-columns animation (220ms ease).
- When collapsed, the pixelEditorBar / canvasToolbar / toolPalette
  get a 26px padding-left so their leftmost buttons don't sit
  under the tab.

Trade-off worth noting: the responsive `minmax(340px, 420px)`
sidebar width has been replaced with a fixed `380px` because
grid-template-columns doesn't smoothly interpolate between
minmax() and a plain length. 380px is in the middle of the
original range and works on every viewport size we care about.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 10:48:44 -04:00
thePR0M3TH3ANandClaude Opus 4.7 a13ef22b92 Add image → pixelize (Phase D) and move it into the pixel editor
A new "From image…" button in the pixel editor toolbar opens a file
picker and then a pixelize modal. The modal previews source and
pixelized output side by side, with live updates as the user changes:

- W / H (4–128, matching the editor's resize cap; default = the
  editor's current dims so Apply lands as a drop-in replacement)
- Colors (2–64; index 0 reserved as transparent)
- Fit mode — Cover / Fit / Stretch

Algorithm:

- _pixelDownsampleImage draws the image onto an off-screen canvas
  with imageSmoothingEnabled=false; fit-mode math computes source
  and dest rects so Cover crops edges, Fit letterboxes with
  transparent, Stretch ignores aspect.
- _pixelMedianCut quantizes the downscaled pixels: collect opaque
  samples, recursively split the bucket with the widest single-
  channel range until N buckets, average each bucket → palette
  entry. Pixels with alpha < 128 become transparent (index 0).
- _pixelNearestPaletteIdx maps each opaque pixel to the nearest
  palette entry by squared RGB distance.

Apply replaces the editor's current pixel art — frames, palette,
and dims all get swapped to match the pixelized output. One
pixelSnapshot before mutating, so Ctrl+Z restores everything.
Standalone mode commits immediately.

Drive-bys in this commit:

- Move the From image entry out of the vector-canvas ▦ popup and
  into the pixel editor toolbar (per request — the editor is where
  the workflow lives).
- Fix the modal show bug: .modal CSS rule sets display:none, so the
  modal must be opened with style.display = 'flex' (matching the
  publish-confirm modal). The previous '' didn't override the rule.
- Drop the long hint paragraph under the colour picker — the SV
  square + hue strip + Add button are self-explanatory and the
  eyedropper has its own tooltip.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 10:33:44 -04:00
thePR0M3TH3ANandClaude Opus 4.7 130e0218d8 Add GIF export + animate multi-frame previews (Phase C5)
Inline GIF89a encoder — no vendored dependency. Pixel art is
already palette-indexed, which is exactly what GIF needs, so the
encoder hands frame index buffers straight to LZW and writes the
GIF structure directly.

Encoder:

- encodePixelArtToGif assembles a full GIF89a: header, global
  colour table sized to the smallest 2^(N+1) that fits the
  palette, NETSCAPE2.0 application extension for loop count
  (0 = infinite, 1 = play once), per-frame Graphic Control
  Extension with disposal=2 (restore to background) and
  transparent-colour-index=0 (matches our palette convention),
  Image Descriptor, LZW-encoded image data in ≤255-byte sub-
  blocks, trailer.
- _gifLzwEncode does standard variable-bit-width GIF LZW: codes
  start at minCodeSize+1 bits, grow to 12, dictionary resets via
  clear codes when full.
- exportPixelArtToGifBlob decodes RLE frames, nearest-neighbour
  upscales to the chosen scale (so cells stay crisp in the GIF),
  encodes, returns a Blob.
- Smoke-tested at 4×4, 8×8, and 32×32: `file` recognizes all
  outputs as valid GIF89a.

Export UI:

- Format picker at the top of the export menu: PNG / GIF. PNG-
  only controls (Transparent bg, Grid lines, Sprite sheet) hide
  when GIF is active. GIF uses the design's stored fps + loop.
- Refuses GIF export with a toast when the design has <2 frames.
- Filename includes the frame count, e.g. mysprite_32x32_6f_4x.gif.

Animated previews (publish modal + community browser + feed cards
+ profile page) — all four entry points share one preview helper,
so this lights up everywhere:

- renderPixelArtPreviewDOM detects multi-frame designs and animates
  via a requestAnimationFrame loop with an accumulator (correct
  pacing across dropped frames).
- Per-frame canvases are pre-rendered once and cached; the tick
  just blits the right cached canvas — no RLE decode per tick.
- The RAF loop self-stops on `cv.isConnected === false`, so closing
  a modal, swapping the preview panel, or scrolling a feed card
  out of the DOM ends the loop. No leaks.
- fps and loop come from the payload; non-looping animations run
  once and freeze on the last frame.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 10:08:40 -04:00
thePR0M3TH3ANandClaude Opus 4.7 20a389f0ee Add sprite-sheet PNG export (Phase C4)
Extends the existing Export PNG menu with a "Sprite sheet (all
frames)" checkbox that tiles every frame into one PNG. A layout
picker (Grid / Row) appears when sprite-sheet is on — Grid is the
default and uses a square-ish layout (ceil(sqrt(N)) columns); Row
is a single horizontal strip. Scale, transparent background, and
grid-lines options all apply to the sheet as a whole.

Filename includes the frame count and a _sheet tag — e.g.
mysprite_32x32_4f_4x_sheet.png — so game-engine tools can parse
the dims from the name.

renderPixelGridToCanvas(pixelArt, frameIndex) now takes an optional
frame index (defaults to 0, so every existing single-frame caller
keeps working unchanged). pixelExportCurrentPixelArt now packages
all frames + fps + loop, so the exporter sees the full animation.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 09:19:14 -04:00
thePR0M3TH3ANandClaude Opus 4.7 aebd86c11c Add onion-skin to the pixel editor (Phase C3)
A new "Onion" checkbox below Loop in the frame strip. When on, the
canvas draws the previous frame as a flat red tint at 30% opacity
and the next frame as a flat blue tint at 30% opacity, both behind
the editing frame in its real colours. Aseprite convention — flat
tints make the neighbouring frames immediately distinguishable from
the frame you're painting.

- Only renders while editing (skipped when overrideIndices is set,
  i.e. during playback — the playing frame stays unobscured).
- Only renders when there are neighbouring frames to show.
- Onion is editor-only state (like the grid toggle); not persisted
  to the design payload.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 09:12:09 -04:00
thePR0M3TH3ANandClaude Opus 4.7 b8e1a11605 Add playback + move the frame strip to a vertical left sidebar (Phase C2)
Playback:

- ▶ / ■ Play button toggles a requestAnimationFrame-driven playback
  loop that uses an accumulator, so framing stays correct across
  tab-switches and dropped frames.
- FPS input (1–60) and Loop checkbox, both saved with the design.
- Space key toggles play / stop while the pixel editor has focus.
- Clicking the canvas during playback stops it and resumes editing.
- Arrow keys, tool shortcuts, and any frame-op stop playback first
  so the canvas and `_pixelEdit.currentFrame` stay in sync. Frame
  switches via pixelSetCurrentFrame auto-stop playback for the same
  reason.
- Refuses to start with fewer than 2 frames.
- Playback frame is rendered via a new optional `overrideIndices`
  argument to renderPixelGrid — `_pixelEdit.indices` (and thus
  `currentFrame`) keep pointing at the editing frame the whole time.

Layout:

- The frame strip is now a vertical sidebar to the left of the
  canvas instead of a horizontal row underneath. Frame thumbnails
  scroll independently (overflow-y: auto on the thumb list), so
  the main art area never gets pushed when there are many frames.
- Strip layout, top to bottom: Play, FPS, Loop, the 3 frame-op
  buttons (+, ⎘, −) in a row, then the scrolling thumb column.
- Thumbs use `height: auto` so their aspect matches the grid:
  square grids → square thumbs, non-square grids → correctly
  proportioned thumbs (cells stay square in the preview).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 09:09:35 -04:00
thePR0M3TH3ANandClaude Opus 4.7 6dad832a8d Add multi-frame timeline UI to the pixel editor (Phase C1)
_pixelEdit now tracks frames[] with currentFrame index and the
animation's fps/loop fields; _pixelEdit.indices is a live reference
to the active frame's Uint8Array so every existing paint / fill /
shape / mirror code path keeps mutating the right frame with no
further changes. decodePixelArtFrames / encodePixelArtFrames round-
trip the whole frame set between JSON RLE and in-memory typed arrays.

A timeline strip below the canvas holds:

- + / ⎘ / − buttons to add a blank frame, duplicate the current
  frame, and delete the current frame (delete disables when only
  one frame remains).
- A scrollable row of frame thumbnails — each thumb is the frame's
  indices rendered at native size and scaled up with image-rendering:
  pixelated. Active frame has an accent-coloured border.
- Click-to-switch and HTML5 drag-reorder.
- Thumbnails refresh on stroke end, transform, resize, and frame ops.

Left / Right arrow keys cycle to the previous / next frame (wraps).

Transforms (flip / rotate / resize) now iterate over every frame so
dims stay coherent across the animation. Undo snapshots capture the
full frame set plus currentFrame; restoring an edit also restores
the editing frame so the user lands back where they edited.

Commit paths (closePixelEditor, pixelCommitIfStandalone,
pixelEditTearDown) all encode all frames + fps + loop. Old single-
frame saves decode correctly because decodePixelArtFrames falls back
to a one-element array.

sanitizePixelArtBlob caps at 32 frames as a defensive ceiling and
preserves fps / loop.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 08:44:40 -04:00
thePR0M3TH3ANandClaude Opus 4.7 2a8d0e3707 Import a published pixel-art design as a layer (Phase B)
The pixel-art tool button (▦) in the tool palette now opens a small
popup with three options, mirroring the disc-slot and deck-slide
patterns:

- Add blank pixel-art layer — original behavior (32×32 default).
- Browse community… — opens the existing Nostr browser filtered
  to nostrType='pixelart'. Selecting a design imports it as a new
  pixelart layer in the current design (rather than loading it as
  the current document).
- Import from file… — file picker for .json designs.

Plumbing:

- sanitizePixelArtBlob clamps dimensions to 4–128 (matches resize
  cap), trims palettes to 64 entries, defaults missing palettes to
  DEFAULT_PIXEL_PALETTE, decodes/re-encodes RLE so corrupt runs
  either coerce to something valid or get rejected.
- importPixelArtAsLayer accepts either a full design payload (with
  .pixelArt) or a raw pixelArt blob; rejected payloads toast an
  error rather than creating a broken layer.
- tryPixelArtImportRow intercepts community-browser selections
  while state._pixelArtImportMode is true — added alongside the
  existing tryDeckImportRow at the four selection sites.
- nostrClose handler clears state._pixelArtImportMode so closing
  the modal without picking doesn't leave the flag stuck on.
- Menu closes on outside click, Escape, and after picking — same
  pattern as the export-PNG popover.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 08:26:34 -04:00
thePR0M3TH3ANandClaude Opus 4.7 f20a90929d Add grid resize to the pixel editor (Phase B)
The dims badge in the pixel editor toolbar is now a button — click
it to open a popover with W / H number inputs, a row of preset
buttons (16 / 32 / 48 / 64 / 96 / 128), and an Apply button.

Resize behavior:

- Bounds clamped 4–128 per side. The cap is set to keep the
  serialized event comfortably under conservative relay limits;
  raise later if real-world art shows it's safe.
- Top-left anchor: existing pixels at (x, y) stay where they are.
- New cells = palette index 0 = transparent (Uint8Array zero-
  initializes; index 0 is the conventional transparent slot in
  DEFAULT_PIXEL_PALETTE).
- Oversize is cropped.
- pixelSnapshot is called before the mutation, so Ctrl+Z reverts
  the resize correctly — snapshots already capture {indices, w, h}.
- pixelCommitIfStandalone is called after, so standalone mode
  pushes a debounced global history entry.

The popover closes on Apply, outside click, Escape, the Done
button, or any tool / transform button click — same pattern as
the Export PNG menu.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-25 15:46:37 -04:00
thePR0M3TH3ANandClaude Opus 4.7 335afdab4b Integrate pixel-editor undo/redo with the top-level toolbar
Pixel editor undo/redo were a local stack with no redo and no
connection to the global design-history stack. Several gaps:

- No redo in the pixel editor.
- The pixel snapshot only captured `indices`, not `w`/`h` — so
  undoing a rotate left the editor with swapped dims and stale data.
- Standalone pixelart mode never pushed global history, so leaving
  the editor (or switching templates) lost the ability to undo
  pixel work from the global stack.
- The pixel toolbar had its own ↶ Undo button duplicating the
  top-level toolbar's ↶.

Changes:

- `pixelSnapshot` now captures `{ indices, w, h }` and clears the
  redo stack on every new mutation. `_pixelRestoreState` updates
  the dims badge alongside indices, so rotate-then-undo is correct.
- Added `pixelRedo` mirroring `pixelUndo`. `_pixelEdit.redo` lives
  on both editor inits.
- Top-level `#undoBtn` / `#redoBtn` are now context-aware: while
  the pixel editor is open they route to `pixelUndo` / `pixelRedo`;
  otherwise they route to the global `undo` / `redo`. Tooltips and
  disabled state flip accordingly via `updateUndoRedoUI`.
- Pixel-editor keydown handles Ctrl+Shift+Z and Ctrl+Y for redo.
- `pixelCommitIfStandalone` pushes one debounced global history
  entry per stroke / shape / transform (600ms window), guarded by
  `_pixelHistorySuppress` so pixel undo/redo and serialize-for-
  publish don't pollute the global stack.
- Removed the redundant local ↶ button from the pixel toolbar
  (the top-level button now drives it).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-25 15:37:19 -04:00
thePR0M3TH3ANandClaude Opus 4.7 f9f800985d Add line / rectangle / ellipse drawing tools (Phase B)
Three new shape tools in the pixel editor toolbar:

- ╱ Line (L): Bresenham line from drag start to current cell.
- □ Rectangle (R): outline by default; filled when the new "Fill" toggle
  is on.
- ○ Ellipse (O): inscribed in the dragged bounding box; outline /
  filled per the Fill toggle. Outline cells are those inside the ellipse
  with at least one outside 4-neighbour.

A new "Fill" checkbox lives next to Mirror Y in the toolbar; applies to
rect / ellipse only. Mirror X / Y still apply to all shape tools.

Live-preview during the drag works by taking a one-shot snapshot of the
indices on pointerdown, then on every pointermove restoring from the
snapshot and re-applying the shape — the in-progress preview never
accumulates. One in-editor undo entry per shape (snapshot taken on
pointerdown), so Ctrl+Z removes the whole shape, not individual cells.
Standalone mode commits the final shape on stroke end so autosave /
publish always see it.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-25 15:21:05 -04:00
thePR0M3TH3ANandClaude Opus 4.7 06af73aeff Add PNG export to the pixel editor (Phase B)
A new "Export PNG ▾" dropdown in the pixel editor toolbar, available in
both the embedded-layer and standalone editing modes:

- Scale: 1× / 2× / 4× / 8× / 16× / 32× (3-column grid).
- Transparent background on/off (index-0 transparent cells stay
  transparent in the PNG when on; a white fill is laid down first when
  off).
- Optional 1-px grid line overlay at ≥ 2× scale.
- Filename is "<title>_<W>x<H>_<scale>x.png"; toast confirms output
  dimensions.

Self-contained <canvas> → toBlob('image/png') pipeline. Reads the
editor's current working grid for both layer-edit and standalone modes,
so you can export while you're still editing. Settings persist for the
session.

Menu dismissal is strict: opens only on the Export button, closes on
outside click, outside pointerdown (catches drag-starts), Esc, picking
a pixel tool, hitting Done, and after Download. Fixed a default-open
bug where the .pixelExportMenu CSS display:flex was beating the [hidden]
attribute via specificity tie + later-rule-wins — switched to a class-
toggled open state.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-25 15:14:55 -04:00
thePR0M3TH3ANandClaude Opus 4.7 07fcde0b97 Fix pixel editor colour picker + close layout gaps
The OS <input type="color"> popup eyedropper and the standalone EyeDropper
API are both broken on the user's Brave build — they return R=NaN /
always-white sample regardless of where you point. Replace them with
controls that don't touch any OS picker:

- New in-app HSV picker: Saturation/Brightness square + Hue strip drawn
  to canvases; pure sRGB math, no OS calls. The cursor position IS the
  picked colour. Render is wrapped in try/catch so SES lockdown (e.g.
  MetaMask) freezing canvas helpers can't break the commit path.
- New 32x32 preview swatch next to the hex input, updated by the
  picker — gives unambiguous visual feedback of the pending colour.
- A typeable #rrggbb hex field with live validation (red border on
  invalid), syncs both ways with the picker.
- A "Pick from screen" button backed by the Screen Capture API: take a
  one-frame screenshot of a chosen surface, show it in our own overlay,
  click any pixel to sample. The capture stream is kept alive for the
  duration of the pixel-edit session so subsequent picks don't re-prompt
  for permission (with a Stop button to end the share early). Auto-
  released when the editor closes.
- Drop the OS <input type="color"> swatch and "More options" disclosure
  entirely; they only confused users on browsers where they don't work.

Also close the editor layout gaps: in pixel-editing / template-pixelart
modes, .previewShell padding goes to 0, #pixelEditorView is height:100%
min-height:0 so it reaches the screen edges, the inner canvas wrap
padding tightens to 12px, and the fit-zoom reserved margin tightens to
28px so the canvas auto-fits at a higher integer zoom in the freed
space.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-25 09:41:04 -04:00
thePR0M3TH3AN 8483718abc Keep canvas size pills on one row 2026-05-25 08:05:45 -04:00
thePR0M3TH3AN af3bc03c71 Wrap publish event IDs in sidebar 2026-05-24 15:42:12 -04:00
thePR0M3TH3AN 866d59e7f3 Improve layer panel group controls 2026-05-24 15:38:12 -04:00
thePR0M3TH3AN 216f176aab Fix canvas group shift selection 2026-05-24 15:33:24 -04:00
thePR0M3TH3AN 237e6ffbf4 Add multi-layer transform box 2026-05-24 15:29:14 -04:00
thePR0M3TH3AN ad22985ca5 Add layer keyboard and reorder controls 2026-05-24 15:25:17 -04:00
thePR0M3TH3AN 86dd560587 Fix layer delete focus handling 2026-05-24 15:10:34 -04:00
thePR0M3TH3AN 279a529b92 Add layer panel range selection 2026-05-24 15:07:09 -04:00
thePR0M3TH3AN 1f2581e1d4 Render layer groups as collapsible folders 2026-05-24 14:59:12 -04:00
thePR0M3TH3AN fc4adb151d Add layer multi-select and groups 2026-05-23 19:57:19 -04:00
thePR0M3TH3AN 9d5c2a8e43 Prevent design object preview flex squish 2026-05-23 19:49:40 -04:00
thePR0M3TH3AN 473e7f6714 Center scaled design object previews 2026-05-23 19:47:47 -04:00
thePR0M3TH3AN 9423f6c2b3 Lock edit banner and Nostr widget heights 2026-05-23 19:45:22 -04:00
thePR0M3TH3AN 3836f4003a Match Nostr widget height to edit banners 2026-05-23 19:44:05 -04:00
thePR0M3TH3AN a610d020e9 Reserve Nostr account space for edit banners 2026-05-23 19:42:35 -04:00
thePR0M3TH3AN daa623271d Preserve transparent pixels in design objects 2026-05-23 19:35:51 -04:00
thePR0M3TH3AN 5e2f98ecd7 Add editable design object layers 2026-05-23 13:22:27 -04:00
thePR0M3TH3AN 8a2c146408 added pixel art editor 2026-05-22 16:06:36 -04:00
thePR0M3TH3ANandClaude Opus 4.7 b24511ad80 Add Pixel Art Phase A1 — editable pixel layers
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]>
2026-05-22 15:30:43 -04:00
thePR0M3TH3ANandClaude Opus 4.7 3e6e89e16f Add private (encrypted) publishing — Phases 1 & 2
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]>
2026-05-22 13:48:59 -04:00
thePR0M3TH3ANandClaude Opus 4.7 1a517c9552 Add on-canvas resize handles and expand the Text tool panel
- Selected layers (text / shape / image / color / QR) now show 8
  drag-resize handles — 4 corners + 4 edge midpoints. Edge handles are
  single-axis (resize one direction only); corner handles resize toward
  the corner with the opposite corner anchored. Resizing is computed in
  the layer's own rotated frame, so rotated layers behave correctly, and
  the aspect-lock toggle applies to corner handles only.
- Resizing a text box changes only its w/h (the text flow area) — the
  point size is never rescaled.
- Expand #textToolOptions to match the pen/shape panels: font family,
  point size, color, alignment, and bold/italic — sets the default for
  the next text box, and restyles the selected text layer live.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-22 07:48:24 -04:00
thePR0M3TH3ANandClaude Opus 4.7 9bf99f1abb Add Slide Decks & Presenter Mode (v1: Phases A–D)
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]>
2026-05-21 22:10:06 -04:00
thePR0M3TH3ANandClaude Opus 4.7 06c5ab6768 Draft slide-deck & presenter-mode feature plan
- 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]>
2026-05-21 14:30:59 -04:00
thePR0M3TH3ANandClaude Opus 4.7 c872b5210d Uniform shape strokes & corners on non-square layers
- Shapes now render into a viewBox whose height tracks the layer's
  aspect ratio (0 0 100 vbH), so the viewBox->layer mapping is a uniform
  scale. Strokes stay even thickness and rounded-rect corners stay
  circular on any aspect — fixes the squished-stroke/corner bug class.
  Applied to both the SVG renderer and the Canvas/PDF/JPEG exporter.
- Clip/mask to a rounded-rect no longer squishes its corners on a
  non-square clipped layer: clipShapeToPathD pre-distorts rx/ry.
- Removed a dead empty if-block in the export clip path.

Square layers render identically to before; circles/polygons still
fill the box (intended bounding-box behavior).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-21 13:47:03 -04:00
thePR0M3TH3ANandClaude Opus 4.7 2298512dce Optional background image, disc-image placement fixes, browser polish
- Publishing no longer requires a background image: cover/jewel/disc
  designs publish with layers-only content. "Save design (JSON)" works
  for layer-only disc designs (removed a silent return + a payload throw).
- Disc background image now lands correctly: renderDesignerDisc() runs
  after the designer view is laid out, and the publish/feed preview uses
  the same cover-pan math as the editor instead of a naive translate(%).
- Publish-modal preview no longer squishes the disc vertically (dropped
  axis-independent max-width/height clamps; fitMiniPreview scales it).
- Template browser: "Fork off!" button renamed to "Use/Fork".

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 22:44:55 -04:00
thePR0M3TH3ANandClaude Opus 4.7 159f60385a Update favicon
Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 22:06:50 -04:00
thePR0M3TH3ANandClaude Opus 4.7 8d07c81667 Support-the-dev publish flow, clip-to-shape fix, rebrand assets
- After a successful publish, a "support the developer" prompt asks if
  the user likes the project and offers to zap the dev — resolves the
  developer's Lightning address from their Nostr profile and hands off
  to the existing tip/zap modal. Shown once per session.
- Fix clip/mask to a custom vector (path) shape: the <clipPath> wrapped
  the path in a <g>, which browsers drop, so path-shape clips silently
  failed. Moved the transform onto the <path> itself.
- A shape used as a clip/mask source now renders no fill or stroke of
  its own (editor, export, and publish preview) so the clipped layer
  shows through cleanly.
- New animated logo (cropped tight, resized), Designer-tab icon, updated
  slogan, GitHub URL, and an artstr.org footer link.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 22:05:55 -04:00
thePR0M3TH3ANandClaude Opus 4.7 ef7d4803f7 Text/Shape/Hand tools, Designer-tab restructure, rename to Artstr Studio
- 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]>
2026-05-20 15:30:46 -04:00
thePR0M3TH3ANandClaude Opus 4.7 183e3b16d3 Unified canvas workspace, docked corner widget, feed flicker fix
- Canvas area is now a docked grid: full-height left tool palette and
  right options panel, full-width bottom zoom bar, all flush and
  sharp-edged with the scrollbar above the zoom bar.
- Nostr login widget docked flush into the top-right corner to match.
- Thinner scrollbars throughout (~half the default).
- Template browser no longer flashes while assets stream: feed
  re-renders are throttled (the FEED_PROGRESS_DEBOUNCE_MS path that
  was declared but never wired up).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-20 11:16:37 -04:00
thePR0M3TH3ANandClaude Opus 4.7 16c1577af4 Pen & pencil tools, unified vector editing, sidebar polish
- 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]>
2026-05-20 10:32:28 -04:00
thePR0M3TH3ANandClaude Opus 4.7 162da4b9e4 QR code layers, combined-wrap panel, profile-close fix, TODO
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]>
2026-05-19 22:09:15 -04:00
thePR0M3TH3ANandClaude Opus 4.7 d0ba9587bc Undo/redo, canvas-fit zoom toolbar, off-canvas veil, scroll-into-view
Undo / redo (Phases A-C per docs/UNDO_REDO_FEATURE.md):
- Memory-only stack of structuredClone'd state snapshots, capped at
  50 entries. UNDO_SNAPSHOT_FIELDS lists exactly which editor-state
  fields participate (appMode, templateMode, savedMetaByMode, meta,
  cover dimensions + artwork, discs / discMeta / spine labels, jewel,
  customArt, discDesign, layers, selectedLayerId, editing-in-place
  context, display toggles). Community / cache / clipboard state is
  intentionally excluded.
- pushHistory captures pre-change state; pushHistoryDebounced
  collapses rapid edits (slider drags, typing, color-picker scrubs)
  into a single history entry per debounce window. _isRestoring
  guard prevents render-driven re-pushes during restore.
- Restore re-runs the sync/render pipeline so input fields, mode
  tabs, and the editing banner all reflect the rolled-back state.
- Undo / Redo buttons in the topbar with disabled state, snapshot
  label hints in the title attribute, and Ctrl+Z / Ctrl+Shift+Z /
  Ctrl+Y keyboard shortcuts that defer to the browser's native
  text-input undo whenever an editable element has focus.
- Push triggers wired across layer lifecycle (add / delete /
  duplicate / cut / paste / paste-all), drag-end on pointerup, every
  shape paint property (fill, gradient stops / angle / center /
  radius, stroke type / color / width / dash, kind-specific params,
  per-element SVG fill / stroke / stroke-width), clip kind /
  inline-shape params / layer-ref selection, custom-path / SVG
  upload modal confirm in create / edit / edit-clip modes, mode
  switches (Template <-> Disc Designer plus cover / disc / jewel /
  customart), close project / clear images / load project / fork /
  enter and exit edit mode, slot artwork loads + transform inputs
  (cover slots, combined wrap, disc design, jewel inserts), spine
  labels, metadata text fields (debounced) and category / language
  selects, Custom Art canvas preset / size / background, dimension
  inputs (panelW / trimH / spineW / bleed / safe), preset selector,
  layer reorder (forward / back), and the display-options toggles.

Canvas zoom + scroll toolbar:
- New always-visible toolbar below the preview area with zoom-out,
  zoom slider (20-200%), zoom-in, percent readout, Fit, and 1:1.
  Sits as a non-scrolling sibling at the bottom of main's flex
  column so it stays on screen regardless of canvas scroll.
- Default zoomAuto = true. computeFitZoom reads previewShell client
  size and picks a percent that lands the sheet inside the viewport
  with ~32 px breathing room. Window resize recomputes if auto-fit
  is still active. Touching any zoom control turns auto-fit off;
  hitting Fit turns it back on.
- main is now a flex column with max-height: 100vh; previewShell
  flex: 1 + overflow: auto + min-height: 0. The previous
  min-height: calc(100vh - 112px) is gone since flex sizes correctly.

Off-canvas veil:
- A transparent .canvasVeil child inside .sheet with inset:0 and a
  giant 9999px box-shadow at 62% preview-gray fills everything
  outside the artboard bounds. Layer content extending past the
  edge sits visually under the shadow so it looks faded, making
  the canvas edges crystal clear. Dark mode uses a dark-tint
  shadow. Print / JPEG export / export-print mode all hide the
  veil so saved output preserves bleed and off-canvas content.

Selected-layer scroll-into-view:
- Clicking a layer on the canvas now scrolls the sidebar's
  "Selected layer" panel into view (block: 'nearest', so it
  no-ops when already visible). Sidebar list clicks don't
  trigger the scroll since the panel is already in the user's
  view.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-19 16:13:42 -04:00
thePR0M3TH3ANandClaude Opus 4.7 a02c562a9f Custom Art metadata simplification, JPEG mask fix, README rewrite
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]>
2026-05-19 14:37:36 -04:00
thePR0M3TH3ANandClaude Opus 4.7 861e17e3f9 Vector shapes (Phases A-E) + project polish
Vector shapes feature (per docs/VECTOR_SHAPES_FEATURE.md):

Phase A - Shape primitive layers (rect, rounded-rect, circle, ellipse,
triangle, polygon, star, line) with a 0-100 viewBox coord system that
renders on all six surfaces (editor canvas + five preview renderers)
plus Canvas2D export. Solid fill, kind-specific controls (corner
radius, sides, star points, inner ratio, line stroke width). Layer
data is purely additive in JSON; old clients skip unknown shape kinds.

Phase B - Linear + radial gradient fills with two-stop pickers. SVG
emits via <defs><linearGradient>/<radialGradient>; canvas via
createLinearGradient/createRadialGradient. Angles follow CSS
linear-gradient convention. Switching fill types preserves the most
relevant color.

Phase C - Solid strokes on all non-line shapes. Color, width (in
shape-local viewBox units), dash style (solid / dashed / dotted) with
auto-scaled dash patterns. SVG emits stroke attributes; canvas uses
strokeStyle + setLineDash.

Phase D - Custom SVG / full-SVG layer via file upload:
- File picker (.svg, image/svg+xml) replaces a paste textarea.
- Detects single <path d=…> vs full <svg>…</svg>; single paths land as
  kind:'path' with editable fill/stroke, multi-element SVGs land as
  kind:'svg' with fills baked in.
- Sanitizer strips <script>, <foreignObject>, <iframe>, <embed>,
  <animate>, <image>, <a>, all on* event handlers, external
  hrefs/xlink:hrefs, and javascript:/external-url style entries.
- Canvas export serializes the sanitized inner into a blob URL, loads
  via Image, drawImage.
- Per-element editor: a list inside the shape panel exposes every
  paintable child (path / rect / circle / ellipse / polygon /
  polyline / line) with fill + stroke color pickers and a stroke-width
  input. Auto-applies a viewBox-relative default stroke width when
  stroke is first set so strokes are visible at any zoom. Layer-level
  fill / stroke panels are hidden for SVG imports (per-element is the
  canonical control).

Phase E - Clip / mask on any layer type:
- New optional `clip` field with two flavors: inline-shape (clip to a
  primitive defined inline) and layer-ref (clip to another shape
  layer's geometry in the same target).
- Clip source dropdown in the selected-layer panel offers rect /
  rounded-rect / circle / ellipse / triangle / polygon / star / custom
  path / From another layer. Kind-specific param inputs show on demand.
- DOM rendering uses a shared hidden SVG defs container with
  <clipPath clipPathUnits="objectBoundingBox"> entries, referenced via
  clip-path: url(#clip-<layerId>) on each layer wrapper.
- Canvas export builds a Path2D with a DOMMatrix transform mapping
  0-100 coords to the layer's pixel bounds, then ctx.clip() before
  drawing the layer contents.
- Cycle detection: if A clips to B and B clips back to A, both fall
  back to unclipped instead of looping.
- Single-path-SVG clips reuse the upload modal in an edit-clip flow
  that rejects multi-element SVGs with an inline error.

Phase A bonus polish:
- Shapes spawn 1:1 (min(bounds.w, bounds.h) * 0.4).
- Session-wide aspect-ratio lock (default on) with a 🔒/🔓 toggle
  between the W/H inputs in the selected-layer panel. Typing in one
  dimension updates the other proportionally on every layer type.

Per-mode metadata isolation:
- state.savedMetaByMode parks Template-mode and Disc-Designer-mode
  metadata separately. swapMetaForAppMode runs before any appMode
  flip so loading a template in disc designer doesn't overwrite the
  designer's title / category / identifiers. omdbApiKey stays sticky
  across modes as a per-user preference.

Strict per-sub-mode layout isolation:
- isLayerForCurrentMode no longer treats target='page' as a universal
  wildcard. Each layer belongs to exactly one sub-mode by its target.
- Removed the "Whole page" entry from the layer-target dropdown.
- migrateLegacyLayerTargets retargets legacy page/empty layers to the
  current sub-mode's primary target on load. Wired into all four
  mode-specific load paths and the legacy full-project path.
- Fixes the bug where case-cover artwork was rendering misaligned in
  the disc-sheet preview.

Layer clipboard:
- Session-only clipboard supports cross-sub-mode and cross-tool paste
  with retargeting. Copy / Cut on selected layer + Copy all / Paste
  in the panel header. Paste retargets to the current sub-mode's
  primary target and clamps positions so a layer copied from a wide
  cover doesn't land off-canvas in a 4.6" disc.

UI polish:
- Hide unused artwork-mode sidebar controls: cover full-wrap mode
  hides the separate Back / Spine / Front controls and vice versa,
  via the existing body.mode-combined / mode-separate classes.
- Category-aware identifier visibility: only show IMDb / UPC /
  TheTVDB / MusicBrainz fields that fit the chosen category. Custom
  art hides all four.
- Added "Custom art" to the canonical mediaCategories list and
  populated both the template and designer category selects from it
  (the template select was previously hardcoded).
- Selected-layer panel hides entirely when nothing is selected.
- Jewel spine-text details block hides until at least one jewel
  insert has artwork.
- Close project / disc-designer reset / clear images all use a
  shared blankMeta() helper that covers UPC / TheTVDB / MusicBrainz
  (the previous reset path was missing them). Close project also
  resets state.customArt to its defaults so the background color
  doesn't carry over.
- Disc Designer mode now has a proper add-layer panel (Add image
  with URL + Copy / Add text / Add color block / Add shape menu) in
  addition to the layer list.
- Project file loads route through loadProjectFromText regardless of
  which "Load from file" button was used; an info toast announces a
  mode switch when one is needed.

Publish flow:
- New publish-confirm modal renders a live design preview, a
  metadata table (Type / Title / Category / Language / IMDb / UPC /
  TheTVDB / MusicBrainz / Author), and an edit-mode tradeoff banner.
  Replaces the bare confirm(). Errors render inline.
- Removed the legacy "Copy Nostr event" button — only "Copy event ID"
  remains, and it stays hidden until something has been published.

Toast system:
- All 41 native alert() calls swapped for in-app toasts (toastError
  with a red border for failures, toastSuccess with the accent color
  for publish/copy confirmations). Auto-dismiss after a few seconds;
  manual close button on each.

NIP-57 zap tab:
- _feedPagination grew a Zapped entry so the new tab can render
  without crashing the rerender pipeline.
- fetchZapsForEvents now runs chunks in parallel and streams a
  debounced rerender on every ingested receipt, so totals appear
  within a second or two instead of taking up to ~28 s.
- invalidateFeedCache clears the zap cache too so Refresh re-queries
  receipts.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-19 14:10:02 -04:00
thePR0M3TH3ANandClaude Opus 4.7 52dbfae945 Strict layout isolation + per-mode metadata + layer clipboard
Strict layout isolation:
- isLayerForCurrentMode no longer treats target='page' or empty as a
  universal "show everywhere" wildcard. Each layer now belongs to
  exactly one sub-mode (cover / disc-sheet / jewel / customart) by
  its target string.
- Removed "Whole page" from the layer-target dropdown so users can't
  create new layers that leak across sub-modes.
- migrateLegacyLayerTargets retargets legacy page/empty layers to the
  current sub-mode's primary target on load (cover→cover-trim,
  disc→disc-top, jewel→jewel-front, customart→canvas). Wired into all
  four mode-specific load paths and the legacy full-project path.
- Fixes the bug where case-cover artwork was rendering misaligned in
  the disc-sheet preview after switching sub-modes.

Per-mode metadata isolation:
- state.savedMetaByMode parks Template-mode and Disc-Designer-mode
  metadata separately. swapMetaForAppMode runs before any appMode
  flip (user-driven switchAppMode, loadProjectFromText auto-switch,
  importDiscDesignFromPayload's openDesigner path), so loading a
  template file in disc-designer mode no longer overwrites the
  designer's title / category / identifiers.
- omdbApiKey is treated as a per-user preference and stays sticky
  across mode switches.

Layer clipboard:
- Session-only clipboard supports cross-sub-mode and cross-tool
  paste with retargeting. Selected-layer panel gains Copy and Cut
  buttons; the Layers panel grows a Copy all / Paste header row with
  a status hint ("Clipboard: N layers from X").
- Paste retargets every item to the current sub-mode's primary
  target (or designer-disc in Disc Designer) and clamps positions
  so a layer copied from a wide cover doesn't land off-canvas when
  pasted into a 4.6" disc. Each pasted layer gets a fresh id and z;
  the last one becomes the selection.
- Resets on reload — not persisted to localStorage in v1.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-18 19:55:57 -04:00
thePR0M3TH3ANandClaude Opus 4.7 556d5c5b09 Route both file-load buttons through the smart project loader
Loading a project file picked from the Disc Designer's "Load from
file" button used to call importDiscDesignFromPayload directly, which
silently no-ops on a template payload. Now both the template and
disc-designer file inputs run the JSON through loadProjectFromText,
which detects the payload type and switches state.appMode to the
matching tool when needed.

Also fixes a gap where importDiscDesignFromPayload's openDesigner
branch updated state.appMode but never called syncModeTabsUI, so the
mode-tab indicator stayed stuck on the previous tool after loading a
disc design from template mode.

An info toast announces the switch when one happens, so the user
isn't surprised by the sidebar reconfiguring under them.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-18 18:39:59 -04:00