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.
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useRef, useMemo, useEffect } from "react";
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 ? isGroupedRows ? "" : isScrollRow ? "gallery-breakout w-screen" : "gallery-breakout w-[var(--gallery-width)] max-w-none" : "";
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
- ] }) : isCompact ? /* @__PURE__ */ jsx("div", { className: "flex items-start gap-3", children: images.map((img, index) => imageElement(index, img, "flex-1")) }) : /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "gallery-scroll-row flex items-start gap-3 overflow-x-auto overscroll-x-contain pb-4", children: images.map((img, index) => /* @__PURE__ */ jsx(
145
+ ] }) : /* @__PURE__ */ jsx(
168
146
  "div",
169
147
  {
170
- title: img.label,
171
- className: "flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] overflow-hidden rounded-sm bg-muted/10 cursor-pointer group",
172
- onClick: () => lightbox.open(index),
173
- children: /* @__PURE__ */ jsx(
174
- LoadingImage,
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 PostListItem({ title, description, date, href, linkPath, external, openInNewTab = true, isSlides }) {
7
- const resolvedHref = href || linkPath || "#";
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 (external) {
38
+ if (useRouter) {
35
39
  return /* @__PURE__ */ jsx(
36
- "a",
40
+ Link,
37
41
  {
38
- href: resolvedHref,
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
- Link,
49
+ "a",
48
50
  {
49
- to: resolvedHref,
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?: string;\n /** Alias for href (used in MDX as linkPath) */\n linkPath?: string;\n external?: boolean;\n openInNewTab?: boolean;\n isSlides?: boolean;\n}\n\nexport function PostListItem({ title, description, date, href, linkPath, external, openInNewTab = true, isSlides }: PostListItemProps) {\n const resolvedHref = href || linkPath || '#';\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 (external) {\n return (\n <a\n href={resolvedHref}\n target={openInNewTab ? \"_blank\" : undefined}\n rel={openInNewTab ? \"noopener noreferrer\" : undefined}\n className={className}\n >\n {content}\n </a>\n );\n }\n\n return (\n <Link\n to={resolvedHref}\n className={className}\n >\n {content}\n </Link>\n );\n}\n"],"names":[],"mappings":";;;;;AAiBO,SAAS,aAAa,EAAE,OAAO,aAAa,MAAM,MAAM,UAAU,UAAU,eAAe,MAAM,SAAA,GAA+B;AACrI,QAAM,eAAe,QAAQ,YAAY;AACzC,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,UAAU;AACZ,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,MAAM;AAAA,QACN,QAAQ,eAAe,WAAW;AAAA,QAClC,KAAK,eAAe,wBAAwB;AAAA,QAC5C;AAAA,QAEC,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,IAAI;AAAA,MACJ;AAAA,MAEC,UAAA;AAAA,IAAA;AAAA,EAAA;AAGP;"}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,4 +1,4 @@
1
- import { useMemo, useRef, useEffect, type CSSProperties } from "react";
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
- ? isGroupedRows
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 ref={scrollRef} className="gallery-scroll-row flex items-start gap-3 overflow-x-auto overscroll-x-contain pb-4">
248
- {images.map((img, index) => (
249
- <div
250
- key={index}
251
- title={img.label}
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?: string;
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
- export function PostListItem({ title, description, date, href, linkPath, external, openInNewTab = true, isSlides }: PostListItemProps) {
19
- const resolvedHref = href || linkPath || '#';
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 (external) {
60
+ if (useRouter) {
58
61
  return (
59
- <a
60
- href={resolvedHref}
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
- </a>
67
+ </Link>
67
68
  );
68
69
  }
69
70
 
70
71
  return (
71
- <Link
72
- to={resolvedHref}
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
- </Link>
79
+ </a>
77
80
  );
78
81
  }