Spaces:
Paused
Paused
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
- 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,
|
| 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 |
-
{/*
|
| 1682 |
-
{
|
| 1683 |
-
<
|
| 1684 |
-
|
| 1685 |
-
|
| 1686 |
-
|
| 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 |
-
|
| 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={
|
| 2619 |
-
|
| 2620 |
-
<
|
| 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="
|
| 7046 |
-
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
|
|
|
| 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 */}
|