diff --git a/flying-gallery/index.html b/flying-gallery/index.html new file mode 100644 index 0000000..6851f82 --- /dev/null +++ b/flying-gallery/index.html @@ -0,0 +1,23 @@ + + + + + + + Flying Gallery — @wethegit/react-gallery + + + +
+ + + diff --git a/package-lock.json b/package-lock.json index 332b678..401a7dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@wethegit/react-gallery", - "version": "4.0.1", + "version": "4.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@wethegit/react-gallery", - "version": "4.0.1", + "version": "4.0.2", "license": "MIT", "devDependencies": { "@types/react": "~19.0.12", diff --git a/src/flying-gallery/FlyingGallery.jsx b/src/flying-gallery/FlyingGallery.jsx new file mode 100644 index 0000000..a573232 --- /dev/null +++ b/src/flying-gallery/FlyingGallery.jsx @@ -0,0 +1,146 @@ +import { + Gallery, + GalleryMain, + GalleryItem, + GalleryNav, + GalleryPagination, + GalleryPaginationItem, +} from "../lib" +import styles from "./flying-gallery.module.css" + +const ITEMS = [ + { + id: 1, + image: + "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=1200&h=700&q=80", + thumb: + "https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?auto=format&fit=crop&w=200&h=120&q=60", + alt: "Sun rays through a forest", + }, + { + id: 2, + image: + "https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1200&h=700&q=80", + thumb: + "https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=200&h=120&q=60", + alt: "Snow-capped mountain peak at night", + }, + { + id: 3, + image: + "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1200&h=700&q=80", + thumb: + "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=200&h=120&q=60", + alt: "Mountain reflected in a still lake", + }, + { + id: 4, + image: + "https://images.unsplash.com/photo-1501854140801-50d01698950b?auto=format&fit=crop&w=1200&h=700&q=80", + thumb: + "https://images.unsplash.com/photo-1501854140801-50d01698950b?auto=format&fit=crop&w=200&h=120&q=60", + alt: "Aerial view of green hills", + }, + { + id: 5, + image: + "https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?auto=format&fit=crop&w=1200&h=700&q=80", + thumb: + "https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?auto=format&fit=crop&w=200&h=120&q=60", + alt: "Orange wildflower field", + }, + { + id: 6, + image: + "https://images.unsplash.com/photo-1547234935-80c7145ec969?auto=format&fit=crop&w=1200&h=700&q=80", + thumb: + "https://images.unsplash.com/photo-1547234935-80c7145ec969?auto=format&fit=crop&w=200&h=120&q=60", + alt: "Tropical beach with clear water", + }, +] + +export function FlyingGallery() { + return ( +
+ +
+ { + const isPrev = activeIndex > index + + return ( + +
+ {item.alt} +
+
+ ) + }} + /> + + + Previous + + + + Next + +
+ + ( + + {item.alt} + + )} + /> +
+
+ ) +} diff --git a/src/flying-gallery/flying-gallery.module.css b/src/flying-gallery/flying-gallery.module.css new file mode 100644 index 0000000..600b9d5 --- /dev/null +++ b/src/flying-gallery/flying-gallery.module.css @@ -0,0 +1,206 @@ +.root { + --perspective: 100px; + --xoffset: 600px; + --yoffset: 200px; + --rotation: 16deg; + --blur: 2px; + + background: #0a0a0f; + color: #fff; + display: flex; + flex-direction: column; + gap: 1.5rem; + min-height: 100vh; + padding: 3rem 0 2rem; + overflow: hidden; +} + +.slideArea { + position: relative; +} + +/* + * The gallery is configured with --item-width: 4px; --gap: 0, so all
  • + * elements collapse to near-zero horizontal positions. + * All visual positioning is handled here on .slide using CSS vars inherited + * from the library-managed
  • : + * + * --center-offset absolute distance from active (0, 1, 2 …) + * --duration transition speed + * + * className={styles.item} is appended to the library's class list on each
  • . + * Since this CSS module is loaded after the library's, same-specificity rules here + * win on source order.. + */ +.item { + transform: translateX(0) !important; +} + +[data-side="prev"].item { + z-index: calc(20 - var(--center-offset, 0)); +} + +.slide { + aspect-ratio: 16 / 9; + border-radius: 10px; + overflow: hidden; + position: relative; + transition: + transform var(--duration, 0.5s), + filter var(--duration, 0.5s), + opacity var(--duration, 0.5s); + width: 100%; +} + +.slide img { + display: block; + height: 100%; + object-fit: cover; + width: 100%; +} + +[data-side="active"] .slide { + filter: none; + opacity: 1; + transform: perspective(var(--perspective, 1000px)); +} + +[data-side="prev"] .slide { + filter: blur(calc(var(--center-offset, 0) * var(--blur, 2px))); + opacity: calc(1 - var(--center-offset, 0) * 0.68); + transform: perspective(var(--perspective, 1000px)) + translateX(calc(var(--center-offset, 0) * -1 * var(--xoffset, 600px))) + translateY(calc(var(--center-offset, 0) * -1 * var(--yoffset, 200px))) + translateZ(calc(var(--center-offset, 0) * 40px)) + rotateZ( + calc( + var(--center-offset, 0) * -1 * var(--rotation, 16deg) + + sin(calc(var(--i, 0) * 1rad)) * 6deg + ) + ); +} + +[data-side="next"] .slide { + filter: blur(calc(var(--center-offset, 0) * var(--blur, 2px))); + opacity: calc(1 - var(--center-offset, 0) * 0.3); + transform: perspective(var(--perspective, 1000px)) + translateX(calc(var(--center-offset, 0) * var(--xoffset, 600px))) + translateY(calc(var(--center-offset, 0) * var(--yoffset, 200px))) + translateZ(calc(var(--center-offset, 0) * -80px)) + rotateZ( + calc( + var(--center-offset, 0) * var(--rotation, 16deg) + + sin(calc(var(--i, 0) * 1.7rad)) * 6deg + ) + ); +} + +@media (prefers-reduced-motion: reduce) { + .slide { + transition: none; + } +} + +.navPrev, +.navNext { + align-items: center; + appearance: none; + background: rgb(255 255 255 / 12%); + backdrop-filter: blur(8px); + border: 1px solid rgb(255 255 255 / 20%); + border-radius: 50%; + color: #fff; + cursor: pointer; + display: flex; + height: 3rem; + justify-content: center; + position: absolute; + top: 50%; + transform: translateY(-50%); + transition: + background 0.2s, + border-color 0.2s; + width: 3rem; + z-index: 1000; +} + +.navPrev:hover, +.navNext:hover { + background: rgb(255 255 255 / 25%); + border-color: rgb(255 255 255 / 40%); +} + +.navPrev:disabled, +.navNext:disabled { + opacity: 0.3; + pointer-events: none; +} + +.navPrev { + left: 1rem; +} + +.navNext { + right: 1rem; +} + +.thumbs { + display: flex !important; + gap: 0.5rem !important; + justify-content: center; + list-style: none; + margin-top: 10px; + overflow-x: auto; + padding: 0 1.5rem; + position: relative; + z-index: 10; +} + +.thumbItem { + flex-shrink: 0; + line-height: 0; +} + +.thumbButton { + appearance: none; + background: none; + border: 2px solid transparent; + border-radius: 5px; + cursor: pointer; + display: block; + height: 52px; + opacity: 0.45; + overflow: hidden; + padding: 0; + transition: + opacity 0.2s, + border-color 0.2s; + width: 88px; +} + +.thumbButton img { + display: block; + height: 100%; + object-fit: cover; + width: 100%; +} + +.thumbButton[aria-current="true"] { + border-color: #fff; + opacity: 1; +} + +.thumbButton:hover { + opacity: 0.8; +} + +.srOnly { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} diff --git a/src/flying-gallery/index.jsx b/src/flying-gallery/index.jsx new file mode 100644 index 0000000..9bf6b91 --- /dev/null +++ b/src/flying-gallery/index.jsx @@ -0,0 +1,12 @@ +import React from "react" +import ReactDOM from "react-dom/client" + +import { HamburgerNav } from "../nav/HamburgerNav" +import { FlyingGallery } from "./FlyingGallery" + +ReactDOM.createRoot(document.getElementById("root")).render( + + + + +) diff --git a/src/main.jsx b/src/main.jsx index 8e98c53..2fb2e32 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,6 +1,7 @@ import React from "react" import ReactDOM from "react-dom/client" +import { HamburgerNav } from "./nav/HamburgerNav" import { Gallery, GalleryMain, @@ -87,6 +88,7 @@ function App() { ReactDOM.createRoot(document.getElementById("root")).render( + ) diff --git a/src/nav/HamburgerNav.jsx b/src/nav/HamburgerNav.jsx new file mode 100644 index 0000000..7970254 --- /dev/null +++ b/src/nav/HamburgerNav.jsx @@ -0,0 +1,59 @@ +import { useState, useEffect, useRef } from "react" +import styles from "./hamburger-nav.module.css" + +const PAGES = [ + { label: "Basic Gallery", href: "/" }, + { label: "Flying Gallery", href: "/flying-gallery/" }, +] + +export function HamburgerNav() { + const [open, setOpen] = useState(false) + const navRef = useRef(null) + + useEffect(() => { + if (!open) return + const handleKey = (e) => e.key === "Escape" && setOpen(false) + const handleClick = (e) => !navRef.current?.contains(e.target) && setOpen(false) + document.addEventListener("keydown", handleKey) + document.addEventListener("mousedown", handleClick) + return () => { + document.removeEventListener("keydown", handleKey) + document.removeEventListener("mousedown", handleClick) + } + }, [open]) + + return ( + + ) +} diff --git a/src/nav/hamburger-nav.module.css b/src/nav/hamburger-nav.module.css new file mode 100644 index 0000000..bb4597e --- /dev/null +++ b/src/nav/hamburger-nav.module.css @@ -0,0 +1,100 @@ +.nav { + position: fixed; + top: 1.25rem; + right: 1.25rem; + z-index: 100; +} + +.toggle { + display: flex; + flex-direction: column; + justify-content: center; + gap: 5px; + width: 2.5rem; + height: 2.5rem; + padding: 0.5rem; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 6px; + cursor: pointer; + box-shadow: 0 1px 4px rgb(0 0 0 / 8%); +} + +.toggle:focus-visible { + outline: 2px solid #333; + outline-offset: 2px; +} + +.bar { + display: block; + width: 100%; + height: 2px; + background: #333; + border-radius: 2px; + transform-origin: center; + transition: + transform 0.2s ease, + opacity 0.2s ease; +} + +.barTop { + transform: translateY(7px) rotate(45deg); +} + +.barMid { + opacity: 0; + transform: scaleX(0); +} + +.barBot { + transform: translateY(-7px) rotate(-45deg); +} + +.menu { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + min-width: 180px; + margin: 0; + padding: 0.375rem; + list-style: none; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 8px; + box-shadow: 0 4px 16px rgb(0 0 0 / 10%); + opacity: 0; + pointer-events: none; + transform: translateY(-6px) scale(0.97); + transform-origin: top right; + transition: + opacity 0.18s ease, + transform 0.18s ease; +} + +.menuOpen { + opacity: 1; + pointer-events: all; + transform: translateY(0) scale(1); +} + +.link { + display: block; + padding: 0.6rem 0.875rem; + border-radius: 5px; + color: #333; + text-decoration: none; + font-family: system-ui, sans-serif; + font-size: 0.9rem; + white-space: nowrap; + transition: background 0.12s ease; +} + +.link:hover { + background: #f5f5f5; +} + +.link[aria-current="page"] { + font-weight: 600; + color: #000; + background: #f0f0f0; +} diff --git a/vite.config.js b/vite.config.js index ce15f10..1892fc1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -16,6 +16,12 @@ export default defineConfig({ // make sure to externalize deps that shouldn't be bundled // into your library external: ["react", "react-dom"], + input: { + // eslint-disable-next-line no-undef + main: resolve(__dirname, "index.html"), + // eslint-disable-next-line no-undef + flyingGallery: resolve(__dirname, "flying-gallery/index.html"), + }, output: { // Provide global variables to use in the UMD build // for externalized deps