Shereen Lee commited on
Commit
c17e007
·
1 Parent(s): 4372e18

feat(upload+darkroom): arrows back beside Upload, corner-x delete; gate shake until developed; smoother baths

Browse files

- bring back browse arrows (no animation) in a ◄ [+ Upload] ► row; demote delete
to a small x at the polaroid's corner
- darkroom hanging print: no shaking until the watercolour has developed — the
centre label now reads 'developing…' (spinner) then 'developed · shake me', and
the cursor shows progress instead of grab while it's still developing
- baths page: cards ease into each tray (slide-in above, sink-in submerged)
instead of popping, for a smoother develop sequence

Files changed (1) hide show
  1. index.html +32 -30
index.html CHANGED
@@ -1607,7 +1607,7 @@
1607
  );
1608
  }
1609
 
1610
- function PhotoStack({ photo, index, total, slideDir, destroying, onDestroyed, onAdd, onAddSample, onLoadSamples, onReposition, onPrev, onNext, navDisabled, title, caption, date, time, onTitleChange, onCaptionChange, onDateChange, onTimeChange }) {
1611
  if (!photo) {
1612
  // no photos left - show an empty polaroid inviting an upload, plus a
1613
  // row of sample photos the user can add with one tap.
@@ -1678,16 +1678,12 @@
1678
  onReposition={onReposition} />
1679
  <span className="pointer-events-none absolute right-2 top-2 rounded-sm bg-card/90 px-2 py-0.5 font-mono text-xs font-semibold text-primary">{index + 1}/{total}</span>
1680
  </div>
1681
- {/* browse arrows straddle the polaroid's left/right edges */}
1682
- {total > 1 ? (
1683
- <>
1684
- <button type="button" onClick={onPrev} disabled={navDisabled} aria-label="Previous photo"
1685
- title={navDisabled ? "Developing… cancel to browse" : "Previous photo"}
1686
- className="sozai-sway-l absolute left-0 top-1/2 z-20 flex size-9 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border-2 border-primary bg-card text-primary shadow-md transition-all hover:bg-accent disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-card"><ChevronLeft className="size-4" /></button>
1687
- <button type="button" onClick={onNext} disabled={navDisabled} aria-label="Next photo"
1688
- title={navDisabled ? "Developing… cancel to browse" : "Next photo"}
1689
- className="sozai-sway-r absolute right-0 top-1/2 z-20 flex size-9 -translate-y-1/2 translate-x-1/2 items-center justify-center rounded-full border-2 border-primary bg-card text-primary shadow-md transition-all hover:bg-accent disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-card"><ChevronRight className="size-4" /></button>
1690
- </>
1691
  ) : null}
1692
  </div>
1693
  <div className="mt-3 rounded-sm border-2 border-primary/70 px-3 py-2">
@@ -2604,21 +2600,23 @@
2604
  onAddSample={onAddSample}
2605
  onLoadSamples={onLoadSamples}
2606
  onReposition={onReposition}
2607
- onPrev={() => changePhoto(-1)} onNext={() => changePhoto(1)} navDisabled={genBusy}
2608
  title={title} caption={caption} date={date} time={time}
2609
  onTitleChange={setTitle} onCaptionChange={setCaption}
2610
  onDateChange={setDate} onTimeChange={setTime} />
2611
  <input ref={addPhotoRef} type="file" accept={ACCEPT_ATTR} multiple className="hidden" onChange={onAddPhotos} />
2612
  {stackPhotos.length >= 1 && (
2613
- <div className="flex w-full max-w-sm items-center gap-3">
 
 
 
2614
  <button type="button" onClick={() => addPhotoRef.current && addPhotoRef.current.click()}
2615
  className="flex flex-1 items-center justify-center gap-2 rounded-full border-2 border-primary bg-primary px-4 py-2.5 font-mono text-xs font-semibold uppercase tracking-widest text-primary-foreground transition-colors hover:bg-primary/90">
2616
  <Plus className="size-4" /> Upload
2617
  </button>
2618
- <button type="button" onClick={onRemovePhoto}
2619
- className="flex flex-1 items-center justify-center gap-2 rounded-full border-2 border-[#800020] bg-[#800020] px-4 py-2.5 font-mono text-xs font-semibold uppercase tracking-widest text-white transition-colors hover:opacity-90">
2620
- <Trash2 className="size-4" /> Delete
2621
- </button>
2622
  </div>
2623
  )}
2624
  {/* How this works - matches the polaroid width, all-mono */}
@@ -6843,7 +6841,7 @@
6843
  {aboveCards.map((s) => (
6844
  <div key={s.id} className="absolute bottom-0 left-1/2 -translate-x-1/2"
6845
  style={{ width: "70%", zIndex: s.z }}>
6846
- <PrintCard sheet={s} />
6847
  </div>
6848
  ))}
6849
  </div>
@@ -6860,7 +6858,7 @@
6860
  {/* submerged card lives inside its tray */}
6861
  {subCards.map((s) => (
6862
  <div key={s.id} className="absolute" style={{ inset: "10%", zIndex: 4 + s.z }}>
6863
- <PrintCard sheet={s} />
6864
  </div>
6865
  ))}
6866
  {/* ripple rings anchored to this tray */}
@@ -6991,15 +6989,18 @@
6991
  if (next >= 1) setRevealed(true);
6992
  }, []);
6993
 
 
 
 
6994
  function onPointerDown(e) {
6995
- if (revealed) return;
6996
  try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}
6997
  dragState.current = { active: true, lastX: e.clientX, lastDir: 0, offset: 0 };
6998
  setDragging(true);
6999
  }
7000
  function onPointerMove(e) {
7001
  const s = dragState.current;
7002
- if (!s.active || revealed) return;
7003
  const dx = e.clientX - s.lastX;
7004
  if (Math.abs(dx) < 4) return;
7005
  const dir = dx > 0 ? 1 : -1;
@@ -7023,7 +7024,7 @@
7023
  <div className="relative z-10 h-4 w-3 rounded-sm bg-[oklch(0.45_0.06_60)] shadow-sm" aria-hidden="true" />
7024
  <figure
7025
  className={cn("group relative -mt-1 select-none rounded-sm bg-[oklch(0.99_0.01_85)] p-2 pb-12 shadow-xl",
7026
- dragging ? "" : "dr-sway", revealed ? "cursor-default" : "cursor-grab active:cursor-grabbing")}
7027
  style={{ animationDelay: swayDelay + "s",
7028
  transform: dragging ? "rotate(" + (tilt * 0.3) + "deg) translateX(" + (tilt * 0.4) + "px)" : undefined,
7029
  transition: dragging ? "none" : "transform 0.4s ease" }}
@@ -7032,18 +7033,15 @@
7032
  <img src={(revealed && developedSrc) ? developedSrc : (photo.src || "/assets/placeholder.svg")} alt={revealed ? photo.alt : ""} draggable={false}
7033
  className="h-full w-full object-cover transition-[filter] duration-200"
7034
  style={{ filter: "blur(" + blurPx + "px) grayscale(" + grayscale + "%) brightness(" + brightness + ") sepia(" + ((1 - progress) * 40) + "%)" }} />
7035
- {/* develop status gate — visible so we can see if the watercolour
7036
- call is developing / done / fell back to base */}
7037
- <div className="pointer-events-none absolute left-1.5 top-1.5 z-10 rounded bg-black/55 px-1.5 py-0.5 font-mono text-[8px] uppercase tracking-wide text-white/90">
7038
- {devStatus === "done" ? "★ developed" : devStatus === "base" ? "✕ base" : "developing…"}
7039
- </div>
7040
  <div className="pointer-events-none absolute inset-0 transition-opacity duration-200"
7041
  style={{ opacity: fogOpacity,
7042
  background: "radial-gradient(circle at 50% 40%, oklch(0.35 0.12 25 / 0.65), oklch(0.18 0.06 25 / 0.85))" }} aria-hidden="true" />
7043
  {!revealed ? (
7044
  <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
7045
- <span className="rounded-full bg-[oklch(0.15_0.04_25/0.7)] px-3 py-1 font-serif italic text-[11px] text-[oklch(0.95_0.05_25)]">
7046
- {progress > 0 ? "developing…" : "shake me"}
 
 
7047
  </span>
7048
  </div>
7049
  ) : (
@@ -7082,9 +7080,13 @@
7082
  "@keyframes dr-sway-kf{0%,100%{transform:rotate(-1.5deg)}50%{transform:rotate(1.5deg)}}" +
7083
  "@keyframes dr-fade{from{opacity:0}to{opacity:1}}" +
7084
  "@keyframes dr-cap-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}" +
 
 
7085
  ".dr-flicker{animation:dr-safelight 5s ease-in-out infinite}" +
7086
  ".dr-sway{animation:dr-sway-kf 4s ease-in-out infinite;transform-origin:top center}" +
7087
- "@media (prefers-reduced-motion: reduce){.dr-flicker,.dr-sway{animation:none}}"
 
 
7088
  }} />
7089
 
7090
  {/* hidden filter that ripples the liquid + submerged prints */}
 
1607
  );
1608
  }
1609
 
1610
+ function PhotoStack({ photo, index, total, slideDir, destroying, onDestroyed, onAdd, onAddSample, onLoadSamples, onReposition, onDelete, title, caption, date, time, onTitleChange, onCaptionChange, onDateChange, onTimeChange }) {
1611
  if (!photo) {
1612
  // no photos left - show an empty polaroid inviting an upload, plus a
1613
  // row of sample photos the user can add with one tap.
 
1678
  onReposition={onReposition} />
1679
  <span className="pointer-events-none absolute right-2 top-2 rounded-sm bg-card/90 px-2 py-0.5 font-mono text-xs font-semibold text-primary">{index + 1}/{total}</span>
1680
  </div>
1681
+ {/* delete this photo: a small x at the polaroid's corner */}
1682
+ {onDelete ? (
1683
+ <button type="button" onClick={onDelete} aria-label="Delete this photo" title="Delete this photo"
1684
+ className="absolute -right-2.5 -top-2.5 z-30 flex size-7 items-center justify-center rounded-full border-2 border-[#800020] bg-card text-[#800020] shadow-md transition-colors hover:bg-[#800020] hover:text-white">
1685
+ <X className="size-3.5" />
1686
+ </button>
 
 
 
 
1687
  ) : null}
1688
  </div>
1689
  <div className="mt-3 rounded-sm border-2 border-primary/70 px-3 py-2">
 
2600
  onAddSample={onAddSample}
2601
  onLoadSamples={onLoadSamples}
2602
  onReposition={onReposition}
2603
+ onDelete={onRemovePhoto}
2604
  title={title} caption={caption} date={date} time={time}
2605
  onTitleChange={setTitle} onCaptionChange={setCaption}
2606
  onDateChange={setDate} onTimeChange={setTime} />
2607
  <input ref={addPhotoRef} type="file" accept={ACCEPT_ATTR} multiple className="hidden" onChange={onAddPhotos} />
2608
  {stackPhotos.length >= 1 && (
2609
+ <div className="flex w-full max-w-sm items-center justify-center gap-3">
2610
+ <button type="button" onClick={() => changePhoto(-1)} disabled={genBusy || stackPhotos.length <= 1}
2611
+ aria-label="Previous photo" title={genBusy ? "Developing… cancel to browse" : "Previous photo"}
2612
+ className="flex size-10 shrink-0 items-center justify-center rounded-full border-2 border-primary text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"><ChevronLeft className="size-4" /></button>
2613
  <button type="button" onClick={() => addPhotoRef.current && addPhotoRef.current.click()}
2614
  className="flex flex-1 items-center justify-center gap-2 rounded-full border-2 border-primary bg-primary px-4 py-2.5 font-mono text-xs font-semibold uppercase tracking-widest text-primary-foreground transition-colors hover:bg-primary/90">
2615
  <Plus className="size-4" /> Upload
2616
  </button>
2617
+ <button type="button" onClick={() => changePhoto(1)} disabled={genBusy || stackPhotos.length <= 1}
2618
+ aria-label="Next photo" title={genBusy ? "Developing… cancel to browse" : "Next photo"}
2619
+ className="flex size-10 shrink-0 items-center justify-center rounded-full border-2 border-primary text-primary transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-30 disabled:hover:bg-transparent"><ChevronRight className="size-4" /></button>
 
2620
  </div>
2621
  )}
2622
  {/* How this works - matches the polaroid width, all-mono */}
 
6841
  {aboveCards.map((s) => (
6842
  <div key={s.id} className="absolute bottom-0 left-1/2 -translate-x-1/2"
6843
  style={{ width: "70%", zIndex: s.z }}>
6844
+ <div className="dr-card-in"><PrintCard sheet={s} /></div>
6845
  </div>
6846
  ))}
6847
  </div>
 
6858
  {/* submerged card lives inside its tray */}
6859
  {subCards.map((s) => (
6860
  <div key={s.id} className="absolute" style={{ inset: "10%", zIndex: 4 + s.z }}>
6861
+ <div className="dr-card-sink"><PrintCard sheet={s} /></div>
6862
  </div>
6863
  ))}
6864
  {/* ripple rings anchored to this tray */}
 
6989
  if (next >= 1) setRevealed(true);
6990
  }, []);
6991
 
6992
+ // No shaking until the watercolour has actually developed (or the develop
6993
+ // feature is off / gave up, where "base" means we reveal the original).
6994
+ const canShake = devStatus === "done" || devStatus === "base";
6995
  function onPointerDown(e) {
6996
+ if (revealed || !canShake) return;
6997
  try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}
6998
  dragState.current = { active: true, lastX: e.clientX, lastDir: 0, offset: 0 };
6999
  setDragging(true);
7000
  }
7001
  function onPointerMove(e) {
7002
  const s = dragState.current;
7003
+ if (!s.active || revealed || !canShake) return;
7004
  const dx = e.clientX - s.lastX;
7005
  if (Math.abs(dx) < 4) return;
7006
  const dir = dx > 0 ? 1 : -1;
 
7024
  <div className="relative z-10 h-4 w-3 rounded-sm bg-[oklch(0.45_0.06_60)] shadow-sm" aria-hidden="true" />
7025
  <figure
7026
  className={cn("group relative -mt-1 select-none rounded-sm bg-[oklch(0.99_0.01_85)] p-2 pb-12 shadow-xl",
7027
+ dragging ? "" : "dr-sway", revealed ? "cursor-default" : (canShake ? "cursor-grab active:cursor-grabbing" : "cursor-progress"))}
7028
  style={{ animationDelay: swayDelay + "s",
7029
  transform: dragging ? "rotate(" + (tilt * 0.3) + "deg) translateX(" + (tilt * 0.4) + "px)" : undefined,
7030
  transition: dragging ? "none" : "transform 0.4s ease" }}
 
7033
  <img src={(revealed && developedSrc) ? developedSrc : (photo.src || "/assets/placeholder.svg")} alt={revealed ? photo.alt : ""} draggable={false}
7034
  className="h-full w-full object-cover transition-[filter] duration-200"
7035
  style={{ filter: "blur(" + blurPx + "px) grayscale(" + grayscale + "%) brightness(" + brightness + ") sepia(" + ((1 - progress) * 40) + "%)" }} />
 
 
 
 
 
7036
  <div className="pointer-events-none absolute inset-0 transition-opacity duration-200"
7037
  style={{ opacity: fogOpacity,
7038
  background: "radial-gradient(circle at 50% 40%, oklch(0.35 0.12 25 / 0.65), oklch(0.18 0.06 25 / 0.85))" }} aria-hidden="true" />
7039
  {!revealed ? (
7040
  <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
7041
+ <span className={cn("flex items-center gap-1.5 rounded-full px-3 py-1 font-serif italic text-[11px]",
7042
+ canShake ? "bg-[oklch(0.15_0.04_25/0.7)] text-[oklch(0.95_0.05_25)]" : "bg-black/65 text-white/90")}>
7043
+ {!canShake ? (<><Loader2 className="size-3 animate-spin" aria-hidden="true" /> developing…</>)
7044
+ : progress > 0 ? "keep shaking…" : "developed · shake me"}
7045
  </span>
7046
  </div>
7047
  ) : (
 
7080
  "@keyframes dr-sway-kf{0%,100%{transform:rotate(-1.5deg)}50%{transform:rotate(1.5deg)}}" +
7081
  "@keyframes dr-fade{from{opacity:0}to{opacity:1}}" +
7082
  "@keyframes dr-cap-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}" +
7083
+ "@keyframes dr-card-in{from{opacity:0;transform:translateX(-22px) scale(.95)}to{opacity:1;transform:none}}" +
7084
+ "@keyframes dr-card-sink{from{opacity:0;transform:translateY(-14px) scale(.97)}to{opacity:1;transform:none}}" +
7085
  ".dr-flicker{animation:dr-safelight 5s ease-in-out infinite}" +
7086
  ".dr-sway{animation:dr-sway-kf 4s ease-in-out infinite;transform-origin:top center}" +
7087
+ ".dr-card-in{animation:dr-card-in .42s cubic-bezier(.34,1.18,.5,1) both}" +
7088
+ ".dr-card-sink{animation:dr-card-sink .5s cubic-bezier(.4,.1,.2,1) both}" +
7089
+ "@media (prefers-reduced-motion: reduce){.dr-flicker,.dr-sway,.dr-card-in,.dr-card-sink{animation:none}}"
7090
  }} />
7091
 
7092
  {/* hidden filter that ripples the liquid + submerged prints */}