Spaces:
Running
Running
Update index.html
Browse files- index.html +188 -370
index.html
CHANGED
@@ -12,242 +12,150 @@
|
|
12 |
/>
|
13 |
|
14 |
<style>
|
15 |
-
/* ─────────────────────────────────────────
|
16 |
-
COLOR SYSTEM (light / dark via variables)
|
17 |
-
───────────────────────────────────────── */
|
18 |
:root{
|
19 |
-
--desk-bg
|
20 |
-
--
|
|
|
21 |
|
22 |
-
--
|
23 |
-
--
|
24 |
-
--shadow: rgba(0,0,0,.35);
|
25 |
|
26 |
-
--
|
27 |
-
|
28 |
-
--board-line: rgba(201,190,170,.18);
|
29 |
-
|
30 |
-
/* sticky colors */
|
31 |
-
--note-yellow: #fff6a8;
|
32 |
-
--note-mint: #d8f7e6;
|
33 |
-
--note-blush: #ffe3e0;
|
34 |
-
--note-blue: #e6f0ff;
|
35 |
-
|
36 |
-
--note-border: rgba(0,0,0,.08);
|
37 |
-
--note-bar: rgba(0,0,0,.04);
|
38 |
-
--note-text: #2b2b2b;
|
39 |
-
|
40 |
-
--btn-fg: #444;
|
41 |
-
--btn-fg-dim: #666;
|
42 |
-
--btn-bg-hover: rgba(0,0,0,.08);
|
43 |
}
|
44 |
body.dark{
|
45 |
-
--desk-bg
|
46 |
-
--
|
47 |
-
|
48 |
-
--
|
49 |
-
--
|
50 |
-
--
|
51 |
-
|
52 |
-
--perforation: #6d6456;
|
53 |
-
|
54 |
-
--board-line: rgba(110,103,94,.28);
|
55 |
-
|
56 |
-
--note-yellow: #6b6434;
|
57 |
-
--note-mint: #3f5a50;
|
58 |
-
--note-blush: #5b3f3c;
|
59 |
-
--note-blue: #3e4c63;
|
60 |
-
|
61 |
-
--note-border: rgba(0,0,0,.25);
|
62 |
-
--note-bar: rgba(255,255,255,.05);
|
63 |
-
--note-text: #f1efe9;
|
64 |
-
|
65 |
-
--btn-fg: #ddd;
|
66 |
-
--btn-fg-dim: #aaa;
|
67 |
-
--btn-bg-hover: rgba(255,255,255,.1);
|
68 |
}
|
69 |
|
70 |
-
/* ─────────────────────────────────────────
|
71 |
-
GLOBAL “DESK” BACKGROUND
|
72 |
-
───────────────────────────────────────── */
|
73 |
html,body{height:100%}
|
74 |
body{
|
75 |
-
margin:0;
|
76 |
-
background:
|
77 |
-
background-image: radial-gradient(var(--desk-dot) 1px,transparent 1px);
|
78 |
background-size:14px 14px;
|
79 |
-
font-family:'Crimson Text','Times New Roman',serif;
|
80 |
-
color:var(--paper-text);
|
81 |
-webkit-font-smoothing:antialiased;
|
82 |
}
|
83 |
|
84 |
-
/*
|
85 |
-
PAPER CONTAINER
|
86 |
-
───────────────────────────────────────── */
|
87 |
.container{
|
88 |
-
max-width:1100px;
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
color:var(--paper-text);
|
93 |
-
border:1px solid rgba(0,0,0,.05);
|
94 |
-
border-radius:12px 12px 10px 10px;
|
95 |
-
position:relative;
|
96 |
-
box-shadow:0 18px 40px -22px var(--shadow),
|
97 |
-
inset 0 2px 6px rgba(0,0,0,.06);
|
98 |
}
|
99 |
-
|
100 |
-
/* perforation holes */
|
101 |
.container::before{
|
102 |
-
content:'';
|
103 |
-
position:absolute;top:26px;bottom:26px;left:30px;width:9px;
|
104 |
background-image:radial-gradient(circle var(--perforation) 0%,var(--perforation) 2px,transparent 3px);
|
105 |
-
background-size:9px 28px;
|
106 |
-
background-repeat:repeat-y;
|
107 |
-
pointer-events:none;
|
108 |
}
|
109 |
-
/* curled corner */
|
110 |
.container::after{
|
111 |
-
content:'';position:absolute;top:0;right:0;width:110px;height:110px;
|
112 |
background:
|
113 |
linear-gradient(135deg,rgba(0,0,0,.08) 0%,rgba(0,0,0,0) 42%),
|
114 |
linear-gradient(135deg,var(--paper-bg) 0%,var(--paper-bg) 50%,rgba(255,255,255,0) 51%);
|
115 |
-
border-bottom-left-radius:12px;
|
116 |
-
pointer-events:none;
|
117 |
}
|
118 |
|
119 |
/* theme toggle */
|
120 |
-
#themeToggle{
|
121 |
-
|
122 |
-
font-size:20px;background:none;border:none;cursor:pointer;
|
123 |
-
transition:transform .25s;
|
124 |
-
user-select:none;
|
125 |
}
|
126 |
-
#themeToggle:hover{transform:rotate(20deg)scale(1.15)}
|
127 |
|
128 |
/* header */
|
129 |
-
.header{text-align:center;margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid rgba(0,0,0,.05)}
|
130 |
-
h1{font-family:'Libre Baskerville',serif;margin:0;font-size:28px;letter-spacing:.4px}
|
131 |
-
.subtitle{font-family:'PT Mono',monospace;font-size:14px;color:#666;margin-top:6px;letter-spacing:1px}
|
132 |
|
133 |
/* board */
|
134 |
#board{
|
135 |
-
min-height:520px;
|
136 |
-
|
137 |
-
|
138 |
-
display:grid;
|
139 |
-
gap:16px;
|
140 |
-
overflow:visible;
|
141 |
|
142 |
-
/*
|
143 |
-
align-items:start;
|
144 |
-
|
145 |
-
/* dynamic columns via CSS vars set from JS */
|
146 |
grid-template-columns: repeat(var(--cols, auto-fit), minmax(var(--minCol, 220px), 1fr));
|
147 |
}
|
148 |
#board::before{
|
149 |
-
content:'';
|
150 |
-
|
151 |
-
|
152 |
-
0deg,
|
153 |
-
transparent,transparent 2.8em,
|
154 |
-
var(--board-line) 2.8em,var(--board-line) 2.85em);
|
155 |
-
pointer-events:none;z-index:1;border-radius:8px;
|
156 |
}
|
157 |
-
#board > *{position:relative;z-index:2}
|
158 |
|
159 |
/* placeholder */
|
160 |
-
.placeholder{
|
161 |
-
color:#888;font-style:italic;text-align:center;
|
162 |
-
padding:120px 20px;user-select:none;grid-column:1/-1
|
163 |
-
}
|
164 |
|
165 |
-
/* sticky
|
166 |
.sticky{
|
167 |
-
position:relative;
|
168 |
-
|
169 |
-
border-radius:10px;
|
170 |
-
color:var(--note-text);
|
171 |
-
box-shadow:
|
172 |
-
0 10px 26px -16px var(--shadow),
|
173 |
-
inset 0 1px 0 rgba(255,255,255,.35);
|
174 |
transform:rotate(var(--tilt,0deg)) scale(1);
|
175 |
transition:transform .12s ease, box-shadow .12s ease, opacity .16s ease;
|
176 |
-
animation:pop .18s ease-out;
|
177 |
-
align-self:start; /* don't stretch tile height */
|
178 |
}
|
179 |
-
.sticky:hover{
|
180 |
-
|
181 |
-
box-shadow:
|
182 |
-
0 14px 32px -18px var(--shadow),
|
183 |
-
inset 0 1px 0 rgba(255,255,255,.45);
|
184 |
}
|
185 |
@keyframes pop{from{transform:scale(.96);opacity:0}to{transform:scale(1);opacity:1}}
|
186 |
|
187 |
-
.sticky.note-yellow{background:linear-gradient(180deg,var(--note-yellow),color-mix(in oklab,var(--note-yellow)
|
188 |
-
.sticky.note-mint {background:linear-gradient(180deg,var(--note-mint), color-mix(in oklab,var(--note-mint)
|
189 |
-
.sticky.note-blush {background:linear-gradient(180deg,var(--note-blush), color-mix(in oklab,var(--note-blush)
|
190 |
-
.sticky.note-blue {background:linear-gradient(180deg,var(--note-blue), color-mix(in oklab,var(--note-blue)
|
191 |
|
192 |
.sticky .bar{
|
193 |
-
display:flex;align-items:center;justify-content:space-between;
|
194 |
-
padding:6px 8px 4px 10px;
|
195 |
-
|
196 |
-
|
197 |
-
font-family:'PT Mono',monospace;
|
198 |
-
font-size:12px;letter-spacing:.3px
|
199 |
-
}
|
200 |
-
.badge{
|
201 |
-
background:rgba(0,0,0,.08);
|
202 |
-
padding:2px 6px;border-radius:999px;font-weight:600;
|
203 |
}
|
|
|
204 |
body.dark .badge{background:rgba(255,255,255,.12)}
|
205 |
|
206 |
.sticky img{
|
207 |
-
width:100%;
|
208 |
-
|
209 |
-
|
210 |
-
border-bottom-left-radius:10px;border-bottom-right-radius:10px;
|
211 |
-
background: linear-gradient(180deg,rgba(255,255,255,.25),rgba(255,255,255,0));
|
212 |
max-height:80vh; /* keep ultra-tall screenshots sane */
|
213 |
}
|
214 |
|
215 |
/* delete button */
|
216 |
.btn-del{
|
217 |
-
position:absolute;top:6px;right:6px;
|
218 |
-
|
219 |
-
|
220 |
-
background
|
221 |
-
color:var(--btn-fg);font-weight:700;line-height:24px;text-align:center;
|
222 |
-
cursor:pointer;backdrop-filter:saturate(120%) blur(2px);
|
223 |
-
display:flex;align-items:center;justify-content:center;
|
224 |
-
opacity:.85;transition:background .12s, transform .12s, opacity .12s;
|
225 |
}
|
226 |
-
.btn-del:hover{background:var(--btn-bg-hover);transform:scale(1.06);opacity:1}
|
227 |
.btn-del:active{transform:scale(.96)}
|
228 |
|
229 |
/* processing badge */
|
230 |
.processing{
|
231 |
-
position:fixed;top:20px;right:20px;background:#333;color:#fff;
|
232 |
-
padding:10px 20px;border-radius:6px;font-family:'PT Mono',monospace;
|
233 |
-
font-size:14px;opacity:0;transition:opacity .25s;z-index:2000
|
234 |
}
|
235 |
.processing.show{opacity:.9}
|
236 |
|
237 |
/* instructions */
|
238 |
-
.instructions{ text-align:center;font-family:'PT Mono',monospace;font-size:14px;color:#666;font-style:italic;margin-top:16px }
|
239 |
|
240 |
-
/* responsive
|
241 |
@media(max-width:768px){
|
242 |
-
.container{margin:20px 16px;padding:24px}
|
243 |
h1{font-size:24px}
|
244 |
-
#board{
|
245 |
-
grid-template-columns: repeat(var(--cols, auto-fit), minmax(180px,1fr));
|
246 |
-
}
|
247 |
}
|
|
|
|
|
248 |
@media print{
|
249 |
body{background:#fff}
|
250 |
-
.container{box-shadow:none;border:none}
|
251 |
.header,.processing,.instructions,#themeToggle{display:none}
|
252 |
}
|
253 |
</style>
|
@@ -255,7 +163,6 @@ body.dark .badge{background:rgba(255,255,255,.12)}
|
|
255 |
|
256 |
<body>
|
257 |
<div class="container">
|
258 |
-
<!-- theme icon -->
|
259 |
<button id="themeToggle" title="Toggle dark / light">🌙</button>
|
260 |
|
261 |
<div class="header">
|
@@ -270,9 +177,7 @@ body.dark .badge{background:rgba(255,255,255,.12)}
|
|
270 |
</div>
|
271 |
</div>
|
272 |
|
273 |
-
<div class="instructions">
|
274 |
-
Tip: each image becomes a sticky. Click the tiny × to delete.
|
275 |
-
</div>
|
276 |
</div>
|
277 |
|
278 |
<div class="processing">Processing…</div>
|
@@ -282,22 +187,12 @@ body.dark .badge{background:rgba(255,255,255,.12)}
|
|
282 |
const board = document.getElementById('board');
|
283 |
const processingNode = document.querySelector('.processing');
|
284 |
const themeBtn = document.getElementById('themeToggle');
|
285 |
-
|
286 |
let noteCount = 0;
|
287 |
|
288 |
-
/* =======
|
289 |
-
function showProcessing(text){
|
290 |
-
|
291 |
-
processingNode.classList.add('show');
|
292 |
-
}
|
293 |
-
function hideProcessing(){
|
294 |
-
setTimeout(()=>{
|
295 |
-
processingNode.classList.remove('show');
|
296 |
-
processingNode.textContent = 'Processing…';
|
297 |
-
},250);
|
298 |
-
}
|
299 |
|
300 |
-
/* ======= placeholder helpers ======= */
|
301 |
function removePlaceholder(){
|
302 |
const ph = board.querySelector('.placeholder');
|
303 |
if(ph) ph.remove();
|
@@ -311,33 +206,68 @@ function ensurePlaceholder(){
|
|
311 |
}
|
312 |
}
|
313 |
|
314 |
-
/* =======
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
315 |
function updateLayout(){
|
316 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
317 |
if(count === 1){
|
318 |
-
|
319 |
-
|
320 |
-
}
|
321 |
-
|
322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
323 |
}else{
|
324 |
-
|
325 |
-
|
326 |
}
|
327 |
}
|
328 |
|
329 |
-
/* ======= random
|
330 |
-
function randTilt(){
|
331 |
-
const d = (Math.random()*4.4 - 2.2).toFixed(2);
|
332 |
-
return d + 'deg';
|
333 |
-
}
|
334 |
function pickColorClass(){
|
335 |
const choices = ['note-yellow','note-mint','note-blush','note-blue'];
|
336 |
return choices[Math.floor(Math.random()*choices.length)];
|
337 |
}
|
338 |
-
function uuid(){
|
339 |
-
return (crypto?.randomUUID?.() || ('id-' + Math.random().toString(16).slice(2) + Date.now()));
|
340 |
-
}
|
341 |
|
342 |
/* ======= PERSISTENCE (IndexedDB with localStorage fallback) ======= */
|
343 |
const PERSIST = { mode: 'idb' };
|
@@ -346,133 +276,47 @@ const STORE = 'stickies';
|
|
346 |
let dbConn = null;
|
347 |
|
348 |
function initDB(){
|
349 |
-
return new Promise((resolve,
|
350 |
const req = indexedDB.open(DB_NAME, 1);
|
351 |
req.onupgradeneeded = () => {
|
352 |
const db = req.result;
|
353 |
if(!db.objectStoreNames.contains(STORE)){
|
354 |
-
const store = db.createObjectStore(STORE, { keyPath:
|
355 |
-
store.createIndex('createdAt',
|
356 |
}
|
357 |
};
|
358 |
-
req.onsuccess = ()
|
359 |
-
req.onerror = ()
|
360 |
});
|
361 |
}
|
362 |
async function initPersistence(){
|
363 |
-
if(!('indexedDB' in window)){
|
364 |
-
|
365 |
-
|
366 |
-
}
|
367 |
-
try{
|
368 |
-
dbConn = await initDB();
|
369 |
-
PERSIST.mode = 'idb';
|
370 |
-
}catch(e){
|
371 |
-
console.warn('IndexedDB unavailable, falling back to localStorage', e);
|
372 |
-
PERSIST.mode = 'ls';
|
373 |
-
}
|
374 |
}
|
375 |
|
376 |
-
/*
|
377 |
-
function idbAdd(rec){
|
378 |
-
|
379 |
-
|
380 |
-
tx.objectStore(STORE).put(rec);
|
381 |
-
tx.oncomplete = ()=> resolve();
|
382 |
-
tx.onerror = ()=> reject(tx.error);
|
383 |
-
});
|
384 |
-
}
|
385 |
-
function idbGetAll(){
|
386 |
-
return new Promise((resolve,reject)=>{
|
387 |
-
const tx = dbConn.transaction(STORE,'readonly');
|
388 |
-
const idx = tx.objectStore(STORE).index('createdAt');
|
389 |
-
const req = idx.getAll();
|
390 |
-
req.onsuccess = ()=> {
|
391 |
-
const arr = req.result || [];
|
392 |
-
arr.sort((a,b)=> a.createdAt - b.createdAt);
|
393 |
-
resolve(arr);
|
394 |
-
};
|
395 |
-
req.onerror = ()=> reject(req.error);
|
396 |
-
});
|
397 |
-
}
|
398 |
-
function idbDelete(id){
|
399 |
-
return new Promise((resolve,reject)=>{
|
400 |
-
const tx = dbConn.transaction(STORE,'readwrite');
|
401 |
-
tx.objectStore(STORE).delete(id);
|
402 |
-
tx.oncomplete = ()=> resolve();
|
403 |
-
tx.onerror = ()=> reject(tx.error);
|
404 |
-
});
|
405 |
-
}
|
406 |
|
407 |
-
/*
|
408 |
-
function lsMeta(){
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
}
|
413 |
-
function lsSetMeta(
|
414 |
-
localStorage.setItem('stickies-meta', JSON.stringify(arr));
|
415 |
-
}
|
416 |
-
function fileToDataURL(file){
|
417 |
-
return new Promise((resolve,reject)=>{
|
418 |
-
const r = new FileReader();
|
419 |
-
r.onload = ()=> resolve(r.result);
|
420 |
-
r.onerror = reject;
|
421 |
-
r.readAsDataURL(file);
|
422 |
-
});
|
423 |
-
}
|
424 |
-
function lsAdd(rec){
|
425 |
-
const meta = lsMeta();
|
426 |
-
meta.push({ id: rec.id, createdAt: rec.createdAt, color: rec.color, tilt: rec.tilt });
|
427 |
-
lsSetMeta(meta);
|
428 |
-
localStorage.setItem('sticky-img-' + rec.id, rec.dataUrl);
|
429 |
-
}
|
430 |
-
function lsGetAll(){
|
431 |
-
const meta = lsMeta().sort((a,b)=> a.createdAt - b.createdAt);
|
432 |
-
return meta.map(m => ({
|
433 |
-
id: m.id,
|
434 |
-
createdAt: m.createdAt,
|
435 |
-
color: m.color,
|
436 |
-
tilt: m.tilt,
|
437 |
-
dataUrl: localStorage.getItem('sticky-img-' + m.id)
|
438 |
-
})).filter(r => !!r.dataUrl);
|
439 |
-
}
|
440 |
-
function lsDelete(id){
|
441 |
-
const meta = lsMeta().filter(m => m.id !== id);
|
442 |
-
lsSetMeta(meta);
|
443 |
-
localStorage.removeItem('sticky-img-' + id);
|
444 |
-
}
|
445 |
|
446 |
-
/* --- persist a new file and return its record --- */
|
447 |
async function persistFile(file, color, tilt){
|
448 |
const rec = { id: uuid(), createdAt: Date.now(), color, tilt };
|
449 |
-
if(PERSIST.mode
|
450 |
-
|
451 |
-
await idbAdd({ ...rec, blob: file });
|
452 |
-
return { ...rec, blob: file };
|
453 |
-
}else{
|
454 |
-
// fallback: store base64 (size-limited)
|
455 |
-
const dataUrl = await fileToDataURL(file);
|
456 |
-
lsAdd({ ...rec, dataUrl });
|
457 |
-
return { ...rec, dataUrl };
|
458 |
-
}
|
459 |
-
}
|
460 |
-
async function loadPersisted(){
|
461 |
-
if(PERSIST.mode === 'idb'){
|
462 |
-
return await idbGetAll();
|
463 |
-
}else{
|
464 |
-
return lsGetAll();
|
465 |
-
}
|
466 |
-
}
|
467 |
-
async function deletePersisted(id){
|
468 |
-
if(PERSIST.mode === 'idb'){
|
469 |
-
await idbDelete(id);
|
470 |
-
}else{
|
471 |
-
lsDelete(id);
|
472 |
-
}
|
473 |
}
|
|
|
|
|
474 |
|
475 |
-
/* =======
|
476 |
function createSticky(src, { isBlobUrl=false, id=null, colorClass=null, tilt=null } = {}){
|
477 |
removePlaceholder();
|
478 |
noteCount++;
|
@@ -498,10 +342,8 @@ function createSticky(src, { isBlobUrl=false, id=null, colorClass=null, tilt=nul
|
|
498 |
btn.textContent = '×';
|
499 |
|
500 |
btn.addEventListener('click', async ()=>{
|
501 |
-
wrap.style.opacity
|
502 |
-
wrap.
|
503 |
-
const stickyId = wrap.dataset.id;
|
504 |
-
try{ if(stickyId) await deletePersisted(stickyId); }catch(e){ console.warn('Delete failed', e); }
|
505 |
setTimeout(()=>{
|
506 |
if(isBlobUrl) URL.revokeObjectURL(src);
|
507 |
wrap.remove();
|
@@ -510,116 +352,92 @@ function createSticky(src, { isBlobUrl=false, id=null, colorClass=null, tilt=nul
|
|
510 |
},140);
|
511 |
});
|
512 |
|
|
|
|
|
|
|
513 |
wrap.appendChild(bar);
|
514 |
wrap.appendChild(img);
|
515 |
wrap.appendChild(btn);
|
516 |
-
board.prepend(wrap);
|
517 |
}
|
518 |
|
519 |
-
/* =======
|
520 |
function filesFromClipboard(dataTransfer){
|
521 |
const items = dataTransfer?.items || [];
|
522 |
const files = [];
|
523 |
for(const it of items){
|
524 |
-
if(it.kind
|
525 |
-
const f = it.getAsFile();
|
526 |
-
if(f) files.push(f);
|
527 |
}
|
528 |
}
|
529 |
return files;
|
530 |
}
|
531 |
|
532 |
async function handleImages(files){
|
533 |
-
if(!files.length){
|
534 |
-
showProcessing('No images found in clipboard');
|
535 |
-
hideProcessing();
|
536 |
-
return;
|
537 |
-
}
|
538 |
showProcessing('Saving…');
|
539 |
for(const file of files){
|
540 |
-
const color = pickColorClass();
|
541 |
-
const tilt = randTilt();
|
542 |
-
// persist first to get an id
|
543 |
const rec = await persistFile(file, color, tilt);
|
544 |
-
|
545 |
-
if(PERSIST.mode === 'idb'){
|
546 |
const url = URL.createObjectURL(file);
|
547 |
-
createSticky(url, { isBlobUrl:true, id:
|
548 |
}else{
|
549 |
-
createSticky(rec.dataUrl, {
|
550 |
}
|
551 |
}
|
552 |
-
updateLayout();
|
553 |
hideProcessing();
|
554 |
}
|
555 |
|
556 |
-
/* ======= paste listener ======= */
|
557 |
document.addEventListener('paste', e=>{
|
558 |
const files = filesFromClipboard(e.clipboardData);
|
559 |
if(files.length){ e.preventDefault(); handleImages(files); }
|
560 |
});
|
561 |
-
|
562 |
-
|
563 |
-
board.addEventListener('dragover', e=>{
|
564 |
-
e.preventDefault();
|
565 |
-
board.style.outline = '2px dashed rgba(0,0,0,.2)';
|
566 |
-
});
|
567 |
-
board.addEventListener('dragleave', ()=>{
|
568 |
-
board.style.outline = 'none';
|
569 |
-
});
|
570 |
board.addEventListener('drop', e=>{
|
571 |
-
e.preventDefault();
|
572 |
-
board.style.outline = 'none';
|
573 |
const dtFiles = [...(e.dataTransfer?.files || [])].filter(f=>f.type.startsWith('image/'));
|
574 |
handleImages(dtFiles);
|
575 |
});
|
576 |
|
577 |
-
/* ======= theme
|
578 |
-
const prefersDark
|
579 |
-
const savedTheme
|
580 |
-
|
581 |
-
initTheme();
|
582 |
-
themeBtn.addEventListener('click',()=>{
|
583 |
-
document.body.classList.toggle('dark');
|
584 |
-
updateIcon();
|
585 |
-
localStorage.setItem('note-theme', document.body.classList.contains('dark') ? 'dark' : 'light');
|
586 |
-
});
|
587 |
function initTheme(){
|
588 |
-
if(savedTheme){
|
589 |
-
|
590 |
-
}else if(prefersDark.matches){
|
591 |
-
document.body.classList.add('dark');
|
592 |
-
}
|
593 |
updateIcon();
|
594 |
}
|
595 |
-
|
596 |
-
|
597 |
-
|
|
|
|
|
598 |
|
599 |
-
/* ======= boot
|
600 |
document.addEventListener('DOMContentLoaded', async ()=>{
|
601 |
const sheet=document.querySelector('.container');
|
602 |
sheet.style.opacity='0';
|
603 |
-
setTimeout(()=>{sheet.style.transition='opacity .6s ease';sheet.style.opacity='1'},80);
|
604 |
|
|
|
605 |
showProcessing('Loading saved…');
|
606 |
await initPersistence();
|
607 |
|
608 |
try{
|
609 |
-
const saved = await loadPersisted();
|
610 |
for(const rec of saved){
|
611 |
-
if(PERSIST.mode
|
612 |
const url = URL.createObjectURL(rec.blob);
|
613 |
-
createSticky(url, { isBlobUrl:true, id:
|
614 |
}else{
|
615 |
-
createSticky(rec.dataUrl, {
|
616 |
}
|
617 |
}
|
618 |
-
}catch(e){
|
619 |
-
console.warn('Load failed', e);
|
620 |
-
}
|
621 |
|
622 |
-
updateLayout();
|
623 |
hideProcessing();
|
624 |
});
|
625 |
</script>
|
|
|
12 |
/>
|
13 |
|
14 |
<style>
|
|
|
|
|
|
|
15 |
:root{
|
16 |
+
--desk-bg:#fbf9f5; --desk-dot:#e2dccd;
|
17 |
+
--paper-bg:#fffefa; --paper-text:#222; --shadow:rgba(0,0,0,.35);
|
18 |
+
--perforation:#d0c6b7; --board-line:rgba(201,190,170,.18);
|
19 |
|
20 |
+
--note-yellow:#fff6a8; --note-mint:#d8f7e6; --note-blush:#ffe3e0; --note-blue:#e6f0ff;
|
21 |
+
--note-border:rgba(0,0,0,.08); --note-bar:rgba(0,0,0,.04); --note-text:#2b2b2b;
|
|
|
22 |
|
23 |
+
--btn-fg:#444; --btn-fg-dim:#666; --btn-bg-hover:rgba(0,0,0,.08);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
}
|
25 |
body.dark{
|
26 |
+
--desk-bg:#2c2a27; --desk-dot:#3a3733;
|
27 |
+
--paper-bg:#302e2b; --paper-text:#e9e7e2; --shadow:rgba(0,0,0,.55);
|
28 |
+
--perforation:#6d6456; --board-line:rgba(110,103,94,.28);
|
29 |
+
--note-yellow:#6b6434; --note-mint:#3f5a50; --note-blush:#5b3f3c; --note-blue:#3e4c63;
|
30 |
+
--note-border:rgba(0,0,0,.25); --note-bar:rgba(255,255,255,.05); --note-text:#f1efe9;
|
31 |
+
--btn-fg:#ddd; --btn-fg-dim:#aaa; --btn-bg-hover:rgba(255,255,255,.1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
}
|
33 |
|
|
|
|
|
|
|
34 |
html,body{height:100%}
|
35 |
body{
|
36 |
+
margin:0; background:var(--desk-bg);
|
37 |
+
background-image:radial-gradient(var(--desk-dot) 1px,transparent 1px);
|
|
|
38 |
background-size:14px 14px;
|
39 |
+
font-family:'Crimson Text','Times New Roman',serif; color:var(--paper-text);
|
|
|
40 |
-webkit-font-smoothing:antialiased;
|
41 |
}
|
42 |
|
43 |
+
/* paper */
|
|
|
|
|
44 |
.container{
|
45 |
+
max-width:1100px; margin:40px auto; padding:34px 34px 40px 50px;
|
46 |
+
background:var(--paper-bg); color:var(--paper-text);
|
47 |
+
border:1px solid rgba(0,0,0,.05); border-radius:12px 12px 10px 10px; position:relative;
|
48 |
+
box-shadow:0 18px 40px -22px var(--shadow), inset 0 2px 6px rgba(0,0,0,.06);
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
}
|
|
|
|
|
50 |
.container::before{
|
51 |
+
content:''; position:absolute; top:26px; bottom:26px; left:30px; width:9px;
|
|
|
52 |
background-image:radial-gradient(circle var(--perforation) 0%,var(--perforation) 2px,transparent 3px);
|
53 |
+
background-size:9px 28px; background-repeat:repeat-y; pointer-events:none;
|
|
|
|
|
54 |
}
|
|
|
55 |
.container::after{
|
56 |
+
content:''; position:absolute; top:0; right:0; width:110px; height:110px;
|
57 |
background:
|
58 |
linear-gradient(135deg,rgba(0,0,0,.08) 0%,rgba(0,0,0,0) 42%),
|
59 |
linear-gradient(135deg,var(--paper-bg) 0%,var(--paper-bg) 50%,rgba(255,255,255,0) 51%);
|
60 |
+
border-bottom-left-radius:12px; pointer-events:none;
|
|
|
61 |
}
|
62 |
|
63 |
/* theme toggle */
|
64 |
+
#themeToggle{ position:absolute; top:12px; right:14px; z-index:10;
|
65 |
+
font-size:20px; background:none; border:none; cursor:pointer; transition:transform .25s; user-select:none;
|
|
|
|
|
|
|
66 |
}
|
67 |
+
#themeToggle:hover{ transform:rotate(20deg) scale(1.15) }
|
68 |
|
69 |
/* header */
|
70 |
+
.header{text-align:center; margin-bottom:18px; padding-bottom:14px; border-bottom:1px solid rgba(0,0,0,.05)}
|
71 |
+
h1{font-family:'Libre Baskerville',serif; margin:0; font-size:28px; letter-spacing:.4px}
|
72 |
+
.subtitle{font-family:'PT Mono',monospace; font-size:14px; color:#666; margin-top:6px; letter-spacing:1px}
|
73 |
|
74 |
/* board */
|
75 |
#board{
|
76 |
+
min-height:520px; position:relative; padding:18px 6px 10px 6px;
|
77 |
+
display:grid; gap:16px; overflow:visible;
|
78 |
+
align-items:start; /* prevents tall stretching of short images */
|
|
|
|
|
|
|
79 |
|
80 |
+
/* dynamic columns driven by JS vars */
|
|
|
|
|
|
|
81 |
grid-template-columns: repeat(var(--cols, auto-fit), minmax(var(--minCol, 220px), 1fr));
|
82 |
}
|
83 |
#board::before{
|
84 |
+
content:''; position:absolute; inset:0 6px;
|
85 |
+
background:repeating-linear-gradient(0deg, transparent,transparent 2.8em, var(--board-line) 2.8em,var(--board-line) 2.85em);
|
86 |
+
pointer-events:none; z-index:1; border-radius:8px;
|
|
|
|
|
|
|
|
|
87 |
}
|
88 |
+
#board > *{ position:relative; z-index:2 }
|
89 |
|
90 |
/* placeholder */
|
91 |
+
.placeholder{ color:#888; font-style:italic; text-align:center; padding:120px 20px; user-select:none; grid-column:1/-1 }
|
|
|
|
|
|
|
92 |
|
93 |
+
/* sticky */
|
94 |
.sticky{
|
95 |
+
position:relative; border:1px solid var(--note-border); border-radius:10px; color:var(--note-text);
|
96 |
+
box-shadow:0 10px 26px -16px var(--shadow), inset 0 1px 0 rgba(255,255,255,.35);
|
|
|
|
|
|
|
|
|
|
|
97 |
transform:rotate(var(--tilt,0deg)) scale(1);
|
98 |
transition:transform .12s ease, box-shadow .12s ease, opacity .16s ease;
|
99 |
+
animation:pop .18s ease-out; align-self:start;
|
|
|
100 |
}
|
101 |
+
.sticky:hover{ transform:rotate(var(--tilt,0deg)) translateY(-2px) scale(1.01);
|
102 |
+
box-shadow:0 14px 32px -18px var(--shadow), inset 0 1px 0 rgba(255,255,255,.45);
|
|
|
|
|
|
|
103 |
}
|
104 |
@keyframes pop{from{transform:scale(.96);opacity:0}to{transform:scale(1);opacity:1}}
|
105 |
|
106 |
+
.sticky.note-yellow{background:linear-gradient(180deg,var(--note-yellow),color-mix(in oklab,var(--note-yellow)85%,#000 15%))}
|
107 |
+
.sticky.note-mint {background:linear-gradient(180deg,var(--note-mint), color-mix(in oklab,var(--note-mint) 85%,#000 15%))}
|
108 |
+
.sticky.note-blush {background:linear-gradient(180deg,var(--note-blush), color-mix(in oklab,var(--note-blush) 85%,#000 15%))}
|
109 |
+
.sticky.note-blue {background:linear-gradient(180deg,var(--note-blue), color-mix(in oklab,var(--note-blue) 85%,#000 15%))}
|
110 |
|
111 |
.sticky .bar{
|
112 |
+
display:flex; align-items:center; justify-content:space-between;
|
113 |
+
padding:6px 8px 4px 10px; background:var(--note-bar);
|
114 |
+
border-top-left-radius:10px; border-top-right-radius:10px;
|
115 |
+
font-family:'PT Mono',monospace; font-size:12px; letter-spacing:.3px
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
}
|
117 |
+
.badge{ background:rgba(0,0,0,.08); padding:2px 6px; border-radius:999px; font-weight:600 }
|
118 |
body.dark .badge{background:rgba(255,255,255,.12)}
|
119 |
|
120 |
.sticky img{
|
121 |
+
width:100%; height:auto; display:block;
|
122 |
+
border-bottom-left-radius:10px; border-bottom-right-radius:10px;
|
123 |
+
background:linear-gradient(180deg,rgba(255,255,255,.25),rgba(255,255,255,0));
|
|
|
|
|
124 |
max-height:80vh; /* keep ultra-tall screenshots sane */
|
125 |
}
|
126 |
|
127 |
/* delete button */
|
128 |
.btn-del{
|
129 |
+
position:absolute; top:6px; right:6px; width:26px; height:26px; border-radius:50%;
|
130 |
+
border:1px solid var(--note-border); background:rgba(255,255,255,.35); color:var(--btn-fg);
|
131 |
+
font-weight:700; line-height:24px; text-align:center; cursor:pointer; backdrop-filter:saturate(120%) blur(2px);
|
132 |
+
display:flex; align-items:center; justify-content:center; opacity:.85; transition:background .12s, transform .12s, opacity .12s;
|
|
|
|
|
|
|
|
|
133 |
}
|
134 |
+
.btn-del:hover{background:var(--btn-bg-hover); transform:scale(1.06); opacity:1}
|
135 |
.btn-del:active{transform:scale(.96)}
|
136 |
|
137 |
/* processing badge */
|
138 |
.processing{
|
139 |
+
position:fixed; top:20px; right:20px; background:#333; color:#fff;
|
140 |
+
padding:10px 20px; border-radius:6px; font-family:'PT Mono',monospace;
|
141 |
+
font-size:14px; opacity:0; transition:opacity .25s; z-index:2000
|
142 |
}
|
143 |
.processing.show{opacity:.9}
|
144 |
|
145 |
/* instructions */
|
146 |
+
.instructions{ text-align:center; font-family:'PT Mono',monospace; font-size:14px; color:#666; font-style:italic; margin-top:16px }
|
147 |
|
148 |
+
/* responsive */
|
149 |
@media(max-width:768px){
|
150 |
+
.container{margin:20px 16px; padding:24px}
|
151 |
h1{font-size:24px}
|
152 |
+
#board{ grid-template-columns: repeat(var(--cols, auto-fit), minmax(180px,1fr)) }
|
|
|
|
|
153 |
}
|
154 |
+
|
155 |
+
/* print */
|
156 |
@media print{
|
157 |
body{background:#fff}
|
158 |
+
.container{box-shadow:none; border:none}
|
159 |
.header,.processing,.instructions,#themeToggle{display:none}
|
160 |
}
|
161 |
</style>
|
|
|
163 |
|
164 |
<body>
|
165 |
<div class="container">
|
|
|
166 |
<button id="themeToggle" title="Toggle dark / light">🌙</button>
|
167 |
|
168 |
<div class="header">
|
|
|
177 |
</div>
|
178 |
</div>
|
179 |
|
180 |
+
<div class="instructions">Tip: each image becomes a sticky. Click the tiny × to delete.</div>
|
|
|
|
|
181 |
</div>
|
182 |
|
183 |
<div class="processing">Processing…</div>
|
|
|
187 |
const board = document.getElementById('board');
|
188 |
const processingNode = document.querySelector('.processing');
|
189 |
const themeBtn = document.getElementById('themeToggle');
|
|
|
190 |
let noteCount = 0;
|
191 |
|
192 |
+
/* ======= UI helpers ======= */
|
193 |
+
function showProcessing(text){ if(text) processingNode.textContent=text; processingNode.classList.add('show') }
|
194 |
+
function hideProcessing(){ setTimeout(()=>{processingNode.classList.remove('show'); processingNode.textContent='Processing…'},250) }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
195 |
|
|
|
196 |
function removePlaceholder(){
|
197 |
const ph = board.querySelector('.placeholder');
|
198 |
if(ph) ph.remove();
|
|
|
206 |
}
|
207 |
}
|
208 |
|
209 |
+
/* ======= SMART LAYOUT by aspect ratio ======= */
|
210 |
+
/* Tune thresholds here */
|
211 |
+
const WIDE_AR = 1.35; // >= this is considered landscape
|
212 |
+
const TALL_AR = 0.85; // <= this is considered portrait
|
213 |
+
|
214 |
+
function setLayout(cols, min){
|
215 |
+
board.style.setProperty('--cols', String(cols));
|
216 |
+
board.style.setProperty('--minCol', min);
|
217 |
+
}
|
218 |
+
|
219 |
+
function decideOrientation(imgs){
|
220 |
+
let loaded=0, wide=0, tall=0, sum=0;
|
221 |
+
for(const img of imgs){
|
222 |
+
const w=img.naturalWidth, h=img.naturalHeight;
|
223 |
+
if(!w || !h) continue;
|
224 |
+
loaded++;
|
225 |
+
const r = w/h; sum += r;
|
226 |
+
if(r >= WIDE_AR) wide++;
|
227 |
+
else if(r <= TALL_AR) tall++;
|
228 |
+
}
|
229 |
+
if(loaded === 0) return 'unknown';
|
230 |
+
const avg = sum/loaded;
|
231 |
+
if(wide >= tall && avg >= 1.20) return 'landscape';
|
232 |
+
if(tall > wide && avg <= 0.95) return 'portrait';
|
233 |
+
return 'mixed';
|
234 |
+
}
|
235 |
+
|
236 |
function updateLayout(){
|
237 |
+
const stickies = [...board.querySelectorAll('.sticky')];
|
238 |
+
const count = stickies.length;
|
239 |
+
|
240 |
+
if(count === 0){
|
241 |
+
setLayout('auto-fit','220px');
|
242 |
+
return;
|
243 |
+
}
|
244 |
if(count === 1){
|
245 |
+
setLayout(1,'0px'); // full width
|
246 |
+
return;
|
247 |
+
}
|
248 |
+
|
249 |
+
const imgs = stickies.map(s=>s.querySelector('img'));
|
250 |
+
const mode = decideOrientation(imgs);
|
251 |
+
|
252 |
+
if(mode === 'landscape'){
|
253 |
+
// Mostly wide → stack vertically (top to bottom)
|
254 |
+
setLayout(1,'0px');
|
255 |
+
}else if(mode === 'portrait'){
|
256 |
+
// Mostly tall → left to right (responsive columns)
|
257 |
+
if(count === 2) setLayout(2,'0px'); else setLayout('auto-fit','260px');
|
258 |
}else{
|
259 |
+
// Mixed or unknown → reasonable responsive default
|
260 |
+
if(count === 2) setLayout(2,'0px'); else setLayout('auto-fit','220px');
|
261 |
}
|
262 |
}
|
263 |
|
264 |
+
/* ======= random styling ======= */
|
265 |
+
function randTilt(){ return (Math.random()*4.4 - 2.2).toFixed(2)+'deg'; }
|
|
|
|
|
|
|
266 |
function pickColorClass(){
|
267 |
const choices = ['note-yellow','note-mint','note-blush','note-blue'];
|
268 |
return choices[Math.floor(Math.random()*choices.length)];
|
269 |
}
|
270 |
+
function uuid(){ return (crypto?.randomUUID?.() || ('id-'+Math.random().toString(16).slice(2)+Date.now())); }
|
|
|
|
|
271 |
|
272 |
/* ======= PERSISTENCE (IndexedDB with localStorage fallback) ======= */
|
273 |
const PERSIST = { mode: 'idb' };
|
|
|
276 |
let dbConn = null;
|
277 |
|
278 |
function initDB(){
|
279 |
+
return new Promise((resolve,reject)=>{
|
280 |
const req = indexedDB.open(DB_NAME, 1);
|
281 |
req.onupgradeneeded = () => {
|
282 |
const db = req.result;
|
283 |
if(!db.objectStoreNames.contains(STORE)){
|
284 |
+
const store = db.createObjectStore(STORE, { keyPath:'id' });
|
285 |
+
store.createIndex('createdAt','createdAt');
|
286 |
}
|
287 |
};
|
288 |
+
req.onsuccess = ()=> resolve(req.result);
|
289 |
+
req.onerror = ()=> reject(req.error);
|
290 |
});
|
291 |
}
|
292 |
async function initPersistence(){
|
293 |
+
if(!('indexedDB' in window)){ PERSIST.mode='ls'; return; }
|
294 |
+
try{ dbConn = await initDB(); PERSIST.mode='idb'; }
|
295 |
+
catch(e){ console.warn('IndexedDB unavailable, fallback to localStorage', e); PERSIST.mode='ls'; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
296 |
}
|
297 |
|
298 |
+
/* IDB ops */
|
299 |
+
function idbAdd(rec){ return new Promise((res,rej)=>{ const tx=dbConn.transaction(STORE,'readwrite'); tx.objectStore(STORE).put(rec); tx.oncomplete=()=>res(); tx.onerror=()=>rej(tx.error); }); }
|
300 |
+
function idbGetAll(){ return new Promise((res,rej)=>{ const tx=dbConn.transaction(STORE,'readonly'); const idx=tx.objectStore(STORE).index('createdAt'); const req=idx.getAll(); req.onsuccess=()=>{ const arr=req.result||[]; arr.sort((a,b)=>a.createdAt-b.createdAt); res(arr); }; req.onerror=()=>rej(req.error); }); }
|
301 |
+
function idbDelete(id){ return new Promise((res,rej)=>{ const tx=dbConn.transaction(STORE,'readwrite'); tx.objectStore(STORE).delete(id); tx.oncomplete=()=>res(); tx.onerror=()=>rej(tx.error); }); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
302 |
|
303 |
+
/* localStorage fallback */
|
304 |
+
function lsMeta(){ try{ return JSON.parse(localStorage.getItem('stickies-meta')||'[]'); }catch{ return []; } }
|
305 |
+
function lsSetMeta(arr){ localStorage.setItem('stickies-meta', JSON.stringify(arr)); }
|
306 |
+
function fileToDataURL(file){ return new Promise((res,rej)=>{ const r=new FileReader(); r.onload=()=>res(r.result); r.onerror=rej; r.readAsDataURL(file); }); }
|
307 |
+
function lsAdd(rec){ const meta=lsMeta(); meta.push({id:rec.id,createdAt:rec.createdAt,color:rec.color,tilt:rec.tilt}); lsSetMeta(meta); localStorage.setItem('sticky-img-'+rec.id, rec.dataUrl); }
|
308 |
+
function lsGetAll(){ const meta=lsMeta().sort((a,b)=>a.createdAt-b.createdAt); return meta.map(m=>({id:m.id,createdAt:m.createdAt,color:m.color,tilt:m.tilt,dataUrl:localStorage.getItem('sticky-img-'+m.id)})).filter(r=>!!r.dataUrl); }
|
309 |
+
function lsDelete(id){ const meta=lsMeta().filter(m=>m.id!==id); lsSetMeta(meta); localStorage.removeItem('sticky-img-'+id); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
310 |
|
|
|
311 |
async function persistFile(file, color, tilt){
|
312 |
const rec = { id: uuid(), createdAt: Date.now(), color, tilt };
|
313 |
+
if(PERSIST.mode==='idb'){ await idbAdd({ ...rec, blob:file }); return { ...rec, blob:file }; }
|
314 |
+
const dataUrl = await fileToDataURL(file); lsAdd({ ...rec, dataUrl }); return { ...rec, dataUrl };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
315 |
}
|
316 |
+
async function loadPersisted(){ return PERSIST.mode==='idb' ? idbGetAll() : lsGetAll(); }
|
317 |
+
async function deletePersisted(id){ return PERSIST.mode==='idb' ? idbDelete(id) : lsDelete(id); }
|
318 |
|
319 |
+
/* ======= sticky creation ======= */
|
320 |
function createSticky(src, { isBlobUrl=false, id=null, colorClass=null, tilt=null } = {}){
|
321 |
removePlaceholder();
|
322 |
noteCount++;
|
|
|
342 |
btn.textContent = '×';
|
343 |
|
344 |
btn.addEventListener('click', async ()=>{
|
345 |
+
wrap.style.opacity='0'; wrap.style.transform='scale(.96)';
|
346 |
+
try{ if(wrap.dataset.id) await deletePersisted(wrap.dataset.id); }catch(e){ console.warn('Delete failed', e); }
|
|
|
|
|
347 |
setTimeout(()=>{
|
348 |
if(isBlobUrl) URL.revokeObjectURL(src);
|
349 |
wrap.remove();
|
|
|
352 |
},140);
|
353 |
});
|
354 |
|
355 |
+
// When the image finishes loading, recompute layout (so aspect ratio is known)
|
356 |
+
img.addEventListener('load', updateLayout);
|
357 |
+
|
358 |
wrap.appendChild(bar);
|
359 |
wrap.appendChild(img);
|
360 |
wrap.appendChild(btn);
|
361 |
+
board.prepend(wrap);
|
362 |
}
|
363 |
|
364 |
+
/* ======= paste / drop ======= */
|
365 |
function filesFromClipboard(dataTransfer){
|
366 |
const items = dataTransfer?.items || [];
|
367 |
const files = [];
|
368 |
for(const it of items){
|
369 |
+
if(it.kind==='file' && it.type.startsWith('image/')){
|
370 |
+
const f = it.getAsFile(); if(f) files.push(f);
|
|
|
371 |
}
|
372 |
}
|
373 |
return files;
|
374 |
}
|
375 |
|
376 |
async function handleImages(files){
|
377 |
+
if(!files.length){ showProcessing('No images found'); hideProcessing(); return; }
|
|
|
|
|
|
|
|
|
378 |
showProcessing('Saving…');
|
379 |
for(const file of files){
|
380 |
+
const color = pickColorClass(), tilt = randTilt();
|
|
|
|
|
381 |
const rec = await persistFile(file, color, tilt);
|
382 |
+
if(PERSIST.mode==='idb'){
|
|
|
383 |
const url = URL.createObjectURL(file);
|
384 |
+
createSticky(url, { isBlobUrl:true, id:rec.id, colorClass:color, tilt });
|
385 |
}else{
|
386 |
+
createSticky(rec.dataUrl, { id:rec.id, colorClass:color, tilt });
|
387 |
}
|
388 |
}
|
|
|
389 |
hideProcessing();
|
390 |
}
|
391 |
|
|
|
392 |
document.addEventListener('paste', e=>{
|
393 |
const files = filesFromClipboard(e.clipboardData);
|
394 |
if(files.length){ e.preventDefault(); handleImages(files); }
|
395 |
});
|
396 |
+
board.addEventListener('dragover', e=>{ e.preventDefault(); board.style.outline='2px dashed rgba(0,0,0,.2)'; });
|
397 |
+
board.addEventListener('dragleave', ()=>{ board.style.outline='none'; });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
398 |
board.addEventListener('drop', e=>{
|
399 |
+
e.preventDefault(); board.style.outline='none';
|
|
|
400 |
const dtFiles = [...(e.dataTransfer?.files || [])].filter(f=>f.type.startsWith('image/'));
|
401 |
handleImages(dtFiles);
|
402 |
});
|
403 |
|
404 |
+
/* ======= theme ======= */
|
405 |
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
406 |
+
const savedTheme = localStorage.getItem('note-theme');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
407 |
function initTheme(){
|
408 |
+
if(savedTheme){ document.body.classList.toggle('dark', savedTheme==='dark'); }
|
409 |
+
else if(prefersDark.matches){ document.body.classList.add('dark'); }
|
|
|
|
|
|
|
410 |
updateIcon();
|
411 |
}
|
412 |
+
themeBtn.addEventListener('click',()=>{
|
413 |
+
document.body.classList.toggle('dark'); updateIcon();
|
414 |
+
localStorage.setItem('note-theme', document.body.classList.contains('dark') ? 'dark' : 'light');
|
415 |
+
});
|
416 |
+
function updateIcon(){ themeBtn.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙'; }
|
417 |
|
418 |
+
/* ======= boot ======= */
|
419 |
document.addEventListener('DOMContentLoaded', async ()=>{
|
420 |
const sheet=document.querySelector('.container');
|
421 |
sheet.style.opacity='0';
|
422 |
+
setTimeout(()=>{ sheet.style.transition='opacity .6s ease'; sheet.style.opacity='1' },80);
|
423 |
|
424 |
+
initTheme();
|
425 |
showProcessing('Loading saved…');
|
426 |
await initPersistence();
|
427 |
|
428 |
try{
|
429 |
+
const saved = await loadPersisted();
|
430 |
for(const rec of saved){
|
431 |
+
if(PERSIST.mode==='idb'){
|
432 |
const url = URL.createObjectURL(rec.blob);
|
433 |
+
createSticky(url, { isBlobUrl:true, id:rec.id, colorClass:rec.color, tilt:rec.tilt });
|
434 |
}else{
|
435 |
+
createSticky(rec.dataUrl, { id:rec.id, colorClass:rec.color, tilt:rec.tilt });
|
436 |
}
|
437 |
}
|
438 |
+
}catch(e){ console.warn('Load failed', e); }
|
|
|
|
|
439 |
|
440 |
+
updateLayout(); // initial
|
441 |
hideProcessing();
|
442 |
});
|
443 |
</script>
|