SolarumAsteridion commited on
Commit
3f72fc7
·
verified ·
1 Parent(s): 20a3962

Update index.html

Browse files
Files changed (1) hide show
  1. 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: #fbf9f5;
20
- --desk-dot: #e2dccd;
 
21
 
22
- --paper-bg: #fffefa;
23
- --paper-text: #222;
24
- --shadow: rgba(0,0,0,.35);
25
 
26
- --perforation: #d0c6b7;
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: #2c2a27;
46
- --desk-dot: #3a3733;
47
-
48
- --paper-bg: #302e2b;
49
- --paper-text: #e9e7e2;
50
- --shadow: rgba(0,0,0,.55);
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: var(--desk-bg);
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
- margin:40px auto;
90
- padding:34px 34px 40px 50px;
91
- background:var(--paper-bg);
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
- position:absolute;top:12px;right:14px;z-index:10;
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
- position:relative;
137
- padding:18px 6px 10px 6px;
138
- display:grid;
139
- gap:16px;
140
- overflow:visible;
141
 
142
- /* prevent short images from becoming tall boxes */
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
- position:absolute;inset:0 6px;
151
- background:repeating-linear-gradient(
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 notes */
166
  .sticky{
167
- position:relative;
168
- border:1px solid var(--note-border);
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
- transform:rotate(var(--tilt,0deg)) translateY(-2px) scale(1.01);
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) 85%, #000 15%))}
188
- .sticky.note-mint {background:linear-gradient(180deg,var(--note-mint), color-mix(in oklab,var(--note-mint) 85%, #000 15%))}
189
- .sticky.note-blush {background:linear-gradient(180deg,var(--note-blush), color-mix(in oklab,var(--note-blush) 85%, #000 15%))}
190
- .sticky.note-blue {background:linear-gradient(180deg,var(--note-blue), color-mix(in oklab,var(--note-blue) 85%, #000 15%))}
191
 
192
  .sticky .bar{
193
- display:flex;align-items:center;justify-content:space-between;
194
- padding:6px 8px 4px 10px;
195
- background:var(--note-bar);
196
- border-top-left-radius:10px;border-top-right-radius:10px;
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
- height:auto;
209
- display:block;
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
- width:26px;height:26px;border-radius:50%;
219
- border:1px solid var(--note-border);
220
- background:rgba(255,255,255,.35);
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 & print */
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
- /* ======= processing UI ======= */
289
- function showProcessing(text){
290
- if(text) processingNode.textContent = text;
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
- /* ======= layout: 1 image full width, 2 images 50/50, 3+ responsive ======= */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  function updateLayout(){
316
- const count = board.querySelectorAll('.sticky').length;
 
 
 
 
 
 
317
  if(count === 1){
318
- board.style.setProperty('--cols', '1');
319
- board.style.setProperty('--minCol', '0px');
320
- }else if(count === 2){
321
- board.style.setProperty('--cols', '2');
322
- board.style.setProperty('--minCol', '0px');
 
 
 
 
 
 
 
 
323
  }else{
324
- board.style.setProperty('--cols', 'auto-fit');
325
- board.style.setProperty('--minCol', '220px');
326
  }
327
  }
328
 
329
- /* ======= random style bits ======= */
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, reject)=>{
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: 'id' });
355
- store.createIndex('createdAt', 'createdAt');
356
  }
357
  };
358
- req.onsuccess = () => resolve(req.result);
359
- req.onerror = () => reject(req.error);
360
  });
361
  }
362
  async function initPersistence(){
363
- if(!('indexedDB' in window)){
364
- PERSIST.mode = 'ls';
365
- return;
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
- /* --- IndexedDB ops --- */
377
- function idbAdd(rec){
378
- return new Promise((resolve,reject)=>{
379
- const tx = dbConn.transaction(STORE,'readwrite');
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
- /* --- localStorage ops (fallback) --- */
408
- function lsMeta(){
409
- try{
410
- return JSON.parse(localStorage.getItem('stickies-meta')||'[]');
411
- }catch{ return []; }
412
- }
413
- function lsSetMeta(arr){
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 === 'idb'){
450
- // store blob directly
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
- /* ======= create sticky DOM ======= */
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 = '0';
502
- wrap.style.transform = 'scale(.96)';
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); // newest on top
517
  }
518
 
519
- /* ======= handle pasted or dropped images ======= */
520
  function filesFromClipboard(dataTransfer){
521
  const items = dataTransfer?.items || [];
522
  const files = [];
523
  for(const it of items){
524
- if(it.kind === 'file' && it.type.startsWith('image/')){
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: rec.id, colorClass: color, tilt });
548
  }else{
549
- createSticky(rec.dataUrl, { isBlobUrl:false, id: rec.id, colorClass: color, tilt });
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
- /* ======= drag & drop ======= */
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 toggler ======= */
578
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
579
- const savedTheme = localStorage.getItem('note-theme');
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
- document.body.classList.toggle('dark', savedTheme==='dark');
590
- }else if(prefersDark.matches){
591
- document.body.classList.add('dark');
592
- }
593
  updateIcon();
594
  }
595
- function updateIcon(){
596
- themeBtn.textContent = document.body.classList.contains('dark') ? '☀️' : '🌙';
597
- }
 
 
598
 
599
- /* ======= boot: fade in and load saved stickies ======= */
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(); // ascending by createdAt
610
  for(const rec of saved){
611
- if(PERSIST.mode === 'idb'){
612
  const url = URL.createObjectURL(rec.blob);
613
- createSticky(url, { isBlobUrl:true, id: rec.id, colorClass: rec.color, tilt: rec.tilt });
614
  }else{
615
- createSticky(rec.dataUrl, { isBlobUrl:false, id: rec.id, colorClass: rec.color, tilt: rec.tilt });
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>