Skip to content

feat(media): masonry gallery, lightbox, media Thing pages + title/description (stacked on #306) - #312

Merged
lopugit merged 15 commits into
developfrom
claude/media-thing-pages
Aug 21, 2026
Merged

feat(media): masonry gallery, lightbox, media Thing pages + title/description (stacked on #306)#312
lopugit merged 15 commits into
developfrom
claude/media-thing-pages

Conversation

@lopugit

@lopugit lopugit commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Media/attachments become first-class Things: masonry image layout in posts, click-to-view lightbox, a dedicated /media/:id Thingtime-UI page per media with its own comments/reactions, plus owner title + description fields — while every media stays an attachment inside its parent post's gallery.

Stacked on #306 (merged); base is now develop.

What's in here

  • Server — optional title (≤200) + description (≤2000) on the attachment crystal (sanitize + canonical + projection + size accounting; legacy docs stay valid); owner-only POST /api/v1/attachments/annotate ({ id, title?, description? }, null/'' clears, works on draft AND bound attachments, storage-delta accounted, registered in all three places incl. apiDocs); media things readable via GET /api/v1/things?id=<attachmentId> with a post-shaped projection + parent walk, no private object fields leaked.
  • Masonry — post image sections render CSS-columns masonry (natural aspect ratios, 1/2/3 responsive columns, break-inside avoid); videos/files unchanged.
  • LightboxMediaLightbox: full image, title/description caption, prev/next (buttons + arrow keys), Open-page deeplink to /media/:id, download, Esc/backdrop close (Esc handled in the window listener since autoFocus is off and Chakra's closeOnEsc never hears the key otherwise).
  • Media page/media/:id in the Thingtime shell: large media, title/description (owner-editable), author, parent-post link, working reactions + comments on the media thing, 404-safe.
  • Annotate UI — shared AttachmentAnnotatePopover pencil on ready composer tiles + edit-gallery tiles (and the media page's own editor): optimistic apply, revert + Lopu toast on failure, popover fits 375px.
  • Tests/docstest:attachments 108/108, test:things 6/6, lint clean on changed files; TESTING.md checklist section for masonry/lightbox/media page/annotate/delete-cascade.

Verification evidence (preview E2E, deployment thingtime-jrfx1ox7z @ 1819b84)

Fixture: private post by throwaway dragdrop-e2e-0818 (3 images + 2 files, reordered C/A/B by the #306 E2E).

  • Masonry: desktop 3-col and 375px 2-col render with natural ratios, files below, no horizontal overflow (measured scrollWidth == clientWidth).
  • Lightbox: opens from tile click, caption shows title · n of 3 + description, ArrowRight steps alpha→bravo, backdrop click closes, Esc closes (regression found in E2E and fixed in this PR), Open-page + download links present.
  • /media/:id: renders desktop + 375px in the app shell; comment "Media page E2E comment 🧪" and ❤️ reaction created through the page and confirmed server-side (commentCount: 1, reactionCounts: {"❤️":1}, viewerReactions: ["❤️"]) and after reload on a different deployment of the same DB.
  • Security: unauthenticated GET /api/v1/things?id=<private media id> → 404, no fields leaked.
  • Annotate, all three surfaces against the live endpoint:
    • media page editor → title: "Charlie the blue square" + description persisted (verified via parent-post projection),
    • edit-gallery pencil popover → title: "Alpha in rose" + description persisted,
    • composer tile pencil on a fresh S3 upload (synthetic drop, draft attachment) → saved, popover re-opens pre-filled from the patched local state; draft removed cleanly afterwards.
  • Masonry tile shows the annotated title as a gradient overlay; lightbox caption picks it up too.

🤖 Generated with Claude Code

Media layout editor (added after review start — commits 368426da, 39e1bb85, 3ce98b2b)

Owner-chosen gallery layouts, three tiers, one source of truth: post-level crystal.mediaLayout ({ mode: masonry|rows|grid, pattern?, columns?, spans? }, strictly bounded in sanitizePostCrystal, absent = today's masonry, legacy posts untouched, projected on posts/comments/media things).

  • Tier 1 — presets: composer + edit picker Auto 🧱 / Rows 🥞 / Grid 🔳; Rows takes a 1-2-3-style pattern (live CSS preview, extras repeat the last row; hero rows 16:9, pairs 4:3, 3+ square); Grid takes a 1–6 column stepper. Rides the existing create/edit payloads incl. the frozen create expectation (sameJson stays exact).
  • Tier 2 — spans: per-tile size badge (grid mode) cycling 1×1 → 2×1 → 1×2 → 2×2 on composer tiles + edit gallery; redundant normals pruned server-side.
  • Tier 3 — canvas editor: live CSS-grid preview with a columns slider and a ⤡ handle per tile — pointer-drag (mouse/touch, pointer capture, snap at ~45% of a cell, live outline) or arrow keys resize; writes the same spans state as the badges.

Layout E2E on preview thingtime-3nwo0lkrb @ 3ce98b2b (fixture post, dragdrop-e2e-0818): edit → Rows 1-2 → Save → crystal {mode:"rows",pattern:[1,2]} + hero-over-pair render after reload; edit reopens with Rows/1-2 pre-seeded; Grid → canvas pointer-drag made charlie wide and ArrowDown made alpha tall (badge + handle labels agree) → Save → crystal {mode:"grid",columns:3,spans:{charlie:wide,alpha:tall}} (normal pruned) → viewer render matches the canvas exactly at desktop AND 375px with zero horizontal overflow; lightbox order stays attachment order. test:schemas 61/61 (8 new mediaLayout tests), test:attachments 108/108, test:things 6/6, lint clean.

Independent re-verification on preview thingtime-5jmk96igv @ 3ce98b2b (same fixture post, second session): stored {mode:"rows",pattern:[1,2]} renders hero-over-pair and the edit picker reopens pre-seeded on Rows/1-2; switching to Grid, a horizontal pointer-drag on charlie's handle snapped it to wide and a vertical drag on alpha's to tall (recorded event trace: pointerdown → 2×pointermove → pointerup, handle aria-labels follow); Save → reload → crystal is exactly {mode:"grid",columns:3,spans:{charlie:"wide",alpha:"tall"}}. Rendered tile geometry matches the spans to the pixel — desktop permalink 402×201 / 198×396 / 198×198, desktop feed 407×204 / 201×401 / 201×201, 375px 204×102 / 99×198 / 99×99 — with scrollWidth === clientWidth (no horizontal overflow) on the permalink, the profile feed (compact PostCard path) and the editor itself at 375px, where the picker pills, column stepper and grid canvas all wrap cleanly.

Scope notes, stated plainly: the non-owner path is verified structurally rather than with a second account — the only fixture post with images is Private, and mediaLayoutOf() projects the field with no viewer branch while PostAttachments contains zero owner/viewer conditionals (only the edit affordances are owner-gated), so a reader sees the same layout by construction; a real finger drag could not be exercised end-to-end because the automation pane's touch emulation stalls after touchstart, so touch was covered by driving the same handlers with pointerType: 'touch' events (bravo → wide), on top of the confirmed touch-action: none on the handle. The local dev stack cannot show this feature at all (no local S3, empty local feed), so the preview is the verification surface.

Follow-up commit dba6c702 — clicking a resize handle and then pressing an arrow key did nothing, because preventDefault() in handlePointerDown also suppresses the native focus; the handle now takes focus explicitly (preventScroll), so the keyboard fallback works after a click and not only after tabbing to it. Re-checked green: test:schemas 61/61, test:attachments 108/108, test:things 6/6, lint clean.

lopugit and others added 3 commits August 18, 2026 23:07
Attachment crystals gain optional title (<=200 chars, single line) and
description (<=2000 chars, newlines allowed) - sanitized, canonical
(closed shape with the two optional keys), projected in public
metadata, and absent-never-empty. POST /api/v1/attachments/annotate
sets/clears them on an owned READY attachment (ready-only so finalize
cannot clobber early annotations) with exact storage-delta accounting
like markReady. Client PublicAttachment + normalize + useApi carry the
fields; mediaPageUrl() names the /media/:id deeplink.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/v1/things?id=<attachmentId> now returns the attachment's
post-shaped projection (reactions, comments, views - the resolvers are
target-generic) plus parent/root resolving to the post it is bound to,
powering the /media/:id Thing page. Attachment kind stays protected:
annotate is the only crystal mutation path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Post image attachments now flow as a natural-aspect CSS-columns masonry
(responsive 1/2/3 columns, title chip overlay). Clicking opens the new
MediaLightbox: arrow-key/edge navigation, description caption, download,
and an Open deeplink to /media/:id - every attachment's own Thingtime
page. The media page renders the attachment as a full PostCard (its own
reactions, comments, and views - relational things targeting the
attachment id), owner title/description editing via annotate, and a
link back to the parent post. PostCard gains a mediaThing mode that
keeps interactions but drops edit/privacy/delete (attachment kind is
protected; lifecycle belongs to the parent post).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview Aug 21, 2026 5:53am
thingtime (develop) Canceled Canceled Aug 21, 2026 5:53am

Request Review

@lopugit
lopugit changed the base branch from claude/media-files-sort-drag-drop-1f75a0 to develop August 18, 2026 14:17
@github-actions
github-actions Bot temporarily deployed to develop-pr-312 August 18, 2026 14:17 Destroyed
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

🧹 Develop S3 preview removed

The PR-specific alias and every workflow-created develop deployment were removed when this PR closed.

The ordinary generated Vercel Preview remains available on the shared development runtime.

…s) + lightbox Esc close

- AttachmentAnnotatePopover: shared pencil popover editing title/description,
  saved via /api/v1/attachments/annotate, optimistic apply + revert with a
  Lopu toast on failure; fits 375px viewports
- wired into ready composer tiles (visual + file rows) via a new
  updateAttachment patch in useAttachmentUploads, and into the edit-mode
  AttachmentReorderGallery (id order untouched, so no false order change)
- MediaLightbox: close on Escape from the window keydown listener too —
  autoFocus is off, so Chakra's closeOnEsc never hears the key when focus
  sits outside the modal (found in preview E2E)
- TESTING.md: media thing pages checklist (masonry, lightbox, /media/:id,
  annotate, delete cascade)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to develop-pr-312 August 18, 2026 14:29 Destroyed
@github-actions
github-actions Bot temporarily deployed to develop-pr-312 August 18, 2026 14:46 Destroyed
lopugit added a commit that referenced this pull request Aug 18, 2026
lopugit added a commit that referenced this pull request Aug 18, 2026
…erver validation

- crystal.mediaLayout on posts: mode masonry|rows|grid, rows pattern (1-25
  entries of 1-6, extras repeat the last row size), grid columns 1-6 +
  per-image spans (wide/tall/big; redundant normals dropped) — sanitized in
  sanitizePostCrystal with strict bounds, legacy crystals stay valid; PATCH
  sets/clears it through the same validator
- projected as PublicPost/PublicComment.mediaLayout (toPublicPosts project +
  buildComment + addComment echo) so cards, comments, shares, and media
  things all carry it
- PostAttachments renders the three modes: masonry unchanged; rows via
  pattern chunks (hero 16:9, pairs 4:3, 3+ square); grid via CSS grid with
  span-aware aspect ratios and dense flow — lightbox order stays attachment
  order in every mode
- 8 new schema tests (test:schemas 61/61); TESTING.md layout checklist

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to develop-pr-312 August 18, 2026 16:01 Destroyed
github-actions Bot added a commit that referenced this pull request Aug 18, 2026
…ayout editor)

- MediaLayoutControls: Auto 🧱 / Rows 🥞 / Grid 🔳 segmented picker with a
  live pattern preview ('1-2-3' style input, bounded parse) and a grid
  columns stepper; SpanCycleButton cycles a tile normal → wide → tall → big
- PostComposer owns the layout state (seeded from editPost.mediaLayout),
  shows the picker from 2+ visual attachments, prunes spans to the outgoing
  visual set, and sends crystal.mediaLayout through create, edit (full-crystal
  replace), and the frozen create expectation (sameJson stays exact)
- createPost/addComment map body.mediaLayout into the sanitized crystal
- AttachmentComposer + AttachmentReorderGallery grow a tileExtras render prop
  (badge bottom-left; grip/pencil corners unchanged), active in grid mode

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tor)

- MediaLayoutCanvas: live CSS-grid preview of the actual layout with a
  columns slider (1-6, relayouts live) and a ⤡ corner handle per tile —
  pointer-event drag (mouse + touch, touchAction none, pointer capture)
  snaps tiles between normal/wide/tall/big at ~45% of a cell, with a live
  outline preview; arrow-key fallback on the focused handle (right wider,
  left narrower, down taller, up shorter)
- shown in the composer + edit mode whenever Grid is active with 2+ visual
  attachments; writes the same layoutSpans/columns state as the tier-2
  badges, so canvas drags, badges, and the saved crystal never drift
- span geometry helpers (spanColumns/spanRows/spanAspect) exported from
  PostAttachments so render and editor share one source of truth

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to develop-pr-312 August 18, 2026 16:11 Destroyed
@github-actions
github-actions Bot temporarily deployed to develop-pr-312 August 19, 2026 00:13 Destroyed
handlePointerDown calls preventDefault to keep the drag from selecting or
scrolling, which also suppresses the native focus — so clicking a tile's
resize handle and then pressing an arrow key did nothing. Take focus
explicitly (preventScroll so the canvas does not jump) and the documented
keyboard fallback works after a click, not only after tabbing to the handle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
github-actions Bot added a commit that referenced this pull request Aug 21, 2026
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🤝 Merged develop into claude/media-thing-pages — conflicts auto-resolved by the resolve-pr-conflicts workflow.

Conflicted files:

  • remix/CHANGELOG.md

Please review the merge commit before relying on it.

Conflicted paths: remix/CHANGELOG.md

Resolved by the resolve-pr-conflicts workflow: https://github.com/lopugit/thingtime/actions/runs/32449776090

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
github-actions Bot temporarily deployed to develop-pr-312 August 21, 2026 05:24 Destroyed
@lopugit
lopugit merged commit f05fb06 into develop Aug 21, 2026
24 checks passed
@lopugit

lopugit commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

🤖 Promotion conflict resolution was queued automatically for promote/pr-312-media-thing-pages--to-main at exact base main (62bd8429712f761daea09bf4e6484883dffcfb8c).

Conflicted source paths: remix/CHANGELOG.md, remix/app/api/utils/attachments/attachmentCore.ts, remix/app/components/Attachments/PostAttachments.tsx, remix/app/components/Attachments/attachmentTypes.ts, remix/app/components/Attachments/attachmentUiCore.ts.

The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

⚠️ Automatic promotion conflict resolution exhausted or failed before a reviewable PR was fully published.

Branch: promote/pr-312-media-thing-pages--to-main · base: main at 1eb3c7b1504c96398fa8b12f9a2fabd1ca36f8b0 · plan: 76df588693b2cd4c3f0d5ab980aa5613cc73eb4ab6367f0c1b4b2d1cd1253109.

No newer branch work was overwritten or deleted. The same immutable snapshot will not spend AI again; a changed source/base snapshot retries automatically. Review the run.

@lopugit

lopugit commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

🤖 Protected promotion replay was queued automatically for promote/pr-312-media-thing-pages--to-main at exact base main (62bd8429712f761daea09bf4e6484883dffcfb8c).

The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.

⚠️ Source lineage is review-required-ambiguous; the resulting PR will carry source-lineage-unverified and must be reviewed for restoration intent.

@github-actions github-actions Bot added the ai-promotion-paused Automatic promotion resolution paused for this exact snapshot label Aug 21, 2026
lopugit added a commit that referenced this pull request Aug 21, 2026
@lopugit

lopugit commented Aug 21, 2026

Copy link
Copy Markdown
Owner Author

🤖 Protected promotion replay was queued automatically for promote/pr-312-media-thing-pages--to-main at exact base main (ea6e8b9982fe6d69eac7cc4a4e2ae199471e0527).

The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.

⚠️ Source lineage is review-required-ambiguous; the resulting PR will carry source-lineage-unverified and must be reviewed for restoration intent.

@github-actions github-actions Bot added ai-promotion-paused Automatic promotion resolution paused for this exact snapshot and removed ai-promotion-paused Automatic promotion resolution paused for this exact snapshot labels Aug 21, 2026
@lopugit

lopugit commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

🤖 Protected promotion replay was queued automatically for promote/pr-312-media-thing-pages--to-main at exact base main (d0879a2035268827ffe8bf030f395c189952a320).

The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.

⚠️ Source lineage is review-required-ambiguous; the resulting PR will carry source-lineage-unverified and must be reviewed for restoration intent.

@github-actions github-actions Bot added ai-promotion-paused Automatic promotion resolution paused for this exact snapshot and removed ai-promotion-paused Automatic promotion resolution paused for this exact snapshot labels Aug 23, 2026
@lopugit

lopugit commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

🤖 Protected promotion replay was queued automatically for promote/pr-312-media-thing-pages--to-main at exact base main (5691480ec73943868b7509f21e29c19fb168f6aa).

The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.

⚠️ Source lineage is review-required-ambiguous; the resulting PR will carry source-lineage-unverified and must be reviewed for restoration intent.

@github-actions github-actions Bot added ai-promotion-paused Automatic promotion resolution paused for this exact snapshot and removed ai-promotion-paused Automatic promotion resolution paused for this exact snapshot labels Aug 23, 2026
@lopugit

lopugit commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

🤖 Protected promotion replay was queued automatically for promote/pr-312-media-thing-pages--to-main at exact base main (18095d4e018226803a4984d9a06a46c4a8c0bcf8).

The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.

⚠️ Source lineage is review-required-ambiguous; the resulting PR will carry source-lineage-unverified and must be reviewed for restoration intent.

@github-actions github-actions Bot added ai-promotion-paused Automatic promotion resolution paused for this exact snapshot and removed ai-promotion-paused Automatic promotion resolution paused for this exact snapshot labels Aug 23, 2026
@lopugit

lopugit commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

🤖 Protected promotion replay was queued automatically for promote/pr-312-media-thing-pages--to-main at exact base main (1eb3c7b1504c96398fa8b12f9a2fabd1ca36f8b0).

The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.

⚠️ Source lineage is review-required-ambiguous; the resulting PR will carry source-lineage-unverified and must be reviewed for restoration intent.

@github-actions github-actions Bot added ai-promotion-paused Automatic promotion resolution paused for this exact snapshot and removed ai-promotion-paused Automatic promotion resolution paused for this exact snapshot labels Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-promotion-paused Automatic promotion resolution paused for this exact snapshot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant