diff --git a/src/index.html b/src/index.html
index 26afcb5..27bd736 100644
--- a/src/index.html
+++ b/src/index.html
@@ -2138,6 +2138,23 @@
.toolPaletteBtn:hover { background:rgba(127,127,127,.14); }
.toolPaletteBtn.active { background:var(--accent); color:#fff; }
.toolPaletteDivider { height:1px; background:var(--line); margin:3px 2px; }
+ .pixelToolWrap { position: relative; }
+ .pixelToolMenu {
+ position: absolute; left: calc(100% + 6px); top: 0; z-index: 40;
+ min-width: 220px; padding: 6px;
+ background: var(--panel, #fff); color: var(--ink);
+ border: 1px solid var(--line); border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(15,23,42,.18);
+ flex-direction: column; gap: 2px;
+ display: none;
+ }
+ .pixelToolMenu.open { display: flex; }
+ .pixelToolMenu button {
+ text-align: left; padding: 8px 10px; font-size: .85rem;
+ border: 0; background: transparent; color: var(--ink);
+ border-radius: 6px; cursor: pointer; font-family: inherit;
+ }
+ .pixelToolMenu button:hover { background: rgba(127,127,127,.14); }
.toolOptions {
grid-area: options;
display: flex;
@@ -3527,7 +3544,15 @@
-
+
+
+
+
+
@@ -9033,6 +9058,110 @@
openPixelEditor(layer.id);
}
+ // Sanitize an arbitrary `pixelArt` blob (from file or community payload)
+ // into a safe, clamped shape we can drop into a new layer.
+ function sanitizePixelArtBlob(pa) {
+ if (!pa || typeof pa !== 'object') return null;
+ const w = Math.max(4, Math.min(128, pa.width | 0));
+ const h = Math.max(4, Math.min(128, pa.height | 0));
+ if (!w || !h) return null;
+ const palette = (Array.isArray(pa.palette) && pa.palette.length)
+ ? pa.palette.slice(0, 64).map(c => (typeof c === 'string' ? c : '#00000000'))
+ : DEFAULT_PIXEL_PALETTE.slice();
+ const srcFrame = (Array.isArray(pa.frames) && pa.frames[0]) ? pa.frames[0] : { pixels: [] };
+ // decode then re-encode so we own the buffer and catch corrupt RLE runs.
+ const indices = decodePixelRLE(srcFrame, w, h);
+ const pixels = encodePixelRLE(indices);
+ return { width: w, height: h, palette, frames: [{ id: 'f1', pixels }] };
+ }
+
+ function importPixelArtAsLayer(payload, targetOverride = '') {
+ if (!payload || typeof payload !== 'object') {
+ toastError('That file does not look like an Artstr Studio design.');
+ return false;
+ }
+ // Accept two shapes: a full design payload (with .pixelArt) OR a raw
+ // pixelArt blob (width/height/palette/frames).
+ const blob = (payload.pixelArt && typeof payload.pixelArt === 'object')
+ ? payload.pixelArt
+ : (payload.width && payload.height && Array.isArray(payload.frames) ? payload : null);
+ const pa = sanitizePixelArtBlob(blob);
+ if (!pa) {
+ toastError('No pixel art found in that design.');
+ return false;
+ }
+ const target = targetOverride || $('layerTarget')?.value || primaryTargetForMode(state.templateMode || 'cover');
+ const bounds = targetBounds(target);
+ applyTargetMode(target);
+ const size = Math.min(bounds.w, bounds.h) * 0.45;
+ pushHistory('import pixel art layer');
+ const layer = {
+ id: makeLayerId(),
+ type: 'pixelart',
+ name: payload?.meta?.title ? `Pixel art — ${payload.meta.title}` : 'Pixel art',
+ target,
+ x: bounds.x + (bounds.w - size) / 2,
+ y: bounds.y + (bounds.h - size) / 2,
+ w: size, h: size,
+ rotate: 0, opacity: 1, z: nextLayerZ(),
+ pixelArt: pa
+ };
+ addLayer(layer);
+ toastSuccess('Pixel art imported.');
+ return true;
+ }
+
+ function openPixelArtImportBrowser() {
+ state._pixelArtImportMode = true;
+ $('nostrType').value = 'pixelart';
+ _feedState.filterNew = 'pixelart';
+ _feedState.filterPopular = 'pixelart';
+ _feedState.filterSearch = 'pixelart';
+ _feedState.filterMine = 'pixelart';
+ $('nostrModal')?.classList.add('open');
+ if (typeof switchModalTab === 'function') switchModalTab('New');
+ }
+
+ // Intercept a community-browser selection while pixel-art import mode is
+ // active: drop the chosen design in as a pixelart layer instead of
+ // loading it as the current document.
+ function tryPixelArtImportRow(row) {
+ if (!state._pixelArtImportMode) return false;
+ state._pixelArtImportMode = false;
+ try { closeNostrPreview(); } catch (e) {}
+ $('nostrModal')?.classList.remove('open');
+ if (row && row.payload) importPixelArtAsLayer(row.payload);
+ return true;
+ }
+
+ function pixelImportFromFile() {
+ $('pixelImportFile')?.click();
+ }
+
+ function handlePixelImportFile(e) {
+ const file = e.target.files && e.target.files[0];
+ e.target.value = '';
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ let payload;
+ try { payload = JSON.parse(reader.result); }
+ catch (err) { toastError('Could not read that file as a design.'); return; }
+ importPixelArtAsLayer(payload);
+ };
+ reader.readAsText(file);
+ }
+
+ function pixelToolMenuToggle(forceOpen) {
+ const menu = $('pixelToolMenu');
+ const btn = $('tool-pixel');
+ if (!menu || !btn) return;
+ const isOpen = menu.classList.contains('open');
+ const willOpen = forceOpen !== undefined ? forceOpen : !isOpen;
+ menu.classList.toggle('open', willOpen);
+ btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
+ }
+
// Commit the standalone pixel editor's working grid into state.pixelArt.
function pixelCommitIfStandalone() {
if (!_pixelEdit || !_pixelEdit.standalone) return;
@@ -16765,6 +16894,7 @@
wall.appendChild(buildFeedCard(row, () => {
if (tryDesignLayerImportRow(row)) return;
if (tryDeckImportRow(row)) return;
+ if (tryPixelArtImportRow(row)) return;
if (row.mode === 'disc-design') {
const slotTarget = state._discImportTargetSlot;
if (slotTarget) {
@@ -16815,6 +16945,7 @@
const card = buildFeedCard(row, () => {
if (tryDesignLayerImportRow(row)) return;
if (tryDeckImportRow(row)) return;
+ if (tryPixelArtImportRow(row)) return;
if (row.mode === 'disc-design') {
const slotTarget = state._discImportTargetSlot;
if (slotTarget) {
@@ -17187,6 +17318,7 @@
if (!row?.payload) return;
if (tryDesignLayerImportRow(row)) return;
if (tryDeckImportRow(row)) return;
+ if (tryPixelArtImportRow(row)) return;
if (row.mode === 'disc-design') {
const slotTarget = state._discImportTargetSlot;
if (slotTarget) {
@@ -18373,6 +18505,7 @@
out.appendChild(buildResultCard(row, () => {
if (tryDesignLayerImportRow(row)) return;
if (tryDeckImportRow(row)) return;
+ if (tryPixelArtImportRow(row)) return;
if (row.mode === 'disc-design') {
const slotTarget = state._discImportTargetSlot;
if (slotTarget) {
@@ -19235,7 +19368,44 @@
$('deckPresentBtn')?.addEventListener('click', presentCurrentDeck);
// ---- Pixel Editor wiring ----
- $('tool-pixel')?.addEventListener('click', () => addPixelArtLayer());
+ // The `tool-pixel` palette button opens a small menu with three options:
+ // add a blank layer, browse the community for an existing design, or
+ // import a design from a local JSON file.
+ $('tool-pixel')?.addEventListener('click', (e) => {
+ e.stopPropagation();
+ pixelToolMenuToggle();
+ });
+ $('pixelAddBlankBtn')?.addEventListener('click', () => {
+ pixelToolMenuToggle(false);
+ addPixelArtLayer();
+ });
+ $('pixelImportNostrBtn')?.addEventListener('click', () => {
+ pixelToolMenuToggle(false);
+ openPixelArtImportBrowser();
+ });
+ $('pixelImportFileBtn')?.addEventListener('click', () => {
+ pixelToolMenuToggle(false);
+ pixelImportFromFile();
+ });
+ $('pixelImportFile')?.addEventListener('change', handlePixelImportFile);
+ // Outside-click and Escape dismiss the menu — mirrors the export-PNG
+ // menu pattern.
+ const _pixelToolMenuEl = $('pixelToolMenu');
+ const _pixelToolBtnEl = $('tool-pixel');
+ if (_pixelToolMenuEl && _pixelToolBtnEl) {
+ const _maybeClosePixelToolMenu = (ev) => {
+ if (!_pixelToolMenuEl.classList.contains('open')) return;
+ if (_pixelToolMenuEl.contains(ev.target) || _pixelToolBtnEl.contains(ev.target)) return;
+ pixelToolMenuToggle(false);
+ };
+ document.addEventListener('click', _maybeClosePixelToolMenu);
+ document.addEventListener('pointerdown', _maybeClosePixelToolMenu, true);
+ document.addEventListener('keydown', (ev) => {
+ if (ev.key === 'Escape' && _pixelToolMenuEl.classList.contains('open')) {
+ pixelToolMenuToggle(false);
+ }
+ });
+ }
$('editPixelsBtn')?.addEventListener('click', () => {
const l = selectedLayer();
if (l && l.type === 'pixelart') openPixelEditor(l.id);
@@ -19671,7 +19841,7 @@
$("designerCategoryField").addEventListener("change", (e)=>{pushHistory(`change category to ${e.target.value || 'Uncategorized'}`); state.meta.category=e.target.value; syncIdentifierVisibility(); render();});
$("designerLanguageField").addEventListener("change", (e)=>{pushHistory('change language'); state.meta.language=e.target.value; render();});
- $("nostrClose").addEventListener("click", () => { closeNostrPreview(); $("nostrModal").classList.remove("open"); _feedCache.softModRevealed.clear(); state._deckImportMode = false; state._designLayerImportTarget = ''; state._designLayerReplaceId = ''; });
+ $("nostrClose").addEventListener("click", () => { closeNostrPreview(); $("nostrModal").classList.remove("open"); _feedCache.softModRevealed.clear(); state._deckImportMode = false; state._pixelArtImportMode = false; state._designLayerImportTarget = ''; state._designLayerReplaceId = ''; });
$("nostrRefreshBtn").addEventListener("click", async () => {
const btn = $("nostrRefreshBtn");
const original = btn.textContent;