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]>
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]>
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]>
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]>
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]>
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]>
A new `pixelart` layer type: a palette-indexed, RLE-encoded grid stored
in the project JSON, rendered crisp (image-rendering: pixelated) across
the editor, preview-DOM renderers, and the export canvas.
A dedicated #pixelEditorView with pencil / eraser / fill / eyedropper
(drag-paint with line interpolation), a palette panel (pick / add
colours), grid toggle, mirror X/Y, zoom, and per-stroke in-editor undo.
Reachable via a tool-palette button (adds a 32x32 layer + opens the
editor) and an "Edit pixels" button / double-click on a pixel layer.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Phase 1 — publish a design encrypted, readable only by the author:
- Crypto helpers: encryptProjectEnvelope / decryptProjectEnvelope —
AES-256-GCM over gzipped JSON, AES key NIP-44 key-wrapped to the
author (NIP-04 fallback), via a nostrEncryptCapability() probe. All
in-browser, no new dependencies.
- publishEncryptedDesign: a feed-invisible kind:30078 event — random
persistent d-tag, only d / alt / encrypted tags, no casewrap tag.
- Publish-confirm modal gains a Public / Private control with the
privacy warning and a capability gate; wired into the template and
disc-design publish paths.
- My Designs decrypts the author's own private events and shows a
lock badge; buildRowsFromEvents skips any stray encrypted event so it
can never surface in the public feed.
- state.visibility / state.privateDTag — restored on load, cleared on
Close project, autosaved.
Phase 2 — flip a published design's visibility in place:
- Owner-only Make Public / Make Private button on the preview page;
republishDesignVisibility re-publishes under the same d-tag with the
opposite wrapper (NIP-33 replace), behind a confirm that states the
public-to-private caveat.
Spec status documented in docs/PRIVATE_PUBLISHING_FEATURE.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
The on-canvas resize handles, text-box-resize behaviour, and expanded
Text tool panel all shipped in 1a517c9; collapse the section to a
shipped note and keep it open for future editor-UX items.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- 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]>
On-canvas resize handles for all layer types, a constraint that text-box
resize never rescales the point size, and expanding the existing
#textToolOptions panel like the pen-tool panel.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for a Nostr-native pixel-art editor: an editable `pixelart` layer
type (palette-indexed + RLE grid stored in JSON, raster as export-only),
a dedicated Pixel Editor canvas-area view, a standalone casewrap-pixelart
Designer-tab design importable into any design as a layer, PNG export,
and a phased path to animation frames + GIF.
Includes a Size limits section: relays cap event size (commonly 64 KB),
so publishing is gated on a measured event-size budget derived from the
relays' NIP-11 max_event_size (min across the user's set, 64 KB
fallback) — soft warning at ~50%, hard block at ~90%, with a live size
readout in the editor. Encoder stores the smaller of RLE / base64 to
bound the worst case; rough size table per grid dimension included.
Also index the spec in TODO.md under drafted-but-unbuilt features.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Replace the thin single-action-per-layer model with real interactive
logic:
- Cards gain onEnter/onExit chains; layers gain an onClick chain. Each is
an ordered action chain, not a single link.
- v1 action set: goto-card, go-back, open-url, show-layer, hide-layer,
toggle-layer — reveals, secret panels, multi-step buttons, branching.
- New action-builder surface: a no-JSON panel with reorderable rows,
id-based target pickers, and authoring warnings.
- Runtime tracks a per-card layer-visibility map (reset on re-entry) and
executes chains; action overlay regions are focusable buttons.
- Safety: onEnter/onExit cannot navigate (no enter->goto->enter loops);
chains capped and non-recursive; unknown action types ignored.
- Re-phased A (format) -> B (behaviour authoring) -> C (runtime) ->
D (route) -> E (stack logic: variables/conditionals, future tier).
Interactivity stays a declarative, interpreted attribute of existing
layers — no new layer types, no HTML/JS, no eval.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- Aspect-ratio changes now re-fit cards via the existing import-letterbox
remap instead of stranding layers off-canvas; imports target the
stack's aspect, not a hardcoded 16:9.
- Clarify how the shared deck renderer is fed for stacks (dims from
stack, background from card) and startCard deletion fallback.
- Viewer: action regions are focusable buttons (Tab/Enter, screen
readers); empty-space clicks are no-ops; goto-card keywords clamp at
the ends; touch taps act as clicks; persist the edited card before
launching; encrypted-stack locked state on the /p/ route.
- Fix the hit-testing risk note (rotation is exact; only clip-masked
layers fall back to a bounding box) and add an accessibility row.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for "HyperCard for Nostr" — fullscreen interactive card stacks built
as a focused extension of the Slide Deck: a new templateMode 'stack'
reusing the deck card-container engine, with a per-stack aspect ratio,
optional per-layer actions (goto-card / open-url), transparent hotspots,
and a fullscreen interactive viewer with click routing. Declarative
safe schema only — no HTML/JS. Phased A-D plus a /p/<naddr> viewer route.
Also index both PRIVATE_PUBLISHING_FEATURE.md and STACKS_FEATURE.md under
the drafted-but-unbuilt features in TODO.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Spec for publishing any design to Nostr encrypted, readable only by the
author (Phase 1 "private to me"). Covers the AES-256-GCM + NIP-44/NIP-04
key-wrap envelope, leak-free tagging (no casewrap feed tag, random d-tag),
progressive decryption in My Designs, an explicit threat model, and a
Phase 0 validation spike. Phases 2-4 (public/private toggle, recipients,
NIP-59/Blossom) sketched as future work.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Build a Nostr-native presentation system on top of the existing canvas
editor:
- Slide (templateMode 'slide') — a 16:9 Custom Art canvas with speaker
notes; publishes/forks/loads as casewrap-slide.
- Deck Builder (templateMode 'deck') — a Template-tab container with an
inline ordered slide list. Slide-sorter view: add blank, import from
Nostr or file (non-16:9 imports letterboxed), duplicate, delete,
drag-reorder, inline-rename; edit a slide in the canvas editor with
write-back. Self-contained save/load/publish/fork as casewrap-deck;
deck feed-card and publish previews show the first slide + count badge.
- Deck theme — deck-level font + background, render-time composition,
per-slide ignoreDeckTheme opt-out.
- Presenter Mode — full-screen runtime: themed slide, notes, next
thumbnail, counter; keyboard/click/on-screen nav; Presenter ⇄ Audience
view toggle (slide-only full-screen with auto-hiding chrome). Launches
from the deck tool palette and a deck's community-browser preview.
Supporting changes:
- New categories: slide ("Slide design") and slide-deck ("Slide deck");
both use the free-form Custom tag field, not media identifiers.
- Swap the Template/Designer tab icons; 50% larger.
- Remove the IMDb/OMDb metadata import (button, handler, omdbApiKey);
the imdbId field stays.
- Update README, SLIDE_DECK_FEATURE.md status, and TODO.md.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- docs/SLIDE_DECK_FEATURE.md: phased spec for Slide Designer (16:9
canvas + speaker notes), Deck Builder (embedded ordered slides,
slide-sorter view, deck theme), and Presenter Mode (single-window
v1, dual-window deferred to Phase E). Phases A-D with ship gates.
- Select tool icon changed to a rotated arrowhead so it reads as a
cursor, not a play button (distinct from the planned Present icon).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
- 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]>
- 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]>
- 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]>
- 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]>
- 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]>
- Pen/pencil tools (Phases A-E): left tool palette, contextual options
panel, freehand pencil strokes, Bézier pen paths, anchor/handle editing,
per-layer hidden flag with eye toggle in the layer list.
- Unified vector editing: the pen panel edits any selected shape's
fill/stroke; a "Make editable path" action converts primitives and
single-path SVGs into editable pen paths (SVG d-string parser + arcs).
- Stroke no longer clips at the shape's bounding box (overflow:visible).
- Toolbars anchored outside the scroll area so they stay locked on screen.
- Keyboard shortcuts: Delete, arrow-nudge, [/], Ctrl+D, Ctrl+C/X/V.
- Collapsible sidebar panels, collapsed by default (Layout stays open);
selecting a layer auto-opens the Layers panel.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
docs/PEN_TOOL_FEATURE.md: pen and pencil tools that author
shape.kind:'path' layers rather than a new raster type, so drawn
paths inherit the existing fill / gradient / stroke / clip / opacity
/ transform / undo / export / Nostr plumbing for free.
Decisions locked with the project owner:
- Pencil strokes are raw polylines, no smoothing (early-web look).
- Only pen paths are node-editable; pencil paths are not.
- A closed pen path auto-fills with a solid accent color on close.
- Pen + pencil work in Disc Designer mode too — the overlay attaches
to #designerDiscWrap and coordinate capture branches on
isDesignerLayer, mirroring startLayerDrag.
Plan: a state.editorTool tool-mode system (select / pen / pencil), a
left floating tool palette and a right contextual options toolbar
matching the zoom-toolbar styling, a pointer-capturing pen overlay,
shape.pen.nodes metadata for re-editable pen paths, and a new
layer.hidden flag for mask-source shapes. Five phases: A tool-mode +
toolbars, B pencil, C pen create, D pen node editing, E visibility
flag + mask polish.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
QR code layer (Phase A):
- New `qr` layer type. Add QR button alongside Image / Text / Color /
Shape in every layer panel. Spawns 1:1 with a placeholder URL.
- Controls: data textarea, error-correction level (L/M/Q/H), quiet
zone, module + background colors, transparent-background toggle.
- Renders as crisp SVG on the editor and all five preview surfaces
(dark modules merged into one path); canvas/PDF/JPEG export draws
it cell-by-cell. Round-trips through JSON and Nostr publish/fork;
old clients skip the unknown type. Works with drag / resize /
rotate / opacity / aspect-lock / clip / clipboard / undo-redo.
Combined-wrap layer panel:
- In cover mode with Artwork mode "One combined wrap image", the
layer panel collapses the separate Back / Spine / Front panels
into a single "Cover trim area" panel — the wrap is one open
canvas, so the sub-regions aren't meaningful. The combined panel
still lists any pre-existing back/spine/front layers via a
matchKeys set so they aren't orphaned from the UI.
Profile modal close fix:
- closeProfilePage used history.back(), which navigated the browser
out of the app entirely when the profile was cold-loaded from a
shared /u/<npub> link (that entry being first in the tab's
history). Switched to history.replaceState to "/" — cleans the
URL in place without navigating or firing popstate. Matches how
closeNostrPreview already handles the /share/ URL.
Docs:
- Add TODO.md tracking deferred work: QR Phase B/C, IMDb-import
removal, profile-page polish, edit-flow polish, and the unbuilt
badges / collections / zap-gated / NWC-zaps features.
- Remove docs/UNDO_REDO_FEATURE.md and docs/QR_CODE_LAYER_FEATURE.md
(shipped) and docs/TMDB_AUTOFILL_FEATURE.md (declined).
- README: document QR layers, undo/redo, fit-to-view + zoom toolbar,
and the off-canvas veil; refresh the schema and docs references.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
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]>
Rewrite docs/UNDO_REDO_FEATURE.md to match what the codebase actually
contains today. Explicit snapshotted-vs-not field list covering
savedMetaByMode, customArt canvas, editing-in-place context, the full
layer schema with shape/fill/stroke/clip sub-objects, and discMeta /
spine labels. Trigger list organized by category (layer lifecycle,
geometry, paint, SVG per-element, clip, mode/structural, artwork
loads, metadata, custom-art canvas) with debounce windows per field
type. Switches storage from localStorage to memory-only for v1 to
avoid quota issues with our richer state. Adds an _isRestoring guard,
focused-input bypass for the keyboard shortcut, and 11 acceptance
criteria covering cross-mode meta swap, edit-in-place, clipboard
interaction, and the stack cap. Four implementation slices.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Custom Art project metadata:
- Hide the "Add Media Identifiers" panel when in Custom Art mode —
IMDb / UPC / TheTVDB / MusicBrainz don't fit wallpaper / banner /
logo work.
- Show a single optional Custom tag field in its place. Stored as
state.meta.customTag, round-trips through save/load, and emits to
Nostr as both an NIP-73-style 'i custom:<tag>' identifier and a
topical 't <tag>' tag (lowercased, hyphenated, capped at 64 chars).
- Hide the IMDb import button + hint in Custom Art mode.
- Auto-select the "Custom art" category the first time the user
enters customart mode without one set.
JPEG export mask fix:
- body.export-print > *:not(#exportPrintContainer) { display: none }
was hiding the global SVG <defs> container that holds <clipPath>
definitions for layer masks. With the defs invisible, every
clip-path: url(#…) reference resolved to nothing during print,
so the PDF that the JPEG flow re-rasterizes had unclipped layers.
Carve #globalClipDefs out of the exclusion selector and force
it to zero-size + overflow:hidden so it stays in the DOM but
takes no space. Direct "Print / Save PDF" was unaffected because
it doesn't use the export-print mode.
README rewrite:
- Brings the README in line with what the app actually does today:
Custom Art mode, vector shape primitives + gradients + strokes +
custom SVG upload with per-element editor, clipping/masking,
layer clipboard, profile pages at /u/:npub, Lightning tips with
WebLN auto-pay + NIP-57 zap tracking + Zapped tab, edit-in-place
via NIP-33 replaceable semantics, the publish-confirm modal, the
current schema version (5 / min 4), and the vendored qrcode lib.
Remove docs/VECTOR_SHAPES_FEATURE.md — the feature shipped.
Co-Authored-By: Claude Opus 4.7 <[email protected]>