feat(media): masonry gallery, lightbox, media Thing pages + title/description (stacked on #306) - #312
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🧹 Develop S3 preview removedThe 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>
…ing pages + title/description (stacked on #306)
…ing pages + title/description (stacked on #306)
…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>
…ing pages + title/description (stacked on #306)
…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>
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>
…ing pages + title/description (stacked on #306)
|
🤝 Merged Conflicted files:
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>
|
🤖 Promotion conflict resolution was queued automatically for Conflicted source paths: The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed. |
|
Branch: 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. |
|
🤖 Protected promotion replay was queued automatically for The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.
|
|
🤖 Protected promotion replay was queued automatically for The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.
|
|
🤖 Protected promotion replay was queued automatically for The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.
|
|
🤖 Protected promotion replay was queued automatically for The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.
|
|
🤖 Protected promotion replay was queued automatically for The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.
|
|
🤖 Protected promotion replay was queued automatically for The trusted worker will reconstruct, verify, publish, and attest the review branch; no manual branch update is needed.
|
Media/attachments become first-class Things: masonry image layout in posts, click-to-view lightbox, a dedicated
/media/:idThingtime-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
title(≤200) +description(≤2000) on the attachment crystal (sanitize + canonical + projection + size accounting; legacy docs stay valid); owner-onlyPOST /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 viaGET /api/v1/things?id=<attachmentId>with a post-shaped projection + parent walk, no private object fields leaked.MediaLightbox: 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 sinceautoFocusis off and Chakra'scloseOnEscnever hears the key otherwise)./media/:idin the Thingtime shell: large media, title/description (owner-editable), author, parent-post link, working reactions + comments on the media thing, 404-safe.AttachmentAnnotatePopoverpencil on ready composer tiles + edit-gallery tiles (and the media page's own editor): optimistic apply, revert + Lopu toast on failure, popover fits 375px.test:attachments108/108,test:things6/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).scrollWidth == clientWidth).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.GET /api/v1/things?id=<private media id>→ 404, no fields leaked.title: "Charlie the blue square"+ description persisted (verified via parent-post projection),title: "Alpha in rose"+ description persisted,🤖 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 insanitizePostCrystal, absent = today's masonry, legacy posts untouched, projected on posts/comments/media things).Auto 🧱 / Rows 🥞 / Grid 🔳; Rows takes a1-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 (sameJsonstays exact).1×1 → 2×1 → 1×2 → 2×2on composer tiles + edit gallery; redundantnormals pruned server-side.Layout E2E on preview
thingtime-3nwo0lkrb@3ce98b2b(fixture post,dragdrop-e2e-0818): edit → Rows1-2→ Save → crystal{mode:"rows",pattern:[1,2]}+ hero-over-pair render after reload; edit reopens with Rows/1-2pre-seeded; Grid → canvas pointer-drag made charliewideand ArrowDown made alphatall(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:schemas61/61 (8 new mediaLayout tests),test:attachments108/108,test:things6/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 onRows/1-2; switching to Grid, a horizontal pointer-drag on charlie's handle snapped it towideand a vertical drag on alpha's totall(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 permalink402×201 / 198×396 / 198×198, desktop feed407×204 / 201×401 / 201×201, 375px204×102 / 99×198 / 99×99— withscrollWidth === clientWidth(no horizontal overflow) on the permalink, the profile feed (compactPostCardpath) 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 whilePostAttachmentscontains 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 aftertouchstart, so touch was covered by driving the same handlers withpointerType: 'touch'events (bravo →wide), on top of the confirmedtouch-action: noneon 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, becausepreventDefault()inhandlePointerDownalso 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:schemas61/61,test:attachments108/108,test:things6/6, lint clean.