Compare commits

...

6 Commits

8 changed files with 1707 additions and 275 deletions

1431
classic.js

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,8 @@ const PALETTE = [
]},
{ family: "Oranges & Browns & Yellows", colors: [
{name:"Pastel Yellow",hex:"#fcfd96"},{name:"Yellow",hex:"#f5e812"},{name:"Goldenrod",hex:"#f7b615"},
{name:"Orange",hex:"#ef6b24"},{name:"Coffee",hex:"#957461"},{name:"Burnt Orange",hex:"#9d4223"}
{name:"Orange",hex:"#ef6b24"},{name:"Coffee",hex:"#957461"},{name:"Burnt Orange",hex:"#9d4223"},
{name:"Blended Brown",hex:"#c9aea0"}
]},
{ family: "Greens", colors: [
{name:"Eucalyptus",hex:"#a3bba3"},{name:"Pastel Green",hex:"#acdba7"},{name:"Lime Green",hex:"#8fc73e"},

View File

@ -24,7 +24,7 @@
</style>
</head>
<body class="p-0 md:p-6 flex flex-col items-center justify-start min-h-screen bg-[conic-gradient(at_top_left,_var(--tw-gradient-stops))] from-indigo-100 via-white to-pink-100 text-slate-800 overflow-hidden">
<div class="container mx-auto mt-2 p-4 lg:p-6 bg-white/80 lg:backdrop-blur-xl rounded-3xl border border-white/50 shadow-2xl flex flex-col gap-4 max-w-7xl lg:h-[calc(100vh-2rem)] overflow-hidden ring-1 ring-black/5">
<div class="container mx-auto mt-2 p-4 lg:p-6 bg-white/80 lg:backdrop-blur-xl rounded-3xl border border-white/50 shadow-2xl flex flex-col gap-4 max-w-7xl lg:h-[calc(97vh-2rem)] overflow-hidden ring-1 ring-black/5">
<header id="app-header" class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-3 px-1 lg:px-0">
<div class="flex items-center gap-3">
@ -251,9 +251,24 @@
</div>
<div class="md:col-span-2 space-y-2">
<div class="text-sm font-medium text-gray-700">Layout</div>
<div class="flex gap-2">
<div class="flex gap-2 flex-wrap">
<button type="button" class="tab-btn tab-active pattern-btn" data-pattern-layout="spiral" aria-pressed="true">Spiral</button>
<button type="button" class="tab-btn tab-idle pattern-btn" data-pattern-layout="stacked" aria-pressed="false">Stacked</button>
<button type="button" class="tab-btn tab-idle" id="classic-manual-btn" aria-pressed="false">Manual paint</button>
</div>
<div id="classic-expanded-row" class="flex items-center gap-2 hidden">
<label class="text-sm inline-flex items-center gap-2 font-medium">
<input id="classic-expanded-toggle" type="checkbox" class="align-middle" checked>
Expanded spacing
</label>
<p class="hint m-0">Separate clusters for easier taps.</p>
</div>
<div id="classic-focus-row" class="flex items-center gap-2 hidden">
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-prev" aria-hidden="true" tabindex="-1">◀ Prev</button>
<span id="classic-focus-label" class="text-sm text-gray-700">Clusters 18</span>
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-next" aria-hidden="true" tabindex="-1">Next ▶</button>
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-focus-zoomout" aria-hidden="true" tabindex="-1">Zoom Out</button>
<button type="button" class="btn-dark text-xs px-3 py-2 hidden" id="classic-quad-reset" aria-hidden="true" tabindex="-1">Reset Quad</button>
</div>
</div>
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
@ -348,6 +363,30 @@
<div id="classic-slots" class="flex items-center gap-2"></div>
<button id="classic-add-slot" class="btn-dark text-sm px-3 py-2 hidden" type="button" title="Add color slot">+</button>
</div>
<div class="flex items-center gap-3 mb-2">
<span class="text-sm font-semibold text-gray-700">Active color:</span>
<button id="classic-active-chip" type="button" class="slot-swatch border border-gray-300" title="Tap to scroll to palette">
<span class="color-dot" id="classic-active-dot"></span>
</button>
</div>
<div class="panel-heading mt-2">Project Palette</div>
<div id="classic-project-palette" class="palette-box min-h-[2.4rem]"></div>
<div class="panel-heading mt-4">Replace Color (Manual)</div>
<div class="panel-card space-y-3">
<div class="flex items-center gap-2 replace-row">
<button type="button" class="replace-chip" id="classic-replace-from-chip" aria-label="Pick color to replace"></button>
<span class="text-xs font-semibold text-slate-500"></span>
<button type="button" class="replace-chip" id="classic-replace-to-chip" aria-label="Pick replacement color"></button>
<span id="classic-replace-count" class="text-xs text-slate-500 ml-auto"></span>
</div>
<div class="grid grid-cols-1 gap-2">
<p class="hint text-xs">Manual paint only. “From” lists colors already used on canvas; “To” comes from the Classic library.</p>
<select id="classic-replace-from" class="sr-only"></select>
<select id="classic-replace-to" class="sr-only"></select>
<button id="classic-replace-btn" class="btn-blue">Replace</button>
<p id="classic-replace-msg" class="hint"></p>
</div>
</div>
<div class="text-sm text-gray-600 mb-1">Pick a color for <span id="classic-active-label" class="font-bold">Slot #1</span> (from colors.js):</div>
<div id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
<div class="flex flex-wrap gap-2 mt-3">
@ -377,10 +416,41 @@
</aside>
<section id="classic-canvas-panel"
class="order-1 w-full lg:flex-1 flex flex-col items-stretch shadow-x3 rounded-2xl overflow-hidden bg-white">
class="order-1 w-full lg:flex-1 grid grid-rows-[1fr] lg:grid-rows-[minmax(0,1fr)] gap-2 shadow-x3 rounded-2xl overflow-hidden bg-white">
<div id="classic-display"
class="rounded-xl"
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
<div id="classic-mobile-bar" class="mobile-action-bar hidden">
<div class="mobile-action-chip" id="classic-active-chip-floating" title="Active">
<span class="color-dot" id="classic-active-dot-floating"></span>
</div>
<div class="mobile-action-row">
<button type="button" class="mobile-action-btn" id="classic-undo-manual" aria-label="Undo">
<i class="fa-solid fa-rotate-left" aria-hidden="true"></i>
<span>Undo</span>
</button>
<button type="button" class="mobile-action-btn" id="classic-redo-manual" aria-label="Redo">
<i class="fa-solid fa-rotate-right" aria-hidden="true"></i>
<span>Redo</span>
</button>
<button type="button" class="mobile-action-btn" id="classic-pick-manual" aria-label="Eyedropper" aria-pressed="false">
<i class="fa-solid fa-eye-dropper" aria-hidden="true"></i>
<span>Pick</span>
</button>
<button type="button" class="mobile-action-btn" id="classic-erase-manual" aria-label="Toggle Erase" aria-pressed="false">
<i class="fa-solid fa-eraser" aria-hidden="true"></i>
<span>Erase</span>
</button>
<button type="button" class="mobile-action-btn danger" id="classic-clear-manual" aria-label="Clear">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
<span>Clear</span>
</button>
<button type="button" class="mobile-action-btn" id="classic-export-manual" aria-label="Export">
<i class="fa-solid fa-download" aria-hidden="true"></i>
<span>Export</span>
</button>
</div>
</div>
<div id="floating-topper-nudge" class="floating-nudge hidden">
<div class="floating-nudge-header">
<div class="panel-heading">Nudge Topper</div>
@ -516,7 +586,8 @@
</aside>
<section id="wall-canvas-panel"
class="order-1 lg:order-2 w-full lg:flex-1 flex flex-col items-stretch rounded-2xl overflow-hidden bg-white/50 shadow-inner ring-1 ring-black/5">
class="order-1 lg:order-2 w-full lg:flex-1 flex flex-col items-stretch rounded-2xl overflow-hidden bg-white/50 shadow-inner ring-1 ring-black/5"
style="height:92%;">
<div class="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-white/70">
<div class="text-base font-semibold text-slate-700">Balloon Wall</div>
<div class="text-sm text-gray-500">Columns/Rows: <span id="wall-grid-label">9 × 7</span></div>
@ -625,5 +696,18 @@
<script src="wall.js" defer></script>
<script src="classic.js" defer></script>
<div id="classic-quad-modal" class="quad-modal hidden" aria-hidden="true">
<div class="quad-modal-backdrop"></div>
<div class="quad-modal-panel" role="dialog" aria-modal="true" aria-label="Quad detail">
<div class="quad-modal-header">
<div class="quad-modal-title">Quad Detail</div>
<button type="button" id="classic-quad-modal-close" class="btn-dark text-xs px-3 py-2">Close</button>
</div>
<div class="quad-modal-body">
<div id="classic-quad-modal-display" class="quad-modal-display"></div>
</div>
</div>
</div>
</body>
</html>

View File

@ -143,6 +143,13 @@
const garlandAccentChip = document.getElementById('garland-accent-chip');
const garlandAccentClearBtn = document.getElementById('garland-accent-clear');
const garlandControls = document.getElementById('garland-controls');
// Optional dropdowns (may not be present in current layout)
const garlandColorMain1Sel = document.getElementById('garland-color-main-1');
const garlandColorMain2Sel = document.getElementById('garland-color-main-2');
const garlandColorMain3Sel = document.getElementById('garland-color-main-3');
const garlandColorMain4Sel = document.getElementById('garland-color-main-4');
const garlandColorAccentSel = document.getElementById('garland-color-accent');
const updateGarlandSwatches = () => {}; // stub for layouts without dropdown swatches
const sizePresetGroup = document.getElementById('size-preset-group');
const toggleShineBtn = null;
@ -1106,50 +1113,31 @@
if (mode === 'garland') requestDraw();
persist();
});
bindActiveChipPicker();
// ====== UI Rendering (Palettes) ======
function renderAllowedPalette() {
if (!paletteBox) return;
paletteBox.innerHTML = '';
(window.PALETTE || []).forEach(group => {
const title = document.createElement('div');
title.className = 'family-title';
title.textContent = group.family;
paletteBox.appendChild(title);
const row = document.createElement('div');
row.className = 'swatch-row';
(group.colors || []).forEach(c => {
const idx =
FLAT_COLORS.find(fc => fc.name === c.name && fc.hex === c.hex && fc.family === group.family)?._idx
?? HEX_TO_FIRST_IDX.get(normalizeHex(c.hex));
const sw = document.createElement('button');
sw.type = 'button';
sw.className = 'swatch';
sw.setAttribute('aria-label', c.name);
if (c.image) {
const meta = FLAT_COLORS[idx] || {};
sw.style.backgroundImage = `url("${c.image}")`;
sw.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
sw.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
} else {
sw.style.backgroundColor = c.hex;
}
if (idx === selectedColorIdx) sw.classList.add('active');
sw.title = c.name;
sw.addEventListener('click', () => {
selectedColorIdx = idx ?? 0;
renderAllowedPalette();
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn-dark w-full';
btn.textContent = 'Choose color';
btn.addEventListener('click', () => {
if (!window.openColorPicker) return;
window.openColorPicker({
title: 'Choose active color',
subtitle: 'Applies to drawing and path tools',
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
onSelect: (item) => {
if (!Number.isInteger(item.idx)) return;
selectedColorIdx = item.idx;
updateCurrentColorChip();
persist();
}
});
row.appendChild(sw);
});
paletteBox.appendChild(row);
});
paletteBox.appendChild(btn);
}
function getUsedColors() {
@ -1196,6 +1184,29 @@
updateChip('mobile-active-color-chip', null, { showLabel: false });
}
function bindActiveChipPicker() {
const chips = ['current-color-chip', 'mobile-active-color-chip'];
chips.forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.style.cursor = 'pointer';
el.addEventListener('click', () => {
if (!window.openColorPicker) return;
window.openColorPicker({
title: 'Choose active color',
subtitle: 'Applies to drawing and path tools',
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
onSelect: (item) => {
if (!Number.isInteger(item.idx)) return;
selectedColorIdx = item.idx;
updateCurrentColorChip();
persist();
}
});
});
});
}
window.organic = {
getColor: () => selectedColorIdx,
updateCurrentColorChip,
@ -1212,58 +1223,8 @@
function renderUsedPalette() {
if (!usedPaletteBox) return;
usedPaletteBox.innerHTML = '';
const used = getUsedColors();
if (used.length === 0) {
usedPaletteBox.innerHTML = '<div class="hint">No colors yet.</div>';
usedPaletteBox.innerHTML = '<div class="hint">Palette opens in modal.</div>';
if (replaceFromSel) replaceFromSel.innerHTML = '';
return;
}
const row = document.createElement('div');
row.className = 'swatch-row';
used.forEach(item => {
const sw = document.createElement('button');
sw.type = 'button';
sw.className = 'swatch';
const name = item.name || NAME_BY_HEX.get(item.hex) || item.hex;
sw.setAttribute('aria-label', `${name} - Count: ${item.count}`);
if (item.image) {
const meta = FLAT_COLORS[HEX_TO_FIRST_IDX.get(item.hex)] || {};
sw.style.backgroundImage = `url("${item.image}")`;
sw.style.backgroundSize = `${100 * SWATCH_TEXTURE_ZOOM}%`;
sw.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
} else {
sw.style.backgroundColor = item.hex;
}
if (normalizeHex(FLAT_COLORS[selectedColorIdx]?.hex) === item.hex) sw.classList.add('active');
sw.title = `${name}${item.count}`;
sw.addEventListener('click', () => {
selectedColorIdx = HEX_TO_FIRST_IDX.get(item.hex) ?? 0;
renderAllowedPalette();
renderUsedPalette();
});
const badge = document.createElement('div');
badge.className = 'badge';
badge.textContent = String(item.count);
sw.appendChild(badge);
row.appendChild(sw);
});
usedPaletteBox.appendChild(row);
// fill "replace from"
if (replaceFromSel) {
replaceFromSel.innerHTML = '';
used.forEach(item => {
const opt = document.createElement('option');
const name = item.name || NAME_BY_HEX.get(item.hex) || item.hex;
opt.value = item.hex;
opt.textContent = `${name} (${item.count})`;
replaceFromSel.appendChild(opt);
});
updateReplaceChips();
}
}
// ====== Balloon Ops & Data/Export ======

View File

@ -526,10 +526,10 @@
let current = '#tab-organic';
const isMobileView = () => window.matchMedia('(max-width: 1023px)').matches;
const updateMobileActionBarVisibility = () => {
if (!mobileActionBar) return;
const modalOpen = !!document.querySelector('.color-modal:not(.hidden)');
const shouldShow = current === '#tab-organic' && isMobileView() && !modalOpen;
mobileActionBar.classList.toggle('hidden', !shouldShow);
const isMobile = isMobileView();
const showOrganic = isMobile && !modalOpen && current === '#tab-organic';
if (mobileActionBar) mobileActionBar.classList.toggle('hidden', !showOrganic);
};
const wireMobileActionButtons = () => {
const guardOrganic = () => current === '#tab-organic';
@ -549,6 +549,7 @@
on('mobile-act-export', () => clickBtn('[data-export="png"]'));
};
wireMobileActionButtons();
window.addEventListener('resize', updateMobileActionBarVisibility);
function setTab(id, isInitial = false) {
if (!id || !document.querySelector(id)) id = '#tab-organic';

View File

@ -13,6 +13,8 @@
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const clamp01 = v => clamp(v, 0, 1);
const normalizeHex = h => (h || '').toLowerCase();
const ACTIVE_COLOR_KEY = 'app:activeColor:v1';
let ACTIVE_COLOR_CACHE = null;
function hexToRgb(hex) {
const h = normalizeHex(hex).replace('#','');
if (h.length === 3) {
@ -29,6 +31,24 @@
}
return { r: 0, g: 0, b: 0 };
}
function getActiveColor() {
if (ACTIVE_COLOR_CACHE) return ACTIVE_COLOR_CACHE;
try {
const saved = JSON.parse(localStorage.getItem(ACTIVE_COLOR_KEY));
if (saved && saved.hex) {
ACTIVE_COLOR_CACHE = { hex: normalizeHex(saved.hex), image: saved.image || null };
return ACTIVE_COLOR_CACHE;
}
} catch {}
ACTIVE_COLOR_CACHE = { hex: '#ff6b6b', image: null };
return ACTIVE_COLOR_CACHE;
}
function setActiveColor(color) {
const clean = { hex: normalizeHex(color?.hex || '#ffffff'), image: color?.image || null };
ACTIVE_COLOR_CACHE = clean;
try { localStorage.setItem(ACTIVE_COLOR_KEY, JSON.stringify(clean)); } catch {}
return clean;
}
function luminance(hex) {
const { r, g, b } = hexToRgb(hex || '#000');
const norm = [r,g,b].map(v => {
@ -206,6 +226,8 @@
imageToDataUrl,
imageUrlToDataUrl,
download,
getActiveColor,
setActiveColor
};
});
})();

119
style.css
View File

@ -29,7 +29,15 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
box-shadow: 0 8px 24px rgba(15,23,42,0.08);
}
#balloon-canvas { touch-action: none; }
#balloon-canvas { touch-action: none;
height: 95%}
.classic-expanded-canvas {
height: 130vh !important;
min-height: 130vh;
overflow: auto;
}
.balloon-canvas {
background: #fff;
@ -388,6 +396,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
z-index: 30;
-webkit-overflow-scrolling: touch;
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
height: 92%;
}
.control-sheet.hidden { display: none; }
.control-sheet.minimized { transform: translateY(100%); }
@ -429,7 +438,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
#classic-display,
#wall-display,
#balloon-canvas {
margin-bottom: 5rem;
margin-bottom: 6.5rem;
}
/* Stack switching: show only the active mobile tab stack across panels */
@ -464,6 +473,8 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
.swatch.tiny { width: 1.8rem; height: 1.8rem; }
.select { min-height: 44px; }
.panel-card { padding: 0.85rem; }
.manual-hub { position: sticky; top: 0; z-index: 6; }
.manual-detail-stage { min-height: 260px; }
}
.mobile-action-bar {
@ -607,6 +618,57 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
box-shadow: 0 0 0 2px rgba(37,99,235,0.18), 0 6px 16px rgba(37,99,235,0.2);
}
.manual-hub {
background: linear-gradient(135deg, rgba(255,255,255,0.9), rgba(240,249,255,0.92));
border: 1px solid rgba(226,232,240,0.9);
border-radius: 1rem;
padding: 0.9rem 1rem;
box-shadow: 0 12px 28px rgba(15,23,42,0.08);
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.manual-hub-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
}
.manual-hub-title { font-weight: 800; color: #0f172a; letter-spacing: -0.015em; }
.manual-hub-subtitle { font-size: 0.85rem; color: #475569; line-height: 1.3; }
.manual-hub-actions { display: flex; gap: 0.35rem; }
.manual-hub-track {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 0.55rem;
}
.manual-hub-count { grid-column: 1 / -1; text-align: right; }
.manual-range {
width: 100%;
accent-color: #2563eb;
}
.manual-detail {
display: none;
}
.manual-detail-stage { display: none; }
.manual-detail-empty { display: none; }
.manual-hub-hint { line-height: 1.4; }
.chip-btn {
background: rgba(255,255,255,0.85);
border: 1px solid rgba(148,163,184,0.4);
border-radius: 999px;
padding: 0.45rem 0.75rem;
font-weight: 700;
font-size: 0.85rem;
color: #0f172a;
box-shadow: 0 6px 16px rgba(15,23,42,0.08);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.chip-btn:active { transform: translateY(1px); }
.chip-btn:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.mobile-tabbar {
position: fixed;
inset-inline: 0;
@ -625,13 +687,29 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
border-top: 1px solid rgba(148, 163, 184, 0.25);
}
.mobile-tabbar.hidden { display: none; }
@media (min-width: 1024px) {
.mobile-tabbar { display: none !important; }
}
@media (max-width: 1023px) {
/* Tuck canvases above the tabbar */
#classic-display,
#wall-display,
#balloon-canvas {
margin-bottom: 5.5rem;
margin-bottom: 0;
height: calc(100vh - 190px) !important; /* tie to viewport minus header/controls */
max-height: calc(100vh - 190px) !important;
}
#classic-display{
height: 92%;
}
/* Keep the main canvas panels above the tabbar/action bar */
#canvas-panel,
#classic-canvas-panel {
padding-bottom: 12vh;
}
#classic-canvas-panel {
padding-bottom: 14vh; /* leave space for action bar */
}
}
.mobile-tabbar .mobile-tab-btn {
@ -666,6 +744,39 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
transform: translateY(-2px);
}
.canvas-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: .75rem;
padding: .6rem .85rem;
border-bottom: 1px solid #e5e7eb;
background: linear-gradient(90deg, #f8fafc, #fff);
position: sticky;
top: 0;
z-index: 5;
}
.canvas-toolbar .toolbar-left,
.canvas-toolbar .toolbar-right {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: .5rem;
}
.canvas-toolbar button { white-space: nowrap; }
.quad-modal{position:fixed;inset:0;z-index:999;display:flex;align-items:center;justify-content:center;pointer-events:none;}
.quad-modal.hidden{display:none;}
.quad-modal:not(.hidden){pointer-events:auto;}
.quad-modal-backdrop{position:absolute;inset:0;background:rgba(15,23,42,0.45);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);}
.quad-modal-panel{position:relative;z-index:1;pointer-events:auto;background:#fff;border-radius:1rem;padding:1rem;box-shadow:0 22px 50px rgba(15,23,42,0.25);width:min(560px,90vw);}
.quad-modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem;}
.quad-modal-title{font-weight:700;font-size:1rem;color:#0f172a;}
.quad-modal-body{border:1px solid #e5e7eb;border-radius:.75rem;overflow:hidden;background:#f8fafc;}
.quad-modal-display{width:100%;height:360px;max-height:70vh;position:relative;background:#fff;perspective:1200px;}
.quad-modal-display svg{transform-style:preserve-3d;transition:transform 240ms ease, opacity 200ms ease;}
.quad-modal-display svg{width:100%;height:100%;}
@media (min-width: 1024px) {
.control-sheet {
@ -673,7 +784,7 @@ body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
top: 7rem;
bottom: auto;
width: 340px;
max-height: calc(100vh - 8rem);
max-height: calc(93vh - 8rem);
border-radius: 1.5rem;
position: sticky;
overflow-y: auto;

103
wall.js
View File

@ -623,36 +623,7 @@
function renderWallUsedPalette() {
if (!wallUsedPaletteEl) return;
const used = wallUsedColors();
wallUsedPaletteEl.innerHTML = '';
if (!used.length) {
wallUsedPaletteEl.innerHTML = '<div class="text-xs text-gray-500">No colors yet.</div>';
populateWallReplaceSelects();
updateWallReplacePreview();
return;
}
const row = document.createElement('div');
row.className = 'swatch-row';
used.forEach(item => {
const sw = document.createElement('button');
sw.type = 'button';
sw.className = 'swatch';
if (item.image) {
sw.style.backgroundImage = `url("${item.image}")`;
sw.style.backgroundSize = `${100 * 2.5}%`;
} else {
sw.style.backgroundColor = item.hex;
}
sw.title = `${item.name || item.hex} (${item.count})`;
sw.addEventListener('click', () => {
if (!Number.isInteger(item.idx)) return;
setActiveColor(normalizeColorIdx(item.idx));
renderWallPalette();
renderWallUsedPalette();
});
row.appendChild(sw);
});
wallUsedPaletteEl.appendChild(row);
wallUsedPaletteEl.innerHTML = '<div class="text-xs text-gray-500">Palette opens in modal.</div>';
populateWallReplaceSelects();
updateWallReplacePreview();
}
@ -1003,47 +974,25 @@
if (!wallPaletteEl) return;
wallPaletteEl.innerHTML = '';
populateWallReplaceSelects();
(window.PALETTE || []).forEach(group => {
const title = document.createElement('div');
title.className = 'family-title';
title.textContent = group.family;
wallPaletteEl.appendChild(title);
const row = document.createElement('div');
row.className = 'swatch-row';
(group.colors || []).forEach(c => {
const normHex = (c.hex || '').toLowerCase();
let idx = FLAT_COLORS.findIndex(fc => fc.name === c.name && fc.hex === c.hex && fc.family === group.family);
if (idx < 0 && window.shared?.HEX_TO_FIRST_IDX?.has(normHex)) {
idx = window.shared.HEX_TO_FIRST_IDX.get(normHex);
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'btn-dark w-full';
btn.textContent = 'Choose color';
btn.addEventListener('click', () => {
if (!window.openColorPicker) return;
window.openColorPicker({
title: 'Choose active wall color',
subtitle: 'Applies to wall fill tools',
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
onSelect: (item) => {
if (!Number.isInteger(item.idx)) return;
setActiveColor(item.idx);
updateWallActiveChip(getActiveWallColorIdx());
updateWallReplacePreview();
}
idx = normalizeColorIdx(idx);
const sw = document.createElement('button');
sw.type = 'button';
sw.className = 'swatch';
if (c.image) {
const meta = FLAT_COLORS[idx] || {};
sw.style.backgroundImage = `url("${c.image}")`;
sw.style.backgroundSize = `${100 * 2.5}%`;
sw.style.backgroundPosition = `${(meta.imageFocus?.x ?? 0.5) * 100}% ${(meta.imageFocus?.y ?? 0.5) * 100}%`;
} else {
sw.style.backgroundColor = c.hex;
}
if (idx === selectedColorIdx) sw.classList.add('active');
sw.title = c.name;
sw.addEventListener('click', () => {
setActiveColor(idx);
window.organic?.updateCurrentColorChip?.(selectedColorIdx);
// Also update the global chip explicitly
if (window.organic?.updateCurrentColorChip) {
window.organic.updateCurrentColorChip(selectedColorIdx);
}
renderWallPalette();
});
row.appendChild(sw);
});
wallPaletteEl.appendChild(row);
});
wallPaletteEl.appendChild(btn);
renderWallUsedPalette();
updateWallActiveChip(getActiveWallColorIdx());
updateWallReplacePreview();
@ -1074,6 +1023,24 @@
else selectedColorIdx = defaultActiveColorIdx();
setActiveColor(selectedColorIdx);
setWallToolMode('paint');
// Allow picking active wall color by clicking the chip.
if (wallActiveChip && window.openColorPicker) {
wallActiveChip.style.cursor = 'pointer';
wallActiveChip.addEventListener('click', () => {
window.openColorPicker({
title: 'Choose wall color',
subtitle: 'Sets the active wall color',
items: (FLAT_COLORS || []).map((c, idx) => ({ label: c.name || c.hex, metaText: c.family || '', idx })),
onSelect: (item) => {
if (!Number.isInteger(item.idx)) return;
setActiveColor(item.idx);
renderWallPalette();
renderWallUsedPalette();
renderWall();
}
});
});
}
loadPatternState(patternKey());
ensureWallGridSize(wallState.rows, wallState.cols);
syncWallInputs();