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]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-05-19 14:10:02 -04:00
1 parent 52dbfae945
commit 861e17e3f9
1 file changed
+1908 -113
+1908 -113
View File
File diff suppressed because it is too large. Load diff