veslx 0.1.66 → 0.1.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/components/gallery/index.js +8 -39
- package/dist/client/components/gallery/index.js.map +1 -1
- package/dist/client/components/post-list-item.js +13 -9
- package/dist/client/components/post-list-item.js.map +1 -1
- package/package.json +1 -1
- package/src/components/gallery/index.tsx +8 -59
- package/src/components/post-list-item.tsx +17 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useMemo } from "react";
|
|
3
3
|
import { Image } from "lucide-react";
|
|
4
4
|
import { Lightbox } from "./components/lightbox.js";
|
|
5
5
|
import { useGalleryImages } from "./hooks/use-gallery-images.js";
|
|
@@ -7,23 +7,6 @@ import { useLightbox } from "./hooks/use-lightbox.js";
|
|
|
7
7
|
import { LoadingImage } from "./components/loading-image.js";
|
|
8
8
|
import { FigureHeader } from "./components/figure-header.js";
|
|
9
9
|
import { FigureCaption } from "./components/figure-caption.js";
|
|
10
|
-
function usePreventSwipeNavigation(ref) {
|
|
11
|
-
useEffect(() => {
|
|
12
|
-
const el = ref.current;
|
|
13
|
-
if (!el) return;
|
|
14
|
-
const handleWheel = (e) => {
|
|
15
|
-
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
|
|
16
|
-
const { scrollLeft, scrollWidth, clientWidth } = el;
|
|
17
|
-
const atLeftEdge = scrollLeft <= 0;
|
|
18
|
-
const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;
|
|
19
|
-
if (atLeftEdge && e.deltaX < 0 || atRightEdge && e.deltaX > 0) {
|
|
20
|
-
e.preventDefault();
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
el.addEventListener("wheel", handleWheel, { passive: false });
|
|
24
|
-
return () => el.removeEventListener("wheel", handleWheel);
|
|
25
|
-
}, [ref]);
|
|
26
|
-
}
|
|
27
10
|
function getImageLabel(path) {
|
|
28
11
|
const cleanPath = path.split(/[?#]/)[0];
|
|
29
12
|
const filename = cleanPath.split("/").pop() || cleanPath;
|
|
@@ -64,8 +47,6 @@ function Gallery({
|
|
|
64
47
|
page
|
|
65
48
|
});
|
|
66
49
|
const lightbox = useLightbox(paths.length);
|
|
67
|
-
const scrollRef = useRef(null);
|
|
68
|
-
usePreventSwipeNavigation(scrollRef);
|
|
69
50
|
const galleryWidthMap = {
|
|
70
51
|
sm: "min(80vw, 18rem)",
|
|
71
52
|
md: "min(85vw, 30rem)",
|
|
@@ -102,13 +83,10 @@ function Gallery({
|
|
|
102
83
|
] }) });
|
|
103
84
|
}
|
|
104
85
|
const isSingle = images.length === 1;
|
|
105
|
-
images.length === 2;
|
|
106
|
-
const isCompact = images.length <= 3;
|
|
107
86
|
const isGroupedRows = rowsToRender.length > 1;
|
|
108
|
-
const isScrollRow = images.length > 3;
|
|
109
87
|
const isSingleWithChildren = images.length === 1 && children;
|
|
110
88
|
const shouldBreakOut = images.length >= 2;
|
|
111
|
-
const breakoutClass = shouldBreakOut
|
|
89
|
+
const breakoutClass = shouldBreakOut && !isGroupedRows ? "gallery-breakout w-[var(--gallery-width)] max-w-none" : "";
|
|
112
90
|
const imageElement = (index, img, className) => /* @__PURE__ */ jsx(
|
|
113
91
|
"div",
|
|
114
92
|
{
|
|
@@ -164,23 +142,14 @@ function Gallery({
|
|
|
164
142
|
/* @__PURE__ */ jsx(FigureHeader, { title, subtitle }),
|
|
165
143
|
imageElement(0, images[0]),
|
|
166
144
|
/* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel })
|
|
167
|
-
] }) :
|
|
145
|
+
] }) : /* @__PURE__ */ jsx(
|
|
168
146
|
"div",
|
|
169
147
|
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
{
|
|
176
|
-
src: img.src,
|
|
177
|
-
alt: img.label,
|
|
178
|
-
className: "w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
|
|
179
|
-
}
|
|
180
|
-
)
|
|
181
|
-
},
|
|
182
|
-
index
|
|
183
|
-
)) }),
|
|
148
|
+
className: "grid gap-3",
|
|
149
|
+
style: { gridTemplateColumns: `repeat(${Math.min(images.length, 3)}, minmax(0, 1fr))` },
|
|
150
|
+
children: images.map((img, index) => imageElement(index, img, "w-full"))
|
|
151
|
+
}
|
|
152
|
+
),
|
|
184
153
|
!isSingleWithChildren && !isSingle && /* @__PURE__ */ jsx("div", { className: "max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]", children: /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel }) })
|
|
185
154
|
]
|
|
186
155
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/components/gallery/index.tsx"],"sourcesContent":["import { useMemo, useRef, useEffect, type CSSProperties } from \"react\";\nimport { Image } from \"lucide-react\";\nimport { Lightbox, LightboxImage } from \"@/components/gallery/components/lightbox\";\nimport { useGalleryImages } from \"./hooks/use-gallery-images\";\nimport { useLightbox } from \"./hooks/use-lightbox\";\nimport { LoadingImage } from \"./components/loading-image\";\nimport { FigureHeader } from \"./components/figure-header\";\nimport { FigureCaption } from \"./components/figure-caption\";\n\n/**\n * Hook to prevent horizontal scroll from triggering browser back/forward gestures.\n * Captures wheel events and prevents default when at scroll boundaries.\n */\nfunction usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {\n useEffect(() => {\n const el = ref.current;\n if (!el) return;\n\n const handleWheel = (e: WheelEvent) => {\n // Only handle horizontal scrolling\n if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;\n\n const { scrollLeft, scrollWidth, clientWidth } = el;\n const atLeftEdge = scrollLeft <= 0;\n const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;\n\n // Prevent default if trying to scroll past boundaries\n if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {\n e.preventDefault();\n }\n };\n\n el.addEventListener('wheel', handleWheel, { passive: false });\n return () => el.removeEventListener('wheel', handleWheel);\n }, [ref]);\n}\n\nfunction getImageLabel(path: string): string {\n const cleanPath = path.split(/[?#]/)[0];\n const filename = cleanPath.split('/').pop() || cleanPath;\n return filename\n .replace(/\\.(png|jpg|jpeg|gif|svg|webp)$/i, '')\n .replace(/[_-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction isAbsoluteUrl(path: string): boolean {\n return /^https?:\\/\\//i.test(path) || path.startsWith('//');\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n const trimmedBase = baseUrl.replace(/\\/+$/, '');\n const trimmedPath = path.replace(/^\\/+/, '');\n return `${trimmedBase}/${trimmedPath}`;\n}\n\nfunction getImageUrl(path: string, baseUrl?: string): string {\n if (isAbsoluteUrl(path)) return path;\n if (baseUrl) return joinUrl(baseUrl, path);\n return `/raw/${path}`;\n}\n\nexport default function Gallery({\n path,\n globs = null,\n baseUrl,\n caption,\n captionLabel,\n title,\n subtitle,\n subtitles,\n size = \"lg\",\n limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | string[][] | null;\n baseUrl?: string;\n caption?: string;\n captionLabel?: string;\n title?: string;\n subtitle?: string;\n subtitles?: string[];\n size?: \"sm\" | \"md\" | \"lg\";\n limit?: number | null;\n page?: number;\n children?: React.ReactNode;\n childAlign?: \"left\" | \"right\";\n}) {\n const { paths, rows, isLoading, isEmpty } = useGalleryImages({\n path,\n globs,\n limit,\n page: page,\n });\n\n const lightbox = useLightbox(paths.length);\n const scrollRef = useRef<HTMLDivElement>(null);\n usePreventSwipeNavigation(scrollRef);\n\n const galleryWidthMap: Record<\"sm\" | \"md\" | \"lg\", string> = {\n sm: \"min(80vw, 18rem)\",\n md: \"min(85vw, 30rem)\",\n lg: \"min(90vw, 48rem)\",\n };\n const galleryStyle = { \"--gallery-width\": galleryWidthMap[size] } as CSSProperties;\n\n const rowsToRender = rows.length > 0 ? rows : [paths];\n const flatPaths = rowsToRender.flat();\n const maxRowColumns = rowsToRender.reduce((max, row) => Math.max(max, row.length), 0);\n\n const images: LightboxImage[] = useMemo(\n () => flatPaths.map(p => ({ src: getImageUrl(p, baseUrl), label: getImageLabel(p) })),\n [flatPaths, baseUrl]\n );\n\n if (isLoading) {\n return (\n <figure className=\"not-prose py-6 md:py-8\" style={galleryStyle}>\n <div className=\"grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto\">\n {[...Array(3)].map((_, i) => (\n <div\n key={i}\n className=\"h-40 rounded-sm bg-muted/20 relative overflow-hidden\"\n >\n <div\n className=\"absolute inset-0 bg-gradient-to-r from-transparent via-muted/30 to-transparent animate-shimmer\"\n style={{ animationDelay: `${i * 150}ms` }}\n />\n </div>\n ))}\n </div>\n </figure>\n );\n }\n\n if (isEmpty) {\n return (\n <figure className=\"not-prose py-12 text-center\" style={galleryStyle}>\n <div className=\"inline-flex items-center gap-2.5 text-muted-foreground/40\">\n <Image className=\"h-3.5 w-3.5\" strokeWidth={1.5} />\n <span className=\"font-mono text-xs uppercase tracking-widest\">No images</span>\n </div>\n </figure>\n );\n }\n\n const isSingle = images.length === 1;\n const isTwo = images.length === 2;\n const isCompact = images.length <= 3;\n const isGroupedRows = rowsToRender.length > 1;\n const isScrollRow = images.length > 3;\n const isSingleWithChildren = images.length === 1 && children;\n // 2+ images should break out of the content width\n const shouldBreakOut = images.length >= 2;\n const breakoutClass = shouldBreakOut\n ? isGroupedRows\n ? \"\"\n : isScrollRow\n ? \"gallery-breakout w-screen\"\n : \"gallery-breakout w-[var(--gallery-width)] max-w-none\"\n : \"\";\n\n const imageElement = (index: number, img: LightboxImage, className?: string) => (\n <div\n key={index}\n title={img.label}\n className={`overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}\n onClick={() => lightbox.open(index)}\n >\n <LoadingImage\n src={img.src}\n alt={img.label}\n className=\"w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n );\n\n return (\n <>\n <figure\n className={`not-prose relative py-6 md:py-8 ${breakoutClass}`}\n style={galleryStyle}\n >\n {!isSingleWithChildren && !isSingle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureHeader title={title} subtitle={subtitle} />\n </div>\n )}\n\n {isSingleWithChildren ? (\n <div className={`grid items-start gap-12 md:gap-16 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] ${childAlign === 'left' ? '' : 'md:[&>div:first-child]:order-2'}`}>\n <div className=\"min-w-0 self-center text-sm leading-relaxed text-foreground/90 space-y-3 [&>ul]:space-y-1.5 [&>ul]:list-disc [&>ul]:pl-5 [&>ol]:space-y-1.5 [&>ol]:list-decimal [&>ol]:pl-5\">\n {children}\n </div>\n <div className=\"min-w-0\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n </div>\n ) : isGroupedRows ? (\n <div className=\"space-y-6\">\n {rowsToRender.map((rowPaths, rowIndex) => {\n const rowImages = rowPaths.map((p) => ({\n src: getImageUrl(p, baseUrl),\n label: getImageLabel(p),\n }));\n const offset = rowsToRender.slice(0, rowIndex).reduce((acc, row) => acc + row.length, 0);\n const rowSubtitle = subtitles?.[rowIndex];\n const rowWrapperClass = \"max-w-[var(--gallery-width)] w-full mx-auto\";\n\n return (\n <div key={`${rowIndex}-${rowPaths.join(\"|\")}`}>\n <div className={rowWrapperClass}>\n <div\n className=\"grid gap-3\"\n style={{ gridTemplateColumns: `repeat(${Math.max(1, maxRowColumns)}, minmax(0, 1fr))` }}\n >\n {rowImages.map((img, index) => imageElement(offset + index, img, 'w-full'))}\n </div>\n </div>\n\n {rowSubtitle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureHeader subtitle={rowSubtitle} />\n </div>\n )}\n </div>\n );\n })}\n </div>\n ) : isSingle ? (\n <div className=\"max-w-[var(--gallery-width)] w-full mx-auto\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n ) : isCompact ? (\n <div className=\"flex items-start gap-3\">\n {images.map((img, index) => imageElement(index, img, 'flex-1'))}\n </div>\n ) : (\n <div ref={scrollRef} className=\"gallery-scroll-row flex items-start gap-3 overflow-x-auto overscroll-x-contain pb-4\">\n {images.map((img, index) => (\n <div\n key={index}\n title={img.label}\n className=\"flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] overflow-hidden rounded-sm bg-muted/10 cursor-pointer group\"\n onClick={() => lightbox.open(index)}\n >\n <LoadingImage\n src={img.src}\n alt={img.label}\n className=\"w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n ))}\n </div>\n )}\n\n {!isSingleWithChildren && !isSingle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n )}\n </figure>\n\n {lightbox.isOpen && lightbox.selectedIndex !== null && (\n <Lightbox\n images={images}\n selectedIndex={lightbox.selectedIndex}\n onClose={lightbox.close}\n onPrevious={lightbox.goToPrevious}\n onNext={lightbox.goToNext}\n />\n )}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/components/gallery/index.tsx"],"sourcesContent":["import { useMemo, type CSSProperties } from \"react\";\nimport { Image } from \"lucide-react\";\nimport { Lightbox, LightboxImage } from \"@/components/gallery/components/lightbox\";\nimport { useGalleryImages } from \"./hooks/use-gallery-images\";\nimport { useLightbox } from \"./hooks/use-lightbox\";\nimport { LoadingImage } from \"./components/loading-image\";\nimport { FigureHeader } from \"./components/figure-header\";\nimport { FigureCaption } from \"./components/figure-caption\";\n\nfunction getImageLabel(path: string): string {\n const cleanPath = path.split(/[?#]/)[0];\n const filename = cleanPath.split('/').pop() || cleanPath;\n return filename\n .replace(/\\.(png|jpg|jpeg|gif|svg|webp)$/i, '')\n .replace(/[_-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction isAbsoluteUrl(path: string): boolean {\n return /^https?:\\/\\//i.test(path) || path.startsWith('//');\n}\n\nfunction joinUrl(baseUrl: string, path: string): string {\n const trimmedBase = baseUrl.replace(/\\/+$/, '');\n const trimmedPath = path.replace(/^\\/+/, '');\n return `${trimmedBase}/${trimmedPath}`;\n}\n\nfunction getImageUrl(path: string, baseUrl?: string): string {\n if (isAbsoluteUrl(path)) return path;\n if (baseUrl) return joinUrl(baseUrl, path);\n return `/raw/${path}`;\n}\n\nexport default function Gallery({\n path,\n globs = null,\n baseUrl,\n caption,\n captionLabel,\n title,\n subtitle,\n subtitles,\n size = \"lg\",\n limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | string[][] | null;\n baseUrl?: string;\n caption?: string;\n captionLabel?: string;\n title?: string;\n subtitle?: string;\n subtitles?: string[];\n size?: \"sm\" | \"md\" | \"lg\";\n limit?: number | null;\n page?: number;\n children?: React.ReactNode;\n childAlign?: \"left\" | \"right\";\n}) {\n const { paths, rows, isLoading, isEmpty } = useGalleryImages({\n path,\n globs,\n limit,\n page: page,\n });\n\n const lightbox = useLightbox(paths.length);\n\n const galleryWidthMap: Record<\"sm\" | \"md\" | \"lg\", string> = {\n sm: \"min(80vw, 18rem)\",\n md: \"min(85vw, 30rem)\",\n lg: \"min(90vw, 48rem)\",\n };\n const galleryStyle = { \"--gallery-width\": galleryWidthMap[size] } as CSSProperties;\n\n const rowsToRender = rows.length > 0 ? rows : [paths];\n const flatPaths = rowsToRender.flat();\n const maxRowColumns = rowsToRender.reduce((max, row) => Math.max(max, row.length), 0);\n\n const images: LightboxImage[] = useMemo(\n () => flatPaths.map(p => ({ src: getImageUrl(p, baseUrl), label: getImageLabel(p) })),\n [flatPaths, baseUrl]\n );\n\n if (isLoading) {\n return (\n <figure className=\"not-prose py-6 md:py-8\" style={galleryStyle}>\n <div className=\"grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto\">\n {[...Array(3)].map((_, i) => (\n <div\n key={i}\n className=\"h-40 rounded-sm bg-muted/20 relative overflow-hidden\"\n >\n <div\n className=\"absolute inset-0 bg-gradient-to-r from-transparent via-muted/30 to-transparent animate-shimmer\"\n style={{ animationDelay: `${i * 150}ms` }}\n />\n </div>\n ))}\n </div>\n </figure>\n );\n }\n\n if (isEmpty) {\n return (\n <figure className=\"not-prose py-12 text-center\" style={galleryStyle}>\n <div className=\"inline-flex items-center gap-2.5 text-muted-foreground/40\">\n <Image className=\"h-3.5 w-3.5\" strokeWidth={1.5} />\n <span className=\"font-mono text-xs uppercase tracking-widest\">No images</span>\n </div>\n </figure>\n );\n }\n\n const isSingle = images.length === 1;\n const isGroupedRows = rowsToRender.length > 1;\n const isSingleWithChildren = images.length === 1 && children;\n // 2+ images should break out of the content width\n const shouldBreakOut = images.length >= 2;\n const breakoutClass = shouldBreakOut && !isGroupedRows\n ? \"gallery-breakout w-[var(--gallery-width)] max-w-none\"\n : \"\";\n\n const imageElement = (index: number, img: LightboxImage, className?: string) => (\n <div\n key={index}\n title={img.label}\n className={`overflow-hidden rounded-sm bg-muted/10 cursor-pointer group ${className || ''}`}\n onClick={() => lightbox.open(index)}\n >\n <LoadingImage\n src={img.src}\n alt={img.label}\n className=\"w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n );\n\n return (\n <>\n <figure\n className={`not-prose relative py-6 md:py-8 ${breakoutClass}`}\n style={galleryStyle}\n >\n {!isSingleWithChildren && !isSingle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureHeader title={title} subtitle={subtitle} />\n </div>\n )}\n\n {isSingleWithChildren ? (\n <div className={`grid items-start gap-12 md:gap-16 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] ${childAlign === 'left' ? '' : 'md:[&>div:first-child]:order-2'}`}>\n <div className=\"min-w-0 self-center text-sm leading-relaxed text-foreground/90 space-y-3 [&>ul]:space-y-1.5 [&>ul]:list-disc [&>ul]:pl-5 [&>ol]:space-y-1.5 [&>ol]:list-decimal [&>ol]:pl-5\">\n {children}\n </div>\n <div className=\"min-w-0\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n </div>\n ) : isGroupedRows ? (\n <div className=\"space-y-6\">\n {rowsToRender.map((rowPaths, rowIndex) => {\n const rowImages = rowPaths.map((p) => ({\n src: getImageUrl(p, baseUrl),\n label: getImageLabel(p),\n }));\n const offset = rowsToRender.slice(0, rowIndex).reduce((acc, row) => acc + row.length, 0);\n const rowSubtitle = subtitles?.[rowIndex];\n const rowWrapperClass = \"max-w-[var(--gallery-width)] w-full mx-auto\";\n\n return (\n <div key={`${rowIndex}-${rowPaths.join(\"|\")}`}>\n <div className={rowWrapperClass}>\n <div\n className=\"grid gap-3\"\n style={{ gridTemplateColumns: `repeat(${Math.max(1, maxRowColumns)}, minmax(0, 1fr))` }}\n >\n {rowImages.map((img, index) => imageElement(offset + index, img, 'w-full'))}\n </div>\n </div>\n\n {rowSubtitle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureHeader subtitle={rowSubtitle} />\n </div>\n )}\n </div>\n );\n })}\n </div>\n ) : isSingle ? (\n <div className=\"max-w-[var(--gallery-width)] w-full mx-auto\">\n <FigureHeader title={title} subtitle={subtitle} />\n {imageElement(0, images[0])}\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n ) : (\n <div\n className=\"grid gap-3\"\n style={{ gridTemplateColumns: `repeat(${Math.min(images.length, 3)}, minmax(0, 1fr))` }}\n >\n {images.map((img, index) => imageElement(index, img, 'w-full'))}\n </div>\n )}\n\n {!isSingleWithChildren && !isSingle && (\n <div className=\"max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]\">\n <FigureCaption caption={caption} label={captionLabel} />\n </div>\n )}\n </figure>\n\n {lightbox.isOpen && lightbox.selectedIndex !== null && (\n <Lightbox\n images={images}\n selectedIndex={lightbox.selectedIndex}\n onClose={lightbox.close}\n onPrevious={lightbox.goToPrevious}\n onNext={lightbox.goToNext}\n />\n )}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -3,8 +3,12 @@ import { Link } from "react-router-dom";
|
|
|
3
3
|
import { cn } from "../lib/utils.js";
|
|
4
4
|
import { formatDate } from "../lib/format-date.js";
|
|
5
5
|
import { ArrowRight, Presentation } from "lucide-react";
|
|
6
|
-
function
|
|
7
|
-
|
|
6
|
+
function isRouterLink(url) {
|
|
7
|
+
return url.startsWith("/") && !url.startsWith("//") && !url.startsWith("/raw/");
|
|
8
|
+
}
|
|
9
|
+
function PostListItem({ title, description, date, href, external, openInNewTab, isSlides }) {
|
|
10
|
+
const useRouter = !external && isRouterLink(href) && !href.startsWith("http") && !href.startsWith("mailto:");
|
|
11
|
+
const newTab = openInNewTab ?? href.startsWith("http");
|
|
8
12
|
const className = cn(
|
|
9
13
|
"group block py-3 px-3 -mx-3 rounded-md",
|
|
10
14
|
"transition-colors duration-150"
|
|
@@ -31,22 +35,22 @@ function PostListItem({ title, description, date, href, linkPath, external, open
|
|
|
31
35
|
}
|
|
32
36
|
)
|
|
33
37
|
] });
|
|
34
|
-
if (
|
|
38
|
+
if (useRouter) {
|
|
35
39
|
return /* @__PURE__ */ jsx(
|
|
36
|
-
|
|
40
|
+
Link,
|
|
37
41
|
{
|
|
38
|
-
|
|
39
|
-
target: openInNewTab ? "_blank" : void 0,
|
|
40
|
-
rel: openInNewTab ? "noopener noreferrer" : void 0,
|
|
42
|
+
to: href,
|
|
41
43
|
className,
|
|
42
44
|
children: content
|
|
43
45
|
}
|
|
44
46
|
);
|
|
45
47
|
}
|
|
46
48
|
return /* @__PURE__ */ jsx(
|
|
47
|
-
|
|
49
|
+
"a",
|
|
48
50
|
{
|
|
49
|
-
|
|
51
|
+
href,
|
|
52
|
+
target: newTab ? "_blank" : void 0,
|
|
53
|
+
rel: newTab ? "noopener noreferrer" : void 0,
|
|
50
54
|
className,
|
|
51
55
|
children: content
|
|
52
56
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-list-item.js","sources":["../../../src/components/post-list-item.tsx"],"sourcesContent":["import { Link } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { formatDate } from \"@/lib/format-date\";\nimport { ArrowRight, Presentation } from \"lucide-react\";\n\ninterface PostListItemProps {\n title: string;\n description?: string;\n date?: Date;\n href
|
|
1
|
+
{"version":3,"file":"post-list-item.js","sources":["../../../src/components/post-list-item.tsx"],"sourcesContent":["import { Link } from \"react-router-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { formatDate } from \"@/lib/format-date\";\nimport { ArrowRight, Presentation } from \"lucide-react\";\n\ninterface PostListItemProps {\n title: string;\n description?: string;\n date?: Date;\n href: string;\n external?: boolean;\n openInNewTab?: boolean;\n isSlides?: boolean;\n}\n\nfunction isRouterLink(url: string): boolean {\n return url.startsWith('/') && !url.startsWith('//') && !url.startsWith('/raw/');\n}\n\nexport function PostListItem({ title, description, date, href, external, openInNewTab, isSlides }: PostListItemProps) {\n const useRouter = !external && isRouterLink(href) && !href.startsWith('http') && !href.startsWith('mailto:');\n const newTab = openInNewTab ?? href.startsWith('http');\n const className = cn(\n \"group block py-3 px-3 -mx-3 rounded-md\",\n \"transition-colors duration-150\",\n );\n\n const content = (\n <article className=\"flex items-center gap-4\">\n {/* Main content */}\n <div className=\"flex-1 min-w-0\">\n <div className={cn(\n \"text-sm font-medium text-foreground\",\n \"group-hover:underline\",\n \"flex items-center gap-2\"\n )}>\n <span>{title}</span>\n <ArrowRight className=\"h-3 w-3 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200 text-primary\" />\n </div>\n\n {description && (\n <div className=\"text-sm text-muted-foreground line-clamp-1 mt-0.5\">\n {description}\n </div>\n )}\n </div>\n\n {isSlides && (\n <Presentation className=\"h-3 w-3 text-muted-foreground\" />\n )}\n <time\n dateTime={date?.toISOString()}\n className=\"font-mono text-xs text-muted-foreground tabular-nums w-20 flex-shrink-0\"\n >\n {date && formatDate(date)}\n </time>\n </article>\n );\n\n if (useRouter) {\n return (\n <Link\n to={href}\n className={className}\n >\n {content}\n </Link>\n );\n }\n\n return (\n <a\n href={href}\n target={newTab ? \"_blank\" : undefined}\n rel={newTab ? \"noopener noreferrer\" : undefined}\n className={className}\n >\n {content}\n </a>\n );\n}\n"],"names":[],"mappings":";;;;;AAeA,SAAS,aAAa,KAAsB;AAC1C,SAAO,IAAI,WAAW,GAAG,KAAK,CAAC,IAAI,WAAW,IAAI,KAAK,CAAC,IAAI,WAAW,OAAO;AAChF;AAEO,SAAS,aAAa,EAAE,OAAO,aAAa,MAAM,MAAM,UAAU,cAAc,YAA+B;AACpH,QAAM,YAAY,CAAC,YAAY,aAAa,IAAI,KAAK,CAAC,KAAK,WAAW,MAAM,KAAK,CAAC,KAAK,WAAW,SAAS;AAC3G,QAAM,SAAS,gBAAgB,KAAK,WAAW,MAAM;AACrD,QAAM,YAAY;AAAA,IAChB;AAAA,IACA;AAAA,EAAA;AAGF,QAAM,UACJ,qBAAC,WAAA,EAAQ,WAAU,2BAEjB,UAAA;AAAA,IAAA,qBAAC,OAAA,EAAI,WAAU,kBACb,UAAA;AAAA,MAAA,qBAAC,SAAI,WAAW;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,MAAA,GAEA,UAAA;AAAA,QAAA,oBAAC,UAAM,UAAA,MAAA,CAAM;AAAA,QACb,oBAAC,YAAA,EAAW,WAAU,8HAAA,CAA8H;AAAA,MAAA,GACtJ;AAAA,MAEC,eACC,oBAAC,OAAA,EAAI,WAAU,qDACZ,UAAA,YAAA,CACH;AAAA,IAAA,GAEJ;AAAA,IAEC,YACC,oBAAC,cAAA,EAAa,WAAU,gCAAA,CAAgC;AAAA,IAE1D;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,UAAU,6BAAM;AAAA,QAChB,WAAU;AAAA,QAET,UAAA,QAAQ,WAAW,IAAI;AAAA,MAAA;AAAA,IAAA;AAAA,EAC1B,GACF;AAGF,MAAI,WAAW;AACb,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,IAAI;AAAA,QACJ;AAAA,QAEC,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,QAAQ,SAAS,WAAW;AAAA,MAC5B,KAAK,SAAS,wBAAwB;AAAA,MACtC;AAAA,MAEC,UAAA;AAAA,IAAA;AAAA,EAAA;AAGP;"}
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo,
|
|
1
|
+
import { useMemo, type CSSProperties } from "react";
|
|
2
2
|
import { Image } from "lucide-react";
|
|
3
3
|
import { Lightbox, LightboxImage } from "@/components/gallery/components/lightbox";
|
|
4
4
|
import { useGalleryImages } from "./hooks/use-gallery-images";
|
|
@@ -7,34 +7,6 @@ import { LoadingImage } from "./components/loading-image";
|
|
|
7
7
|
import { FigureHeader } from "./components/figure-header";
|
|
8
8
|
import { FigureCaption } from "./components/figure-caption";
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
* Hook to prevent horizontal scroll from triggering browser back/forward gestures.
|
|
12
|
-
* Captures wheel events and prevents default when at scroll boundaries.
|
|
13
|
-
*/
|
|
14
|
-
function usePreventSwipeNavigation(ref: React.RefObject<HTMLElement | null>) {
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
const el = ref.current;
|
|
17
|
-
if (!el) return;
|
|
18
|
-
|
|
19
|
-
const handleWheel = (e: WheelEvent) => {
|
|
20
|
-
// Only handle horizontal scrolling
|
|
21
|
-
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
|
|
22
|
-
|
|
23
|
-
const { scrollLeft, scrollWidth, clientWidth } = el;
|
|
24
|
-
const atLeftEdge = scrollLeft <= 0;
|
|
25
|
-
const atRightEdge = scrollLeft + clientWidth >= scrollWidth - 1;
|
|
26
|
-
|
|
27
|
-
// Prevent default if trying to scroll past boundaries
|
|
28
|
-
if ((atLeftEdge && e.deltaX < 0) || (atRightEdge && e.deltaX > 0)) {
|
|
29
|
-
e.preventDefault();
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
el.addEventListener('wheel', handleWheel, { passive: false });
|
|
34
|
-
return () => el.removeEventListener('wheel', handleWheel);
|
|
35
|
-
}, [ref]);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
10
|
function getImageLabel(path: string): string {
|
|
39
11
|
const cleanPath = path.split(/[?#]/)[0];
|
|
40
12
|
const filename = cleanPath.split('/').pop() || cleanPath;
|
|
@@ -98,8 +70,6 @@ export default function Gallery({
|
|
|
98
70
|
});
|
|
99
71
|
|
|
100
72
|
const lightbox = useLightbox(paths.length);
|
|
101
|
-
const scrollRef = useRef<HTMLDivElement>(null);
|
|
102
|
-
usePreventSwipeNavigation(scrollRef);
|
|
103
73
|
|
|
104
74
|
const galleryWidthMap: Record<"sm" | "md" | "lg", string> = {
|
|
105
75
|
sm: "min(80vw, 18rem)",
|
|
@@ -149,19 +119,12 @@ export default function Gallery({
|
|
|
149
119
|
}
|
|
150
120
|
|
|
151
121
|
const isSingle = images.length === 1;
|
|
152
|
-
const isTwo = images.length === 2;
|
|
153
|
-
const isCompact = images.length <= 3;
|
|
154
122
|
const isGroupedRows = rowsToRender.length > 1;
|
|
155
|
-
const isScrollRow = images.length > 3;
|
|
156
123
|
const isSingleWithChildren = images.length === 1 && children;
|
|
157
124
|
// 2+ images should break out of the content width
|
|
158
125
|
const shouldBreakOut = images.length >= 2;
|
|
159
|
-
const breakoutClass = shouldBreakOut
|
|
160
|
-
?
|
|
161
|
-
? ""
|
|
162
|
-
: isScrollRow
|
|
163
|
-
? "gallery-breakout w-screen"
|
|
164
|
-
: "gallery-breakout w-[var(--gallery-width)] max-w-none"
|
|
126
|
+
const breakoutClass = shouldBreakOut && !isGroupedRows
|
|
127
|
+
? "gallery-breakout w-[var(--gallery-width)] max-w-none"
|
|
165
128
|
: "";
|
|
166
129
|
|
|
167
130
|
const imageElement = (index: number, img: LightboxImage, className?: string) => (
|
|
@@ -239,26 +202,12 @@ export default function Gallery({
|
|
|
239
202
|
{imageElement(0, images[0])}
|
|
240
203
|
<FigureCaption caption={caption} label={captionLabel} />
|
|
241
204
|
</div>
|
|
242
|
-
) : isCompact ? (
|
|
243
|
-
<div className="flex items-start gap-3">
|
|
244
|
-
{images.map((img, index) => imageElement(index, img, 'flex-1'))}
|
|
245
|
-
</div>
|
|
246
205
|
) : (
|
|
247
|
-
<div
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
className="flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] overflow-hidden rounded-sm bg-muted/10 cursor-pointer group"
|
|
253
|
-
onClick={() => lightbox.open(index)}
|
|
254
|
-
>
|
|
255
|
-
<LoadingImage
|
|
256
|
-
src={img.src}
|
|
257
|
-
alt={img.label}
|
|
258
|
-
className="w-full h-auto object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
|
|
259
|
-
/>
|
|
260
|
-
</div>
|
|
261
|
-
))}
|
|
206
|
+
<div
|
|
207
|
+
className="grid gap-3"
|
|
208
|
+
style={{ gridTemplateColumns: `repeat(${Math.min(images.length, 3)}, minmax(0, 1fr))` }}
|
|
209
|
+
>
|
|
210
|
+
{images.map((img, index) => imageElement(index, img, 'w-full'))}
|
|
262
211
|
</div>
|
|
263
212
|
)}
|
|
264
213
|
|
|
@@ -7,16 +7,19 @@ interface PostListItemProps {
|
|
|
7
7
|
title: string;
|
|
8
8
|
description?: string;
|
|
9
9
|
date?: Date;
|
|
10
|
-
href
|
|
11
|
-
/** Alias for href (used in MDX as linkPath) */
|
|
12
|
-
linkPath?: string;
|
|
10
|
+
href: string;
|
|
13
11
|
external?: boolean;
|
|
14
12
|
openInNewTab?: boolean;
|
|
15
13
|
isSlides?: boolean;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
function isRouterLink(url: string): boolean {
|
|
17
|
+
return url.startsWith('/') && !url.startsWith('//') && !url.startsWith('/raw/');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function PostListItem({ title, description, date, href, external, openInNewTab, isSlides }: PostListItemProps) {
|
|
21
|
+
const useRouter = !external && isRouterLink(href) && !href.startsWith('http') && !href.startsWith('mailto:');
|
|
22
|
+
const newTab = openInNewTab ?? href.startsWith('http');
|
|
20
23
|
const className = cn(
|
|
21
24
|
"group block py-3 px-3 -mx-3 rounded-md",
|
|
22
25
|
"transition-colors duration-150",
|
|
@@ -54,25 +57,25 @@ export function PostListItem({ title, description, date, href, linkPath, externa
|
|
|
54
57
|
</article>
|
|
55
58
|
);
|
|
56
59
|
|
|
57
|
-
if (
|
|
60
|
+
if (useRouter) {
|
|
58
61
|
return (
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
target={openInNewTab ? "_blank" : undefined}
|
|
62
|
-
rel={openInNewTab ? "noopener noreferrer" : undefined}
|
|
62
|
+
<Link
|
|
63
|
+
to={href}
|
|
63
64
|
className={className}
|
|
64
65
|
>
|
|
65
66
|
{content}
|
|
66
|
-
</
|
|
67
|
+
</Link>
|
|
67
68
|
);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
return (
|
|
71
|
-
<
|
|
72
|
-
|
|
72
|
+
<a
|
|
73
|
+
href={href}
|
|
74
|
+
target={newTab ? "_blank" : undefined}
|
|
75
|
+
rel={newTab ? "noopener noreferrer" : undefined}
|
|
73
76
|
className={className}
|
|
74
77
|
>
|
|
75
78
|
{content}
|
|
76
|
-
</
|
|
79
|
+
</a>
|
|
77
80
|
);
|
|
78
81
|
}
|