Compare commits
No commits in common. "main" and "version-v2" have entirely different histories.
main
...
version-v2
1974
classic.js
@ -12,8 +12,7 @@ const PALETTE = [
|
|||||||
]},
|
]},
|
||||||
{ family: "Oranges & Browns & Yellows", colors: [
|
{ family: "Oranges & Browns & Yellows", colors: [
|
||||||
{name:"Pastel Yellow",hex:"#fcfd96"},{name:"Yellow",hex:"#f5e812"},{name:"Goldenrod",hex:"#f7b615"},
|
{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: [
|
{ family: "Greens", colors: [
|
||||||
{name:"Eucalyptus",hex:"#a3bba3"},{name:"Pastel Green",hex:"#acdba7"},{name:"Lime Green",hex:"#8fc73e"},
|
{name:"Eucalyptus",hex:"#a3bba3"},{name:"Pastel Green",hex:"#acdba7"},{name:"Lime Green",hex:"#8fc73e"},
|
||||||
|
|||||||
520
index.html
@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/mithril/mithril.js" defer></script>
|
<script src="https://unpkg.com/mithril/mithril.js" defer></script>
|
||||||
|
|
||||||
@ -24,9 +23,9 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</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">
|
<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(97vh-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(100vh-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">
|
<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">
|
<div class="flex items-center gap-3">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -34,17 +33,10 @@
|
|||||||
<div class="text-xs text-indigo-500 font-bold uppercase tracking-wider">Professional Design Tool</div>
|
<div class="text-xs text-indigo-500 font-bold uppercase tracking-wider">Professional Design Tool</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4">
|
<nav id="mode-tabs" class="flex gap-2">
|
||||||
<nav id="mode-tabs" class="flex gap-2">
|
<button type="button" class="tab-btn tab-active" data-target="#tab-organic" aria-pressed="true">Organic</button>
|
||||||
<button type="button" class="tab-btn tab-active" data-target="#tab-organic" aria-pressed="true">Organic</button>
|
<button type="button" class="tab-btn tab-idle" data-target="#tab-classic" aria-pressed="false">Classic (Arch/Column)</button>
|
||||||
<button type="button" class="tab-btn tab-idle" data-target="#tab-classic" aria-pressed="false">Classic</button>
|
</nav>
|
||||||
<button type="button" class="tab-btn tab-idle" data-target="#tab-wall" aria-pressed="false">Wall</button>
|
|
||||||
</nav>
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<button id="app-fullscreen-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Toggle fullscreen">Fullscreen</button>
|
|
||||||
<button id="clear-canvas-btn-top" class="btn-danger text-xs px-3 py-2">Start Fresh</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section id="tab-organic" class="flex flex-col lg:flex-row gap-4 lg:h-[calc(100vh-10rem)]">
|
<section id="tab-organic" class="flex flex-col lg:flex-row gap-4 lg:h-[calc(100vh-10rem)]">
|
||||||
@ -55,15 +47,11 @@
|
|||||||
<div class="control-stack" data-mobile-tab="controls">
|
<div class="control-stack" data-mobile-tab="controls">
|
||||||
<div class="panel-heading">Tools</div>
|
<div class="panel-heading">Tools</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div class="grid grid-cols-4 gap-2 mb-3">
|
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||||
<button id="tool-draw" class="tool-btn" aria-pressed="true" title="V">
|
<button id="tool-draw" class="tool-btn" aria-pressed="true" title="V">
|
||||||
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||||
<span class="hidden sm:inline">Draw</span>
|
<span class="hidden sm:inline">Draw</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="tool-garland" class="tool-btn" aria-pressed="false" title="G">
|
|
||||||
<svg viewBox="0 0 24 24"><path d="M4 17c3-4 6-6 9-6s5 2 7 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="7" cy="14" r="1.4"/><circle cx="11.5" cy="12.5" r="1.4"/><circle cx="16" cy="13.5" r="1.4"/><circle cx="19" cy="16.5" r="1.4"/></svg>
|
|
||||||
<span class="hidden sm:inline">Path</span>
|
|
||||||
</button>
|
|
||||||
<button id="tool-erase" class="tool-btn" aria-pressed="false" title="E">
|
<button id="tool-erase" class="tool-btn" aria-pressed="false" title="E">
|
||||||
<svg viewBox="0 0 24 24"><path d="M16.24 3.56l4.95 4.94c.78.79.78 2.05 0 2.84L12 20.53a4.008 4.008 0 0 1-5.66 0L2.81 17c-.78-.79-.78-2.05 0-2.84l10.6-10.6c.79-.78 2.05-.78 2.83 0zM4.22 15.58l3.54 3.53c.78.79 2.04.79 2.83 0l8.48-8.48-3.54-3.54-8.48 8.48c-.79.79-.79 2.05 0 2.84z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M16.24 3.56l4.95 4.94c.78.79.78 2.05 0 2.84L12 20.53a4.008 4.008 0 0 1-5.66 0L2.81 17c-.78-.79-.78-2.05 0-2.84l10.6-10.6c.79-.78 2.05-.78 2.83 0zM4.22 15.58l3.54 3.53c.78.79 2.04.79 2.83 0l8.48-8.48-3.54-3.54-8.48 8.48c-.79.79-.79 2.05 0 2.84z"/></svg>
|
||||||
<span class="hidden sm:inline">Erase</span>
|
<span class="hidden sm:inline">Erase</span>
|
||||||
@ -74,11 +62,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-2 mb-3">
|
<div class="grid grid-cols-3 gap-2 mb-3">
|
||||||
<button id="tool-undo" class="tool-btn" aria-label="Undo">
|
<button id="tool-undo" class="tool-btn" title="Ctrl+Z" aria-label="Undo">
|
||||||
<svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"/></svg>
|
||||||
<span class="hidden sm:inline">Undo</span>
|
<span class="hidden sm:inline">Undo</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="tool-redo" class="tool-btn" aria-label="Redo">
|
<button id="tool-redo" class="tool-btn" title="Ctrl+Y" aria-label="Redo">
|
||||||
<svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>
|
<svg viewBox="0 0 24 24"><path d="M18.4 10.6C16.55 9 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"/></svg>
|
||||||
<span class="hidden sm:inline">Redo</span>
|
<span class="hidden sm:inline">Redo</span>
|
||||||
</button>
|
</button>
|
||||||
@ -87,44 +75,32 @@
|
|||||||
<span class="hidden sm:inline">Picker</span>
|
<span class="hidden sm:inline">Picker</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint mt-1">Use Path to click-drag a line; balloons will be auto-placed along it.</p>
|
<div id="eraser-controls" class="hidden flex flex-col gap-2">
|
||||||
<div id="garland-controls" class="mt-2 flex flex-col gap-3 text-sm text-gray-700">
|
<label class="text-sm font-medium text-gray-700">Eraser Size: <span id="eraser-size-label">30</span>px</label>
|
||||||
<div class="flex items-center gap-3">
|
<input type="range" id="eraser-size" min="10" max="120" value="30" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||||
<label for="garland-density" class="font-medium w-20">Density</label>
|
<p class="hint">Click-drag to erase. Preview circle shows the area.</p>
|
||||||
<input id="garland-density" type="range" min="0.6" max="1.6" step="0.1" value="1" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
</div>
|
||||||
<span id="garland-density-label" class="w-10 text-right text-xs text-gray-500">1.0</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="font-medium text-sm text-gray-700">Main Colors</span>
|
|
||||||
<button type="button" id="garland-add-color" class="btn-blue text-xs px-3 py-1">+ Add</button>
|
|
||||||
</div>
|
|
||||||
<div id="garland-main-chips" class="flex flex-wrap gap-2"></div>
|
|
||||||
<p class="hint text-xs">Tap a chip to change it. You can add up to 10 main colors.</p>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="font-medium text-sm text-gray-700">Accent</span>
|
|
||||||
<button type="button" id="garland-accent-chip" class="replace-chip" aria-label="Pick accent color"></button>
|
|
||||||
<button type="button" id="garland-accent-clear" class="btn-yellow text-xs px-3 py-1">Clear</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="eraser-controls" class="hidden flex flex-col gap-2">
|
|
||||||
<label class="text-sm font-medium text-gray-700">Eraser Size: <span id="eraser-size-label">30</span>px</label>
|
|
||||||
<input type="range" id="eraser-size" min="10" max="120" value="30" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
|
||||||
<p class="hint">Hover to preview. Click-drag to erase.</p>
|
|
||||||
</div>
|
|
||||||
<div id="select-controls" class="hidden flex flex-col gap-2">
|
<div id="select-controls" class="hidden flex flex-col gap-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button id="delete-selected" class="btn-danger" disabled>Delete</button>
|
<button id="delete-selected" class="btn-danger" disabled>Delete</button>
|
||||||
<button id="duplicate-selected" class="btn-dark" disabled>Duplicate</button>
|
<button id="duplicate-selected" class="btn-dark" disabled>Duplicate</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p class="hint">Drag balloons to reposition. Use arrows/touches for fine nudges.</p>
|
<div class="flex items-center gap-2 text-xs text-gray-600 mb-1">
|
||||||
|
<span class="font-semibold">Move</span>
|
||||||
|
<span class="hint">↑↓←→</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 gap-1 max-w-xs">
|
||||||
|
<button type="button" class="btn-dark nudge-selected text-sm py-2" data-dx="0" data-dy="-5" aria-label="Move Selection Up">↑</button>
|
||||||
|
<button type="button" class="btn-dark nudge-selected text-sm py-2" data-dx="5" data-dy="0" aria-label="Move Selection Right">→</button>
|
||||||
|
<button type="button" class="btn-dark nudge-selected text-sm py-2" data-dx="0" data-dy="5" aria-label="Move Selection Down">↓</button>
|
||||||
|
<button type="button" class="btn-dark nudge-selected text-sm py-2" data-dx="-5" data-dy="0" aria-label="Move Selection Left">←</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex items-center gap-2 text-xs text-gray-600">
|
<div class="mt-2 flex items-center gap-2 text-xs text-gray-600">
|
||||||
<span class="font-semibold">Resize</span>
|
<span class="font-semibold">Resize</span>
|
||||||
<input type="range" id="selected-size" min="5" max="32" step="0.5" value="11" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" disabled>
|
<input type="range" id="selected-size" min="5" max="200" value="40" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" disabled>
|
||||||
<span id="selected-size-label" class="text-xs w-12 text-right">0\"</span>
|
<span id="selected-size-label" class="text-xs w-10 text-right">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 grid grid-cols-2 gap-2">
|
<div class="mt-2 grid grid-cols-2 gap-2">
|
||||||
<button type="button" class="btn-dark text-sm py-2" id="bring-forward" disabled>Bring Forward</button>
|
<button type="button" class="btn-dark text-sm py-2" id="bring-forward" disabled>Bring Forward</button>
|
||||||
@ -139,60 +115,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-heading mt-4">Size & Shine</div>
|
<div class="panel-heading mt-4">Size & Shine</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div id="size-preset-group" class="grid grid-cols-5 gap-2 mb-2"></div>
|
<div id="size-preset-group" class="grid grid-cols-5 gap-2 mb-2"></div>
|
||||||
<p class="hint mb-3">Size presets adjust the diameter for new balloons.</p>
|
<p class="hint mb-3">Global scale lives in <code>PX_PER_INCH</code> (see <code>script.js</code>).</p>
|
||||||
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
||||||
<input id="toggle-shine-checkbox" type="checkbox" class="align-middle" checked>
|
<input id="toggle-shine-checkbox" type="checkbox" class="align-middle" checked>
|
||||||
Enable Shine
|
Enable Shine
|
||||||
</label>
|
</label>
|
||||||
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
|
||||||
<input id="toggle-border-checkbox" type="checkbox" class="align-middle">
|
|
||||||
Outline Balloons
|
|
||||||
</label>
|
|
||||||
<button type="button" id="fit-view-btn" class="btn-dark text-sm mt-3 w-full">Fit to Design</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="colors">
|
<div class="control-stack" data-mobile-tab="colors">
|
||||||
<div class="panel-heading">Organic Colors</div>
|
<div class="panel-heading">Used Colors</div>
|
||||||
<div class="panel-card space-y-4">
|
<div class="panel-card">
|
||||||
<div class="space-y-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="flex items-center gap-3">
|
<span class="text-sm text-gray-600">Built from the current design. Click a swatch to select that color.</span>
|
||||||
<span class="text-sm font-medium text-gray-700">Active color</span>
|
<button id="sort-used-toggle" class="text-sm underline">Sort: Most → Least</button>
|
||||||
<div id="current-color-chip" class="current-color-chip">
|
|
||||||
<span id="current-color-label" class="text-xs font-semibold text-slate-700"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="used-palette" class="palette-box min-h-[3rem]"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="panel-heading mt-4">Allowed Colors</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="panel-card">
|
||||||
<span class="text-sm font-semibold text-gray-700">Project Palette</span>
|
<p class="hint mb-2">Alt+Click a balloon on canvas to pick its color.</p>
|
||||||
<button id="sort-used-toggle" class="text-sm underline">Sort: Most → Least</button>
|
<div id="color-palette" class="palette-box"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="used-palette" class="palette-box min-h-[3rem]"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="panel-heading mt-4">Replace Color</div>
|
||||||
<div class="text-sm font-semibold text-gray-700">Replace Color</div>
|
<div class="panel-card">
|
||||||
<div class="flex items-center gap-2 replace-row">
|
<div class="grid grid-cols-1 gap-2">
|
||||||
<button type="button" class="replace-chip" id="replace-from-chip" aria-label="Pick color to replace"></button>
|
<label class="text-sm font-medium">From (used):</label>
|
||||||
<span class="text-xs font-semibold text-slate-500">→</span>
|
<select id="replace-from" class="select"></select>
|
||||||
<button type="button" class="replace-chip" id="replace-to-chip" aria-label="Pick replacement color"></button>
|
|
||||||
<span id="replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 gap-2">
|
|
||||||
<select id="replace-from" class="sr-only"></select>
|
|
||||||
<select id="replace-to" class="sr-only"></select>
|
|
||||||
<button id="replace-btn" class="btn-blue">Replace</button>
|
|
||||||
<p id="replace-msg" class="hint"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
<label class="text-sm font-medium">To (allowed):</label>
|
||||||
<div class="text-sm font-semibold text-gray-700">Color Library</div>
|
<select id="replace-to" class="select"></select>
|
||||||
<div id="color-palette" class="palette-box"></div>
|
|
||||||
|
<button id="replace-btn" class="btn-blue">Replace</button>
|
||||||
|
<p id="replace-msg" class="hint"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -218,7 +177,7 @@
|
|||||||
<div class="flex flex-wrap gap-3 mt-2">
|
<div class="flex flex-wrap gap-3 mt-2">
|
||||||
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
|
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
|
||||||
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
|
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
|
||||||
<p class="hint w-full">SVG export keeps vectors in Classic; Organic embeds textures.</p>
|
<p class="hint w-full">SVG currently Classic only.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -254,24 +213,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-2 space-y-2">
|
<div class="md:col-span-2 space-y-2">
|
||||||
<div class="text-sm font-medium text-gray-700">Layout</div>
|
<div class="text-sm font-medium text-gray-700">Layout</div>
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2">
|
||||||
<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-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 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 1–8</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>
|
||||||
</div>
|
</div>
|
||||||
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
|
<select id="classic-pattern" class="select align-middle hidden" aria-hidden="true" tabindex="-1">
|
||||||
@ -292,7 +236,7 @@
|
|||||||
<div id="classic-topper-toggle-row" class="flex items-center gap-3 pt-2 border-t border-gray-200 hidden">
|
<div id="classic-topper-toggle-row" class="flex items-center gap-3 pt-2 border-t border-gray-200 hidden">
|
||||||
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
||||||
<input id="classic-topper-enabled" type="checkbox" class="align-middle">
|
<input id="classic-topper-enabled" type="checkbox" class="align-middle">
|
||||||
Add Topper
|
Add Topper (24")
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -304,24 +248,10 @@
|
|||||||
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="star" aria-pressed="false"><i class="fa-solid fa-star"></i><span class="hidden sm:inline">Star</span></button>
|
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="star" aria-pressed="false"><i class="fa-solid fa-star"></i><span class="hidden sm:inline">Star</span></button>
|
||||||
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="heart" aria-pressed="false"><i class="fa-solid fa-heart"></i><span class="hidden sm:inline">Heart</span></button>
|
<button type="button" class="tab-btn topper-type-btn tab-idle" data-type="heart" aria-pressed="false"><i class="fa-solid fa-heart"></i><span class="hidden sm:inline">Heart</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
|
||||||
<div class="text-xs font-semibold text-gray-600 mb-1">Numbers</div>
|
|
||||||
<div class="topper-number-grid">
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-0" aria-pressed="false">0</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-1" aria-pressed="false">1</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-2" aria-pressed="false">2</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-3" aria-pressed="false">3</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-4" aria-pressed="false">4</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-5" aria-pressed="false">5</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-6" aria-pressed="false">6</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-7" aria-pressed="false">7</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-8" aria-pressed="false">8</button>
|
|
||||||
<button type="button" class="tab-btn topper-type-btn topper-number-btn tab-idle" data-type="num-9" aria-pressed="false">9</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:col-span-2 flex flex-wrap gap-2 justify-end">
|
<div class="sm:col-span-2">
|
||||||
<button type="button" id="classic-nudge-open" class="btn-dark text-xs px-3 py-2">Nudge Panel</button>
|
<div class="text-sm font-medium mb-1">Topper Size</div>
|
||||||
|
<input id="classic-topper-size" type="range" min="0.5" max="2" step="0.05" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -334,15 +264,10 @@
|
|||||||
<input id="classic-shine-enabled" type="checkbox" class="align-middle" checked>
|
<input id="classic-shine-enabled" type="checkbox" class="align-middle" checked>
|
||||||
Enable Shine
|
Enable Shine
|
||||||
</label>
|
</label>
|
||||||
<label class="text-sm inline-flex items-center gap-2 font-medium">
|
|
||||||
<input id="classic-border-enabled" type="checkbox" class="align-middle">
|
|
||||||
Outline Balloons
|
|
||||||
</label>
|
|
||||||
<label class="text-sm inline-flex items-center gap-2">
|
<label class="text-sm inline-flex items-center gap-2">
|
||||||
<input id="classic-reverse" type="checkbox" class="align-middle">
|
<input id="classic-reverse" type="checkbox" class="align-middle">
|
||||||
Reverse spiral
|
Reverse spiral
|
||||||
</label>
|
</label>
|
||||||
<p class="hint">Use stacked for “same color per quad” layouts; reverse flips the spiral.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -350,106 +275,47 @@
|
|||||||
<div class="control-stack" data-mobile-tab="colors">
|
<div class="control-stack" data-mobile-tab="colors">
|
||||||
<div class="panel-heading">Classic Colors</div>
|
<div class="panel-heading">Classic Colors</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div id="classic-slots" class="flex items-center gap-2"></div>
|
<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>
|
<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 text-gray-700 classic-label">Active color</span>
|
|
||||||
<div id="classic-active-chip" class="current-color-chip cursor-pointer" title="Tap to scroll to palette">
|
|
||||||
<span id="classic-active-label" class="text-xs font-semibold text-slate-700"></span>
|
|
||||||
</div>
|
</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-topper-color-block" class="mb-3 hidden">
|
<div id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
|
||||||
<div class="text-sm text-gray-700 classic-label">Topper Color</div>
|
<div class="flex flex-wrap gap-2 mt-3">
|
||||||
<div class="flex items-center gap-3 mt-1">
|
<button id="classic-randomize-colors" class="btn-dark">Randomize 5</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-heading mt-3">Topper Color</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<button id="classic-topper-color-swatch" class="slot-swatch" title="Click to change topper color">T</button>
|
<button id="classic-topper-color-swatch" class="slot-swatch" title="Click to change topper color">T</button>
|
||||||
|
<p class="hint">Select a color then click to apply.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div id="classic-project-block" class="space-y-1">
|
|
||||||
<div class="text-sm text-gray-700 classic-label">Project Palette</div>
|
|
||||||
<div id="classic-project-palette" class="palette-box min-h-[2.4rem]"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="classic-replace-block" class="mt-2">
|
|
||||||
<div class="text-sm text-gray-700 classic-label">Replace Color</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">
|
|
||||||
<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>
|
|
||||||
<div id="classic-swatch-grid" class="palette-box min-h-[3rem]"></div>
|
|
||||||
<div class="flex flex-wrap gap-2 mt-3">
|
|
||||||
<button id="classic-randomize-colors" class="btn-dark">Randomize</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="save">
|
<div class="control-stack" data-mobile-tab="save">
|
||||||
<div class="panel-heading">Save & Share</div>
|
<div class="panel-heading">Save & Share</div>
|
||||||
<div class="panel-card space-y-3">
|
<div class="panel-card space-y-3">
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
|
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
|
||||||
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
|
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
|
||||||
<p class="hint w-full">SVG keeps the vector Classic layout; PNG is raster.</p>
|
<p class="hint w-full">SVG recommended for Classic.</p>
|
||||||
|
</div>
|
||||||
|
<p class="hint text-red-500">Classic JSON save/load not available yet.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint text-red-500">Classic JSON save/load coming soon.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section id="classic-canvas-panel"
|
<section id="classic-canvas-panel"
|
||||||
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">
|
class="order-1 w-full lg:flex-1 flex flex-col items-stretch shadow-x3 rounded-2xl overflow-hidden bg-white">
|
||||||
<div id="classic-display"
|
<div id="classic-display"
|
||||||
class="rounded-xl"
|
class="rounded-xl"
|
||||||
style="width:100%;height:72vh;border:1px solid #e5e7eb;background:#fff;overflow:auto;"></div>
|
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 id="floating-topper-nudge" class="floating-nudge hidden">
|
||||||
<div class="floating-nudge-header">
|
<div class="floating-nudge-header">
|
||||||
<div class="panel-heading">Nudge & Size Topper</div>
|
<div class="panel-heading">Nudge Topper</div>
|
||||||
<button type="button" id="floating-nudge-toggle" class="btn-dark text-xs px-3 py-2" aria-label="Close nudge panel">×</button>
|
<button type="button" id="floating-nudge-toggle" class="btn-dark text-xs px-3 py-2">Hide</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="floating-nudge-body space-y-3">
|
<div class="floating-nudge-body">
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<div></div>
|
<div></div>
|
||||||
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="0.5" aria-label="Move Topper Up">↑</button>
|
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="0.5" aria-label="Move Topper Up">↑</button>
|
||||||
@ -461,140 +327,11 @@
|
|||||||
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="-0.5" aria-label="Move Topper Down">↓</button>
|
<button type="button" class="btn-dark nudge-topper" data-dx="0" data-dy="-0.5" aria-label="Move Topper Down">↓</button>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<div class="text-sm font-medium mb-1">Topper Size</div>
|
|
||||||
<input id="classic-topper-size" type="range" min="0.5" max="2" step="0.05" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="tab-wall" class="hidden flex flex-col lg:flex-row gap-4 lg:h-[calc(100vh-10rem)]">
|
|
||||||
<aside id="wall-controls-panel" class="control-sheet lg:static lg:w-[360px] lg:max-h-none lg:overflow-y-auto">
|
|
||||||
<div class="panel-header-row">
|
|
||||||
<h2 class="panel-title">Wall Controls</h2>
|
|
||||||
</div>
|
|
||||||
<div class="control-stack" data-mobile-tab="controls">
|
|
||||||
<div class="panel-heading">Grid</div>
|
|
||||||
<div class="panel-card grid grid-cols-2 gap-3">
|
|
||||||
<label class="text-sm font-medium flex flex-col gap-1">Columns
|
|
||||||
<input id="wall-cols" type="number" min="2" max="20" step="1" value="9" class="w-full px-2 py-1 border rounded">
|
|
||||||
</label>
|
|
||||||
<label class="text-sm font-medium flex flex-col gap-1">Rows
|
|
||||||
<input id="wall-rows" type="number" min="2" max="20" step="1" value="7" class="w-full px-2 py-1 border rounded">
|
|
||||||
</label>
|
|
||||||
<label class="text-sm font-medium flex flex-col gap-1 col-span-2">Pattern
|
|
||||||
<select id="wall-pattern" class="select">
|
|
||||||
<option value="grid">Square Grid</option>
|
|
||||||
<option value="x">X / Diamond</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="text-sm font-medium inline-flex items-center gap-2 col-span-2">
|
|
||||||
<input id="wall-show-wire" type="checkbox" class="align-middle" checked>
|
|
||||||
Show wireframe for empty spots
|
|
||||||
</label>
|
|
||||||
<label class="text-sm font-medium inline-flex items-center gap-2 col-span-2">
|
|
||||||
<input id="wall-outline" type="checkbox" class="align-middle" checked>
|
|
||||||
Outline balloons
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-heading mt-4">Tools</div>
|
|
||||||
<div class="panel-card">
|
|
||||||
<div class="wall-toolbar">
|
|
||||||
<button type="button" id="wall-tool-paint" class="tool-btn" aria-pressed="true">
|
|
||||||
<i class="fa-solid fa-brush"></i>
|
|
||||||
<span>Paint</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" id="wall-tool-erase" class="tool-btn" aria-pressed="false">
|
|
||||||
<i class="fa-solid fa-eraser"></i>
|
|
||||||
<span>Erase</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="hint mt-2 text-xs">Paint applies the active color; Erase clears. Hold modifier on desktop to erase temporarily.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="colors">
|
|
||||||
<div class="panel-heading">Wall Colors</div>
|
|
||||||
<div class="panel-card space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<span class="text-sm font-medium text-gray-700">Active color</span>
|
|
||||||
<div id="wall-active-color-chip" class="current-color-chip">
|
|
||||||
<span id="wall-active-color-label" class="text-xs font-semibold text-slate-700"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="text-sm font-semibold text-gray-700">Project Palette</span>
|
|
||||||
</div>
|
|
||||||
<div id="wall-used-palette" class="palette-box min-h-[2.4rem]"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center gap-2 replace-row">
|
|
||||||
<button type="button" class="replace-chip" id="wall-replace-from-chip" aria-label="Pick wall color to replace"></button>
|
|
||||||
<span class="text-xs font-semibold text-slate-500">→</span>
|
|
||||||
<button type="button" class="replace-chip" id="wall-replace-to-chip" aria-label="Pick wall replacement color"></button>
|
|
||||||
<span id="wall-replace-count" class="text-xs text-slate-500 ml-auto"></span>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 gap-2">
|
|
||||||
<select id="wall-replace-from" class="sr-only"></select>
|
|
||||||
<select id="wall-replace-to" class="sr-only"></select>
|
|
||||||
<button type="button" id="wall-replace-btn" class="btn-dark text-sm">Replace</button>
|
|
||||||
<div id="wall-replace-msg" class="text-xs text-gray-500"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="text-sm font-semibold text-gray-700">Color Library</div>
|
|
||||||
<div id="wall-palette" class="palette-box min-h-[3rem]"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-stack" data-mobile-tab="save">
|
|
||||||
<div class="panel-heading">Save & Export</div>
|
|
||||||
<div class="panel-card space-y-3">
|
|
||||||
<div class="flex flex-wrap gap-3">
|
|
||||||
<button class="btn-dark bg-blue-600" data-export="png">Export PNG</button>
|
|
||||||
<button class="btn-dark bg-blue-700" data-export="svg">Export SVG</button>
|
|
||||||
<p class="hint w-full">Exports the current wall view.</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="panel-heading text-sm">Quick Paint (uses active color)</div>
|
|
||||||
<div class="grid grid-cols-2 gap-2 mt-2">
|
|
||||||
<button type="button" id="wall-paint-links" class="btn-blue text-xs px-2 py-2">Paint Links</button>
|
|
||||||
<button type="button" id="wall-paint-small" class="btn-blue text-xs px-2 py-2">Paint 5" Nodes</button>
|
|
||||||
<button type="button" id="wall-paint-gaps" class="btn-blue text-xs px-2 py-2">Paint 11" Gaps</button>
|
|
||||||
</div>
|
|
||||||
<p class="hint mt-2">Fills only that group; pattern-aware for Grid vs X.</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap gap-3">
|
|
||||||
<button type="button" id="wall-clear" class="btn-danger text-sm px-3 py-2 flex-1">Clear</button>
|
|
||||||
<button type="button" id="wall-fill-all" class="btn-blue text-sm px-3 py-2 flex-1">Fill All</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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"
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
<div id="wall-display" class="flex-1 bg-white relative overflow-auto">
|
|
||||||
<div class="p-6 text-gray-500 text-sm">Wall designer will load here.</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="mobile-tabbar" class="mobile-tabbar">
|
<div id="mobile-tabbar" class="mobile-tabbar">
|
||||||
@ -612,71 +349,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile sticky action bar -->
|
|
||||||
<div id="mobile-action-bar" class="mobile-action-bar hidden">
|
|
||||||
<div class="mobile-action-chip" id="mobile-active-color-chip" title="Active color"></div>
|
|
||||||
<div class="mobile-action-row">
|
|
||||||
<button type="button" class="mobile-action-btn" id="mobile-act-undo" 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="mobile-act-redo" 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="mobile-act-eyedrop" aria-label="Eyedropper">
|
|
||||||
<i class="fa-solid fa-eye-dropper" aria-hidden="true"></i>
|
|
||||||
<span>Pick</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="mobile-action-btn" id="mobile-act-erase" aria-label="Toggle Erase">
|
|
||||||
<i class="fa-solid fa-eraser" aria-hidden="true"></i>
|
|
||||||
<span>Erase</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="mobile-action-btn danger" id="mobile-act-clear" aria-label="Clear canvas">
|
|
||||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
|
||||||
<span>Clear</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="mobile-action-btn" id="mobile-act-export" aria-label="Export PNG">
|
|
||||||
<i class="fa-solid fa-download" aria-hidden="true"></i>
|
|
||||||
<span>Export</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Color picker modal -->
|
|
||||||
<div id="color-picker-modal" class="color-modal hidden" role="dialog" aria-modal="true" aria-labelledby="color-picker-title">
|
|
||||||
<div class="color-modal-backdrop"></div>
|
|
||||||
<div class="color-modal-card">
|
|
||||||
<div class="color-modal-header">
|
|
||||||
<div>
|
|
||||||
<div id="color-picker-title" class="color-modal-title">Choose a color</div>
|
|
||||||
<div id="color-picker-subtitle" class="color-modal-subtitle"></div>
|
|
||||||
</div>
|
|
||||||
<button type="button" id="color-picker-close" class="color-modal-close" aria-label="Close">×</button>
|
|
||||||
</div>
|
|
||||||
<div id="color-picker-grid" class="color-modal-grid"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Export modal -->
|
|
||||||
<div id="export-modal" class="color-modal hidden" role="dialog" aria-modal="true" aria-labelledby="export-modal-title">
|
|
||||||
<div class="color-modal-backdrop"></div>
|
|
||||||
<div class="color-modal-card">
|
|
||||||
<div class="color-modal-header">
|
|
||||||
<div>
|
|
||||||
<div id="export-modal-title" class="color-modal-title">Export design</div>
|
|
||||||
<div class="color-modal-subtitle">Choose a format to download</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" id="export-modal-close" class="color-modal-close" aria-label="Close">×</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col sm:flex-row gap-3">
|
|
||||||
<button type="button" class="btn-blue flex-1" data-export-choice="png">Export PNG</button>
|
|
||||||
<button type="button" class="btn-dark flex-1" data-export-choice="svg">Export SVG</button>
|
|
||||||
</div>
|
|
||||||
<p class="hint mt-2 text-xs text-slate-500">SVG keeps vector shapes where possible (textures/images stay raster). PNG renders a high-res snapshot.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="message-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
<div id="message-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-gray-900 bg-opacity-50">
|
||||||
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm text-center">
|
<div class="bg-white p-6 rounded-lg shadow-lg max-w-sm text-center">
|
||||||
<p id="modal-text" class="text-gray-800 text-lg"></p>
|
<p id="modal-text" class="text-gray-800 text-lg"></p>
|
||||||
@ -686,25 +358,9 @@
|
|||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js" defer></script>
|
<script src="https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js" defer></script>
|
||||||
|
|
||||||
<!-- Palette must load before shared.js; it is already included in the <head>. -->
|
|
||||||
<script src="shared.js" defer></script>
|
|
||||||
<script src="script.js" defer></script>
|
<script src="script.js" defer></script>
|
||||||
<script src="organic.js" defer></script>
|
|
||||||
<script src="wall.js" defer></script>
|
|
||||||
<script src="classic.js" defer></script>
|
|
||||||
|
|
||||||
<div id="classic-quad-modal" class="quad-modal hidden" aria-hidden="true">
|
<script src="classic.js" defer></script>
|
||||||
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
2166
organic.js
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 95 KiB |
233
shared.js
@ -1,233 +0,0 @@
|
|||||||
(() => {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// Shared helpers & palette flattening
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const PX_PER_INCH = 4;
|
|
||||||
const SIZE_PRESETS = [24, 18, 11, 9, 5];
|
|
||||||
const TEXTURE_ZOOM_DEFAULT = 1.8;
|
|
||||||
const TEXTURE_FOCUS_DEFAULT = { x: 0.5, y: 0.5 };
|
|
||||||
const SWATCH_TEXTURE_ZOOM = 2.5;
|
|
||||||
const PNG_EXPORT_SCALE = 3;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const r = parseInt(h[0] + h[0], 16);
|
|
||||||
const g = parseInt(h[1] + h[1], 16);
|
|
||||||
const b = parseInt(h[2] + h[2], 16);
|
|
||||||
return { r, g, b };
|
|
||||||
}
|
|
||||||
if (h.length === 6) {
|
|
||||||
const r = parseInt(h.slice(0,2), 16);
|
|
||||||
const g = parseInt(h.slice(2,4), 16);
|
|
||||||
const b = parseInt(h.slice(4,6), 16);
|
|
||||||
return { r, g, b };
|
|
||||||
}
|
|
||||||
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 => {
|
|
||||||
const c = v / 255;
|
|
||||||
return c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4);
|
|
||||||
});
|
|
||||||
return 0.2126*norm[0] + 0.7152*norm[1] + 0.0722*norm[2];
|
|
||||||
}
|
|
||||||
function shineStyle(colorHex) {
|
|
||||||
const hex = normalizeHex(colorHex);
|
|
||||||
const isRetroWhite = hex === '#e8e3d9';
|
|
||||||
const isPureWhite = hex === '#ffffff';
|
|
||||||
const lum = luminance(hex);
|
|
||||||
if (isPureWhite || isRetroWhite) {
|
|
||||||
return { fill: 'rgba(220,220,220,0.22)', stroke: null };
|
|
||||||
}
|
|
||||||
if (lum > 0.7) {
|
|
||||||
const t = clamp01((lum - 0.7) / 0.3);
|
|
||||||
const fillAlpha = 0.08 + (0.04 - 0.08) * t;
|
|
||||||
return { fill: `rgba(0,0,0,${fillAlpha})`, stroke: null };
|
|
||||||
}
|
|
||||||
const base = 0.20;
|
|
||||||
const softened = lum > 0.4 ? base * 0.7 : base;
|
|
||||||
const finalAlpha = isRetroWhite ? softened * 0.6 : softened;
|
|
||||||
return { fill: `rgba(255,255,255,${finalAlpha})`, stroke: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const FLAT_COLORS = [];
|
|
||||||
const NAME_BY_HEX = new Map();
|
|
||||||
const HEX_TO_FIRST_IDX = new Map();
|
|
||||||
const allowedSet = new Set();
|
|
||||||
|
|
||||||
(function buildFlat() {
|
|
||||||
if (!Array.isArray(window.PALETTE)) return;
|
|
||||||
window.PALETTE.forEach(group => {
|
|
||||||
(group.colors || []).forEach(c => {
|
|
||||||
if (!c?.hex) return;
|
|
||||||
const item = { ...c, family: group.family };
|
|
||||||
item.imageZoom = Number.isFinite(c.imageZoom) ? Math.max(1, c.imageZoom) : TEXTURE_ZOOM_DEFAULT;
|
|
||||||
item.imageFocus = {
|
|
||||||
x: clamp01(c.imageFocusX ?? c.imageFocus?.x ?? TEXTURE_FOCUS_DEFAULT.x),
|
|
||||||
y: clamp01(c.imageFocusY ?? c.imageFocus?.y ?? TEXTURE_FOCUS_DEFAULT.y)
|
|
||||||
};
|
|
||||||
item._idx = FLAT_COLORS.length;
|
|
||||||
FLAT_COLORS.push(item);
|
|
||||||
|
|
||||||
const key = (c.hex || '').toLowerCase();
|
|
||||||
if (!NAME_BY_HEX.has(key)) NAME_BY_HEX.set(key, c.name);
|
|
||||||
if (!HEX_TO_FIRST_IDX.has(key)) HEX_TO_FIRST_IDX.set(key, item._idx);
|
|
||||||
allowedSet.add(key);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
const IMG_CACHE = new Map();
|
|
||||||
function getImage(path, onLoad) {
|
|
||||||
if (!path) return null;
|
|
||||||
let img = IMG_CACHE.get(path);
|
|
||||||
if (!img) {
|
|
||||||
img = new Image();
|
|
||||||
// Avoid CORS issues on file:// by only setting crossOrigin for http/https
|
|
||||||
const href = (() => { try { return new URL(path, window.location.href); } catch { return null; } })();
|
|
||||||
const isFile = href?.protocol === 'file:' || window.location.protocol === 'file:';
|
|
||||||
if (!isFile) img.crossOrigin = 'anonymous';
|
|
||||||
img.decoding = 'async';
|
|
||||||
img.loading = 'eager';
|
|
||||||
img.src = path;
|
|
||||||
if (onLoad) img.onload = onLoad;
|
|
||||||
IMG_CACHE.set(path, img);
|
|
||||||
}
|
|
||||||
return img;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATA_URL_CACHE = new Map();
|
|
||||||
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
||||||
const blobToDataUrl = blob => new Promise((resolve, reject) => {
|
|
||||||
const r = new FileReader();
|
|
||||||
r.onloadend = () => resolve(r.result);
|
|
||||||
r.onerror = reject;
|
|
||||||
r.readAsDataURL(blob);
|
|
||||||
});
|
|
||||||
function imageToDataUrl(img) {
|
|
||||||
if (!img || !img.complete || img.naturalWidth === 0) return null;
|
|
||||||
// On file:// origins, drawing may be blocked; return null to fall back to original href.
|
|
||||||
if (window.location.protocol === 'file:') return null;
|
|
||||||
try {
|
|
||||||
const c = document.createElement('canvas');
|
|
||||||
c.width = img.naturalWidth;
|
|
||||||
c.height = img.naturalHeight;
|
|
||||||
c.getContext('2d').drawImage(img, 0, 0);
|
|
||||||
return c.toDataURL('image/png');
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[Export] imageToDataUrl failed:', err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function imageUrlToDataUrl(src) {
|
|
||||||
if (!src || src.startsWith('data:')) return src;
|
|
||||||
if (DATA_URL_CACHE.has(src)) return DATA_URL_CACHE.get(src);
|
|
||||||
const abs = (() => { try { return new URL(src, window.location.href).href; } catch { return src; } })();
|
|
||||||
const urlObj = (() => { try { return new URL(abs); } catch { return null; } })();
|
|
||||||
const isFile = urlObj?.protocol === 'file:';
|
|
||||||
|
|
||||||
const cachedImg = IMG_CACHE.get(src);
|
|
||||||
const cachedUrl = imageToDataUrl(cachedImg);
|
|
||||||
if (cachedUrl) { DATA_URL_CACHE.set(src, cachedUrl); return cachedUrl; }
|
|
||||||
|
|
||||||
let dataUrl = null;
|
|
||||||
try {
|
|
||||||
if (isFile) {
|
|
||||||
// On file:// we cannot safely read pixels; return the original path.
|
|
||||||
dataUrl = src;
|
|
||||||
} else {
|
|
||||||
const resp = await fetch(abs);
|
|
||||||
if (!resp.ok) throw new Error(`Status ${resp.status}`);
|
|
||||||
dataUrl = await blobToDataUrl(await resp.blob());
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!isFile) console.warn('[Export] Fetch failed for', abs, err);
|
|
||||||
dataUrl = await new Promise(resolve => {
|
|
||||||
const img = new Image();
|
|
||||||
if (!isFile) img.crossOrigin = 'anonymous';
|
|
||||||
img.onload = () => {
|
|
||||||
try {
|
|
||||||
const c = document.createElement('canvas');
|
|
||||||
c.width = img.naturalWidth || 1;
|
|
||||||
c.height = img.naturalHeight || 1;
|
|
||||||
c.getContext('2d').drawImage(img, 0, 0);
|
|
||||||
resolve(c.toDataURL('image/png'));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[Export] Canvas fallback failed for', abs, e);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
img.onerror = () => resolve(null);
|
|
||||||
img.src = abs;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!dataUrl) dataUrl = abs;
|
|
||||||
DATA_URL_CACHE.set(src, dataUrl);
|
|
||||||
return dataUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
function download(href, suggestedFilename) {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = href;
|
|
||||||
a.download = suggestedFilename || 'download';
|
|
||||||
a.rel = 'noopener';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
a.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.shared = {
|
|
||||||
PX_PER_INCH,
|
|
||||||
SIZE_PRESETS,
|
|
||||||
TEXTURE_ZOOM_DEFAULT,
|
|
||||||
TEXTURE_FOCUS_DEFAULT,
|
|
||||||
SWATCH_TEXTURE_ZOOM,
|
|
||||||
PNG_EXPORT_SCALE,
|
|
||||||
clamp,
|
|
||||||
clamp01,
|
|
||||||
normalizeHex,
|
|
||||||
hexToRgb,
|
|
||||||
shineStyle,
|
|
||||||
luminance,
|
|
||||||
FLAT_COLORS,
|
|
||||||
NAME_BY_HEX,
|
|
||||||
HEX_TO_FIRST_IDX,
|
|
||||||
allowedSet,
|
|
||||||
getImage,
|
|
||||||
DATA_URL_CACHE,
|
|
||||||
XLINK_NS,
|
|
||||||
blobToDataUrl,
|
|
||||||
imageToDataUrl,
|
|
||||||
imageUrlToDataUrl,
|
|
||||||
download,
|
|
||||||
getActiveColor,
|
|
||||||
setActiveColor
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
494
style.css
@ -1,78 +1,16 @@
|
|||||||
/* Minimal extras (Tailwind handles most styling) */
|
/* Minimal extras (Tailwind handles most styling) */
|
||||||
body { color: #1f2937; }
|
body { color: #1f2937; }
|
||||||
body[data-active-tab="#tab-classic"] #clear-canvas-btn-top,
|
|
||||||
body[data-active-tab="#tab-wall"] #clear-canvas-btn-top {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
white-space: nowrap;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app-header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 35;
|
|
||||||
background: rgba(255,255,255,0.92);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
padding: .75rem 0.5rem;
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
box-shadow: 0 8px 24px rgba(15,23,42,0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
#balloon-canvas { touch-action: none;
|
|
||||||
height: 95%}
|
|
||||||
|
|
||||||
.classic-expanded-canvas {
|
|
||||||
height: 130vh !important;
|
|
||||||
min-height: 130vh;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#balloon-canvas { touch-action: none; }
|
||||||
|
|
||||||
.balloon-canvas {
|
.balloon-canvas {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
box-shadow: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06);
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06);
|
||||||
border: 1px black solid;
|
border: 1px black solid;
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
.btn-dark,
|
|
||||||
.btn-blue,
|
|
||||||
.btn-green,
|
|
||||||
.btn-yellow,
|
|
||||||
.btn-danger,
|
|
||||||
.btn-indigo {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: .35rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
border: 0;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
.btn-dark:focus-visible,
|
|
||||||
.btn-blue:focus-visible,
|
|
||||||
.btn-green:focus-visible,
|
|
||||||
.btn-yellow:focus-visible,
|
|
||||||
.btn-danger:focus-visible,
|
|
||||||
.btn-indigo:focus-visible,
|
|
||||||
.tool-btn:focus-visible {
|
|
||||||
outline: 2px solid #6366f1;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
.tool-btn {
|
.tool-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -89,7 +27,6 @@ height: 95%}
|
|||||||
.tool-btn svg { width: 1.1em; height: 1.1em; fill: currentColor; }
|
.tool-btn svg { width: 1.1em; height: 1.1em; fill: currentColor; }
|
||||||
.tool-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
|
.tool-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
|
||||||
.tool-btn[aria-pressed="true"] { background:#3b82f6; color:#fff; border-color:#3b82f6; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); }
|
.tool-btn[aria-pressed="true"] { background:#3b82f6; color:#fff; border-color:#3b82f6; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); }
|
||||||
.tool-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; }
|
|
||||||
|
|
||||||
/* Base button style - Slate Gradient */
|
/* Base button style - Slate Gradient */
|
||||||
.btn-dark { background: linear-gradient(135deg, #334155, #0f172a); color:#fff; padding:.6rem .8rem; border-radius:.75rem; transition: all 0.2s; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.15); }
|
.btn-dark { background: linear-gradient(135deg, #334155, #0f172a); color:#fff; padding:.6rem .8rem; border-radius:.75rem; transition: all 0.2s; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.15); }
|
||||||
@ -128,14 +65,12 @@ height: 95%}
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
background: rgba(255,255,255,0.82);
|
background: rgba(255,255,255,0.6); /* More transparent */
|
||||||
border: 1px solid rgba(226,232,240,0.9);
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: .9rem;
|
border-radius: .75rem;
|
||||||
max-height: 260px;
|
max-height: 260px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
|
||||||
touch-action: pan-y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
@ -153,27 +88,9 @@ height: 95%}
|
|||||||
.swatch:hover { transform: scale(1.1); z-index: 10; }
|
.swatch:hover { transform: scale(1.1); z-index: 10; }
|
||||||
.swatch:focus-visible { outline: 2px solid #6366f1; outline-offset: 2px; }
|
.swatch:focus-visible { outline: 2px solid #6366f1; outline-offset: 2px; }
|
||||||
.swatch.active { outline: 2px solid #6366f1; outline-offset: 2px; }
|
.swatch.active { outline: 2px solid #6366f1; outline-offset: 2px; }
|
||||||
.swatch.tiny { width: 1.4rem; height: 1.4rem; border-width: 1px; box-shadow: none; }
|
|
||||||
|
|
||||||
.current-color-chip {
|
|
||||||
min-width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: 2px solid rgba(51,65,85,0.15);
|
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 .6rem;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swatch-row { display:flex; flex-wrap:wrap; gap:.5rem; }
|
.swatch-row { display:flex; flex-wrap:wrap; gap:.5rem; }
|
||||||
.family-title { font-weight:700; color:#334155; margin-top:.25rem; font-size:.9rem; letter-spacing: -0.01em; }
|
.family-title { font-weight:700; color:#334155; margin-top:.25rem; font-size:.9rem; letter-spacing: -0.01em; }
|
||||||
#wall-display { min-height: 60vh; }
|
|
||||||
#wall-display svg { width: 100%; height: 100%; display: block; }
|
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
position:absolute;
|
position:absolute;
|
||||||
@ -203,14 +120,7 @@ height: 95%}
|
|||||||
|
|
||||||
#classic-swatch-grid .sw { width: 24px; height: 24px; border-radius: 6px; border: 1px solid rgba(0,0,0,.1); cursor: pointer; }
|
#classic-swatch-grid .sw { width: 24px; height: 24px; border-radius: 6px; border: 1px solid rgba(0,0,0,.1); cursor: pointer; }
|
||||||
#classic-swatch-grid .sw:focus { outline: 2px solid #2563eb; outline-offset: 2px; }
|
#classic-swatch-grid .sw:focus { outline: 2px solid #2563eb; outline-offset: 2px; }
|
||||||
.slot-btn { position: relative; overflow: hidden; }
|
|
||||||
.slot-btn[aria-pressed="true"] { background:#3b82f6; color:#fff; }
|
.slot-btn[aria-pressed="true"] { background:#3b82f6; color:#fff; }
|
||||||
.slot-btn.slot-active {
|
|
||||||
box-shadow: 0 0 0 3px rgba(255,255,255,0.95);
|
|
||||||
outline: 3px solid #f97316;
|
|
||||||
outline-offset: 3px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.slot-container {
|
.slot-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -239,7 +149,6 @@ height: 95%}
|
|||||||
color: rgba(0,0,0,0.4);
|
color: rgba(0,0,0,0.4);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-swatch:hover {
|
.slot-swatch:hover {
|
||||||
@ -250,35 +159,6 @@ height: 95%}
|
|||||||
border-color: #2563eb;
|
border-color: #2563eb;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
.slot-swatch.active::after { display: none; }
|
|
||||||
|
|
||||||
.replace-row {
|
|
||||||
padding: 0.35rem;
|
|
||||||
background: rgba(248,250,252,0.9);
|
|
||||||
border: 1px solid rgba(226,232,240,0.9);
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
.replace-chip {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 2px solid rgba(51,65,85,0.18);
|
|
||||||
box-shadow: 0 3px 8px rgba(0,0,0,0.06);
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-color: #fff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wall-toolbar {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.wall-toolbar .tool-btn {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topper-type-group {
|
.topper-type-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -293,31 +173,6 @@ height: 95%}
|
|||||||
gap: .4rem;
|
gap: .4rem;
|
||||||
}
|
}
|
||||||
.topper-type-btn i { font-size: 1rem; }
|
.topper-type-btn i { font-size: 1rem; }
|
||||||
.topper-number-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin-top: 0.4rem;
|
|
||||||
}
|
|
||||||
.topper-number-btn {
|
|
||||||
padding: 0.4rem 0.5rem;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
.number-tint-row {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
padding: 0.65rem 0.75rem;
|
|
||||||
background: rgba(255,255,255,0.7);
|
|
||||||
border: 1px solid rgba(226, 232, 240, 0.9);
|
|
||||||
border-radius: 0.85rem;
|
|
||||||
}
|
|
||||||
.number-tint-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-nudge {
|
.floating-nudge {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -326,30 +181,23 @@ height: 95%}
|
|||||||
background: rgba(255,255,255,0.95);
|
background: rgba(255,255,255,0.95);
|
||||||
border: 1px solid rgba(148,163,184,0.3);
|
border: 1px solid rgba(148,163,184,0.3);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
padding: 0.55rem;
|
padding: 0.75rem;
|
||||||
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
|
||||||
width: 180px;
|
width: 210px;
|
||||||
max-width: 85vw;
|
|
||||||
z-index: 35;
|
z-index: 35;
|
||||||
touch-action: none;
|
|
||||||
}
|
}
|
||||||
.floating-nudge-header {
|
.floating-nudge-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: .35rem;
|
gap: .5rem;
|
||||||
margin-bottom: .25rem;
|
margin-bottom: .35rem;
|
||||||
}
|
}
|
||||||
.floating-nudge-body.collapsed { display: none; }
|
.floating-nudge-body.collapsed { display: none; }
|
||||||
.floating-nudge.collapsed .floating-nudge-body { display: none; }
|
.floating-nudge.collapsed .floating-nudge-body { display: none; }
|
||||||
.floating-nudge.collapsed #floating-nudge-toggle { opacity: 0.8; }
|
.floating-nudge.collapsed #floating-nudge-toggle { opacity: 0.8; }
|
||||||
.floating-nudge.dragging { cursor: grabbing; }
|
@media (min-width: 1024px) {
|
||||||
.floating-nudge .panel-heading { font-size: 0.95rem; margin-bottom: 0; }
|
.floating-nudge { display: none !important; }
|
||||||
.floating-nudge .btn-dark { padding: 0.35rem 0.5rem; font-size: 0.8rem; }
|
|
||||||
.floating-nudge-tab { display: none; }
|
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
|
||||||
.floating-nudge { bottom: 6rem; left: auto; right: 0.9rem; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slot-label {
|
.slot-label {
|
||||||
@ -365,13 +213,13 @@ height: 95%}
|
|||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
.panel-card {
|
.panel-card {
|
||||||
background: rgba(255,255,255,0.82);
|
background: rgba(255,255,255,0.7);
|
||||||
backdrop-filter: blur(14px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(14px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border: 1px solid rgba(226,232,240,0.9);
|
border: 1px solid rgba(255,255,255,0.6);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow: 0 12px 30px rgba(15,23,42,0.06);
|
box-shadow: 0 4px 20px rgba(0,0,0,0.03);
|
||||||
}
|
}
|
||||||
.control-stack {
|
.control-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -396,7 +244,6 @@ height: 95%}
|
|||||||
z-index: 30;
|
z-index: 30;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
height: 92%;
|
|
||||||
}
|
}
|
||||||
.control-sheet.hidden { display: none; }
|
.control-sheet.hidden { display: none; }
|
||||||
.control-sheet.minimized { transform: translateY(100%); }
|
.control-sheet.minimized { transform: translateY(100%); }
|
||||||
@ -430,245 +277,20 @@ height: 92%;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
body { padding-bottom: 0; overflow: auto; }
|
body { padding-bottom: 88px; }
|
||||||
html, body { height: auto; overflow: auto; }
|
html, body { height: 100%; overflow: hidden; }
|
||||||
#current-color-chip-global { display: none; }
|
|
||||||
#clear-canvas-btn-top { display: none !important; }
|
|
||||||
/* Add breathing room under canvases so sheets/tabbar don’t cover content */
|
|
||||||
#classic-display,
|
|
||||||
#wall-display,
|
|
||||||
#balloon-canvas {
|
|
||||||
margin-bottom: 6.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stack switching: show only the active mobile tab stack across panels */
|
|
||||||
.control-sheet .control-stack { display: none; }
|
.control-sheet .control-stack { display: none; }
|
||||||
body[data-mobile-tab="controls"] #controls-panel [data-mobile-tab="controls"],
|
body[data-mobile-tab="controls"] #controls-panel [data-mobile-tab="controls"],
|
||||||
body[data-mobile-tab="colors"] #controls-panel [data-mobile-tab="colors"],
|
body[data-mobile-tab="colors"] #controls-panel [data-mobile-tab="colors"],
|
||||||
body[data-mobile-tab="save"] #controls-panel [data-mobile-tab="save"],
|
body[data-mobile-tab="save"] #controls-panel [data-mobile-tab="save"],
|
||||||
body[data-mobile-tab="controls"] #classic-controls-panel [data-mobile-tab="controls"],
|
body[data-mobile-tab="controls"] #classic-controls-panel [data-mobile-tab="controls"],
|
||||||
body[data-mobile-tab="colors"] #classic-controls-panel [data-mobile-tab="colors"],
|
body[data-mobile-tab="colors"] #classic-controls-panel [data-mobile-tab="colors"],
|
||||||
body[data-mobile-tab="save"] #classic-controls-panel [data-mobile-tab="save"],
|
body[data-mobile-tab="save"] #classic-controls-panel [data-mobile-tab="save"] {
|
||||||
body[data-mobile-tab="controls"] #wall-controls-panel [data-mobile-tab="controls"],
|
|
||||||
body[data-mobile-tab="colors"] #wall-controls-panel [data-mobile-tab="colors"],
|
|
||||||
body[data-mobile-tab="save"] #wall-controls-panel [data-mobile-tab="save"] {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.control-sheet { bottom: 4.5rem; max-height: 55vh; }
|
|
||||||
.control-sheet.minimized { transform: translateY(115%); }
|
|
||||||
|
|
||||||
/* Larger tap targets and spacing */
|
|
||||||
.tool-btn,
|
|
||||||
.btn-dark,
|
|
||||||
.btn-blue,
|
|
||||||
.btn-green,
|
|
||||||
.btn-yellow,
|
|
||||||
.btn-danger,
|
|
||||||
.btn-indigo {
|
|
||||||
min-height: 44px;
|
|
||||||
padding: 0.75rem 0.85rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
.swatch { width: 2.4rem; height: 2.4rem; }
|
|
||||||
.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 {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 4.75rem;
|
|
||||||
padding: 0.35rem 0.75rem 0.7rem;
|
|
||||||
background: linear-gradient(180deg, rgba(255,255,255,0.72) 0%, rgba(255,255,255,0.96) 100%);
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
-webkit-backdrop-filter: blur(18px);
|
|
||||||
border-top: 1px solid rgba(226,232,240,0.9);
|
|
||||||
box-shadow: 0 -10px 30px rgba(15,23,42,0.08);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
z-index: 20; /* below control sheets (30) and modals (60) */
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-modal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 60;
|
|
||||||
}
|
|
||||||
.color-modal.hidden { display: none; }
|
|
||||||
.color-modal-backdrop {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(15,23,42,0.35);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
.color-modal-card {
|
|
||||||
position: relative;
|
|
||||||
width: min(640px, 92vw);
|
|
||||||
max-height: 80vh;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
padding: 1.1rem 1.1rem 1.25rem;
|
|
||||||
box-shadow: 0 24px 60px rgba(15,23,42,0.2);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.color-modal-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
.color-modal-title { font-size: 1.1rem; font-weight: 800; color: #0f172a; letter-spacing: -0.01em; }
|
|
||||||
.color-modal-subtitle { font-size: 0.9rem; color: #475569; }
|
|
||||||
.color-modal-close {
|
|
||||||
background: #e2e8f0;
|
|
||||||
border: none;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
.color-modal-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
|
||||||
gap: 0.75rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0.25rem;
|
|
||||||
}
|
|
||||||
.color-option {
|
|
||||||
border: 1px solid rgba(226,232,240,0.9);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0.5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 4px 12px rgba(15,23,42,0.05);
|
|
||||||
transition: transform 0.1s ease;
|
|
||||||
}
|
|
||||||
.color-option:hover { transform: translateY(-1px); }
|
|
||||||
.color-option .swatch {
|
|
||||||
width: 2.4rem;
|
|
||||||
height: 2.4rem;
|
|
||||||
border-width: 2px;
|
|
||||||
}
|
|
||||||
.color-option .label {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
.color-option .meta {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: #475569;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.mobile-action-bar.hidden { display: none; }
|
|
||||||
.mobile-action-chip {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 14px;
|
|
||||||
border: 2px solid rgba(51,65,85,0.18);
|
|
||||||
box-shadow: 0 4px 10px rgba(0,0,0,0.08);
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
.mobile-action-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
||||||
gap: 0.35rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.mobile-action-btn {
|
|
||||||
background: rgba(255,255,255,0.92);
|
|
||||||
border: 1px solid rgba(226,232,240,0.9);
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 0.55rem 0.35rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
box-shadow: 0 4px 14px rgba(15,23,42,0.08);
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.15rem;
|
|
||||||
}
|
|
||||||
.mobile-action-btn i { font-size: 1rem; }
|
|
||||||
.mobile-action-btn.danger { color: #dc2626; border-color: rgba(248,113,113,0.35); }
|
|
||||||
.mobile-action-btn:active { transform: translateY(1px); }
|
|
||||||
.mobile-action-btn.active {
|
|
||||||
border-color: #2563eb;
|
|
||||||
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 {
|
.mobile-tabbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset-inline: 0;
|
inset-inline: 0;
|
||||||
@ -686,32 +308,6 @@ height: 92%;
|
|||||||
box-shadow: 0 -6px 30px rgba(15, 23, 42, 0.12);
|
box-shadow: 0 -6px 30px rgba(15, 23, 42, 0.12);
|
||||||
border-top: 1px solid rgba(148, 163, 184, 0.25);
|
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: 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 {
|
.mobile-tabbar .mobile-tab-btn {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -744,39 +340,6 @@ height: 92%;
|
|||||||
transform: translateY(-2px);
|
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) {
|
@media (min-width: 1024px) {
|
||||||
.control-sheet {
|
.control-sheet {
|
||||||
@ -784,7 +347,7 @@ height: 92%;
|
|||||||
top: 7rem;
|
top: 7rem;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
width: 340px;
|
width: 340px;
|
||||||
max-height: calc(93vh - 8rem);
|
max-height: calc(100vh - 8rem);
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.5rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -792,17 +355,4 @@ height: 92%;
|
|||||||
border: 1px solid rgba(255,255,255,0.4);
|
border: 1px solid rgba(255,255,255,0.4);
|
||||||
}
|
}
|
||||||
body { padding-bottom: 0; overflow: auto; }
|
body { padding-bottom: 0; overflow: auto; }
|
||||||
.mobile-action-bar { display: none !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compact viewport fallback */
|
|
||||||
@media (max-height: 760px) {
|
|
||||||
body { overflow: auto; }
|
|
||||||
.control-sheet {
|
|
||||||
position: static;
|
|
||||||
max-height: none;
|
|
||||||
}
|
|
||||||
.floating-nudge {
|
|
||||||
bottom: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
18
svg.sh
@ -1,18 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# Backup originals once
|
|
||||||
mkdir -p output_webp_backup
|
|
||||||
cp --no-clobber output_webp/*.svg output_webp_backup/ || true
|
|
||||||
|
|
||||||
# Convert strokes to filled shapes and force a solid fill
|
|
||||||
FILL="#ffffff" # solid white interior for mask; stroke stays black for outline
|
|
||||||
for f in output_webp/*.svg; do
|
|
||||||
echo "Fixing $f"
|
|
||||||
inkscape "$f" --batch-process \
|
|
||||||
--actions="select-all;object-stroke-to-path;object-to-path;object-set-attribute:fill,$FILL;object-set-attribute:stroke,#000000;object-set-attribute:fill-rule,evenodd;object-set-attribute:style," \
|
|
||||||
--export-type=svg --export-filename="$f" --export-overwrite
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Done. Originals in output_webp_backup/"
|
|
||||||