Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/lightbox-double-click-point.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] Lightbox: a double-click now zooms 2× about the clicked point instead of about the centre, and the next goes back; the keyboard keeps its 2× zoom about the centre.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/lightbox-every-pointer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] Lightbox: with `hasZoom`, a ctrl-wheel or a trackpad pinch now zooms a photo, and a trackpad's scroll pans a zoomed photo.

@vjeux
11 changes: 11 additions & 0 deletions .changeset/lightbox-gallery-paging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@astryxdesign/core': patch
---

[feat] A Lightbox gallery now pages the way the iOS Photos viewer does. On a phone or tablet, drag or flick between items: the row follows your finger, and the release lands on the next or previous item, one at most. A gap of 40 px separates the items while they move. A zoomed photo pans with your finger until it reaches its edge; a new drag from that edge moves to the next item. A photo you leave zoomed returns to its fitted size once the gallery rests. Under a finger, the previous and next buttons and the arrow keys slide to the item instead of swapping it in place.

Each item's caption moves with its item, right under its photo. `onIndexChange` also reports a swipe.

Lightbox now loads its touch code with it, as Toast and Slider do. Its import grows from 51.4 KB to 99.6 KB gzip with the zoom and paging beneath, mostly shared touch modules that a page loads once; Lightbox's own code adds 14.1 KB minified.

@vjeux
9 changes: 9 additions & 0 deletions .changeset/lightbox-touch-zoom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astryxdesign/core': patch
---

[feat] Lightbox's zoom now works under a finger the way the iOS Photos viewer does, on a phone, a tablet or a touchscreen laptop. Pinch the photo to zoom around your fingers, and move them to pan while you zoom. Past the zoom limits the photo gives way, then eases back when you let go. Double-tap zooms until the photo fills the screen, at least 2×, and keeps the tapped point under your finger; double-tap again to go back. On a phone or a tablet, a zoomed photo pans with momentum, stretches past its edges and settles back. The keyboard keeps the zoom it had: 2× about the centre, over the same 200 ms ease-out, and keyboard panning now stops at the photo's edges instead of carrying it out of view.

The photo's stage fills the viewer. A pinch on the photo no longer zooms the page, and a pinch on the controls or caption still does. With `hasZoom` on, a tap on the dark area beside the photo closes the viewer after the double-tap wait instead of at once; a click still closes it at once. The zoom animations run on the compositor, so they stay smooth while the page is busy. With `hasZoom` off, nothing changes. With `hasZoom`, Close, Previous, Next and the counter sit on a dark translucent backing, so they stay readable over a zoomed photo.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/uikit-zoom-media.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the UIKit constants table gains UIScrollView zooming for a photo viewer, measured on iOS 26.2. It holds the give past the zoom limits, the settle's eased curve and length law, and Photos' aspect-fill double tap and maximum zoom. The zoom stage geometry that Lightbox's touch zoom is built on comes with it. Neither is exported from the package entry point, and Lightbox's touch zoom reads them.

@vjeux
133 changes: 133 additions & 0 deletions apps/storybook/stories/Lightbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,111 @@ export const WithZoom: Story = {
},
};

/** Zoom on a photo with a caption, which sits under the fitted photo. */
export const ZoomWithCaption: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open zoom with caption</button>
<Lightbox
isOpen={isOpen}
onOpenChange={setIsOpen}
media={{
src: SAMPLE_IMAGE,
alt: 'Forest path',
caption: 'A winding path through the forest',
}}
hasZoom
/>
</>
);
},
};

/**
* A gallery a phone can page through. Drag or flick between photos, with
* Photos' 40 pt gap. Pinch or double-tap a photo to zoom, and drag a zoomed
* photo to its edge before the next drag pages.
*/
export const GalleryWithZoom: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
const [index, setIndex] = useState(0);
return (
<>
<button onClick={() => setIsOpen(true)}>Open gallery with zoom</button>
<Lightbox
isOpen={isOpen}
onOpenChange={setIsOpen}
media={GALLERY_MEDIA}
index={index}
onIndexChange={setIndex}
hasZoom
/>
</>
);
},
};

/**
* A gallery whose captions differ in length: the photo keeps one box
* whichever pages, and so whichever captions, are rendered.
*/
export const GalleryWithCaptions: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>
Open gallery with captions
</button>
<Lightbox
isOpen={isOpen}
onOpenChange={setIsOpen}
media={[
{
...GALLERY_MEDIA[0],
caption:
'A winding path through the forest, past the old mill and down to the river, where the trail turns back toward the village',
},
GALLERY_MEDIA[1],
GALLERY_MEDIA[3],
{...GALLERY_MEDIA[2], caption: 'Golden hour'},
]}
hasZoom
/>
</>
);
},
};

/**
* A controlled gallery whose parent keeps its index, paging only with its own
* buttons, say while a change is confirmed. A swipe returns to the photo the
* parent shows.
*/
export const GalleryKeepsIndex: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>
Open gallery that keeps its photo
</button>
<Lightbox
isOpen={isOpen}
onOpenChange={setIsOpen}
media={GALLERY_MEDIA}
index={0}
onIndexChange={() => {}}
hasZoom
/>
</>
);
},
};

export const WithCaption: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
Expand All @@ -125,6 +230,34 @@ export const WithCaption: Story = {
},
};

/**
* A gallery with a video between two photos. A drag on the video's frame
* pages, and a drag that starts on its controls stays the video's.
*/
export const GalleryWithVideo: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open gallery with video</button>
<Lightbox
isOpen={isOpen}
onOpenChange={setIsOpen}
media={[
GALLERY_MEDIA[0],
{
src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm',
alt: 'Flower blooming',
type: 'video',
},
GALLERY_MEDIA[1],
]}
/>
</>
);
},
};

export const Video: Story = {
render: () => {
const [isOpen, setIsOpen] = useState(false);
Expand Down
3 changes: 3 additions & 0 deletions docs/specs/AST-094-touch-presentations/motion-roles.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ Every motion role a registered table lists (`spec:AST-094` FR4, DEC-5): its kind
| `timing.curves.move` | curve | cubic-bezier(0.42, 0, 0.58, 1) | `spec:AST-094` FR4 |
| `timing.curves.viewChange` | curve | cubic-bezier(0.25, 0.1, 0.25, 1) | `spec:AST-094` FR4 |
| `timing.stateChangeMs` | duration | 250 ms | `spec:AST-094` FR4 |
| `zoomMedia.settle.baseMs` | duration | 100 ms | `component:Lightbox` |
| `zoomMedia.settle.lnRatioMs` | duration | 300 ms | `component:Lightbox` |
| `zoomMedia.settle.maxMs` | duration | 400 ms | `component:Lightbox` |
Loading