veslx 0.1.42 → 0.1.43

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/README.md CHANGED
@@ -32,7 +32,7 @@ bun install -g veslx
32
32
  veslx serve
33
33
  ```
34
34
 
35
- That's it. Your docs are live at `localhost:3000`.
35
+ That's it. Your docs are live at `localhost:3000` (or the next available port).
36
36
 
37
37
  ---
38
38
 
@@ -77,7 +77,7 @@ cd docs
77
77
  veslx serve
78
78
  ```
79
79
 
80
- Open [localhost:3000](http://localhost:3000). Done.
80
+ Open the URL printed in the console (defaults to [localhost:3000](http://localhost:3000)). Done.
81
81
 
82
82
  ---
83
83
 
@@ -100,7 +100,7 @@ Open [localhost:3000](http://localhost:3000). Done.
100
100
 
101
101
  ## Content Structure
102
102
 
103
- veslx scans your directory for `README.mdx` (posts) and `SLIDES.mdx` (presentations):
103
+ veslx scans your directory for `README.mdx` or `index.mdx` (docs) and `SLIDES.mdx` or `*.slides.mdx` (presentations):
104
104
 
105
105
  ```
106
106
  content/
@@ -115,6 +115,18 @@ content/
115
115
  └── README.mdx
116
116
  ```
117
117
 
118
+ ### Custom Homepage
119
+
120
+ Put `README.mdx` (or `index.mdx`) at the root to replace the default homepage. A common pattern is to use `<PostList />` with `globs` to group content:
121
+
122
+ ```mdx
123
+ # Docs
124
+ <PostList globs={["00-*", "01-*"]} />
125
+
126
+ # Guides
127
+ <PostList globs={["guides/*"]} />
128
+ ```
129
+
118
130
  ### Frontmatter
119
131
 
120
132
  ```yaml
@@ -244,7 +256,7 @@ Each slide becomes one PDF page, centered and optimized for print.
244
256
  ## CLI Commands
245
257
 
246
258
  ```bash
247
- veslx init # Create veslx.config.ts
259
+ veslx init # Create veslx.yaml
248
260
  veslx serve # Start dev server with hot reload
249
261
  veslx build # Build for production → dist/
250
262
  veslx start # Run as background daemon (PM2)
@@ -255,12 +267,14 @@ veslx stop # Stop the daemon
255
267
 
256
268
  ## Configuration
257
269
 
258
- Create `veslx.config.ts` in your content root:
270
+ Create `veslx.yaml` in your content root:
259
271
 
260
- ```typescript
261
- export default {
262
- dir: './content', // Content directory (default: current dir)
263
- }
272
+ ```yaml
273
+ dir: content # Content directory (default: .)
274
+ site:
275
+ name: My Project
276
+ description: Project documentation
277
+ github: myorg/myproject
264
278
  ```
265
279
 
266
280
  Or run `veslx init` to generate it.
@@ -307,7 +321,7 @@ veslx/
307
321
  │ ├── hooks/ # React hooks
308
322
  │ ├── pages/ # Route pages
309
323
  │ └── lib/ # Utilities
310
- └── test-content/ # Example content for testing
324
+ └── content/ # Docs and examples used for the site
311
325
  ```
312
326
 
313
327
  ---
package/bin/lib/serve.ts CHANGED
@@ -33,6 +33,37 @@ async function getDefaultConfig(cwd: string) {
33
33
  };
34
34
  }
35
35
 
36
+ function isAddressInUse(err: unknown) {
37
+ if (!err || typeof err !== 'object') return false;
38
+ const anyErr = err as { code?: string; message?: string };
39
+ if (anyErr.code === 'EADDRINUSE') return true;
40
+ return typeof anyErr.message === 'string' && anyErr.message.includes('already in use');
41
+ }
42
+
43
+ async function listenWithFallback(server: Awaited<ReturnType<typeof createServer>>) {
44
+ const startPort = server.config.server.port ?? 3000;
45
+ const maxAttempts = 50;
46
+
47
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
48
+ const port = startPort + attempt;
49
+ try {
50
+ await server.listen(port);
51
+ const address = server.httpServer?.address();
52
+ const resolvedPort = typeof address === 'object' && address ? address.port : port;
53
+ log.success(`listening :${resolvedPort}`);
54
+ return;
55
+ } catch (err: any) {
56
+ if (!isAddressInUse(err)) {
57
+ throw err;
58
+ }
59
+ log.info(`busy :${port} → :${port + 1}`);
60
+ }
61
+ }
62
+
63
+ log.error(`no available ports from ${startPort} to ${startPort + maxAttempts - 1}`);
64
+ process.exit(1);
65
+ }
66
+
36
67
  export default async function serve(dir?: string) {
37
68
  const cwd = process.cwd()
38
69
 
@@ -82,6 +113,9 @@ export default async function serve(dir?: string) {
82
113
  const server = await createServer({
83
114
  root: veslxRoot,
84
115
  configFile,
116
+ server: {
117
+ strictPort: true,
118
+ },
85
119
  // Cache in user's project so it persists across bunx runs
86
120
  cacheDir: path.join(cwd, 'node_modules/.vite'),
87
121
  plugins: [
@@ -89,15 +123,7 @@ export default async function serve(dir?: string) {
89
123
  ],
90
124
  })
91
125
 
92
- try {
93
- await server.listen()
94
- } catch (err: any) {
95
- if (err?.code === 'EADDRINUSE') {
96
- log.error(`port already in use`)
97
- process.exit(1)
98
- }
99
- throw err
100
- }
126
+ await listenWithFallback(server);
101
127
 
102
128
  const info = server.resolvedUrls
103
129
  if (info?.local[0]) {
@@ -103,7 +103,7 @@ function Gallery({
103
103
  return /* @__PURE__ */ jsxs(Fragment, { children: [
104
104
  /* @__PURE__ */ jsxs("figure", { className: `not-prose relative py-6 md:py-8 ${shouldBreakOut ? isTwo ? "w-[75vw] ml-[calc(-37.5vw+50%)]" : isCompact ? "w-[96vw] ml-[calc(-48vw+50%)]" : "w-[var(--gallery-width)] ml-[calc(-45vw+50%)]" : ""}`, children: [
105
105
  !isSingleWithChildren && !isSingle && /* @__PURE__ */ jsx("div", { className: "max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]", children: /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }) }),
106
- isSingleWithChildren ? /* @__PURE__ */ jsxs("div", { className: `grid items-start gap-10 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] ${childAlign === "left" ? "" : "md:[&>div:first-child]:order-2"}`, children: [
106
+ isSingleWithChildren ? /* @__PURE__ */ jsxs("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"}`, children: [
107
107
  /* @__PURE__ */ jsx("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", children }),
108
108
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
109
109
  /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }),
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/components/gallery/index.tsx"],"sourcesContent":["import { useMemo, useRef, useEffect } 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 filename = path.split('/').pop() || path;\n return filename\n .replace(/\\.(png|jpg|jpeg|gif|svg|webp)$/i, '')\n .replace(/[_-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction getImageUrl(path: string): string {\n return `/raw/${path}`;\n}\n\nexport default function Gallery({\n path,\n globs = null,\n caption,\n captionLabel,\n title,\n subtitle,\n limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | null;\n caption?: string;\n captionLabel?: string;\n title?: string;\n subtitle?: string;\n limit?: number | null;\n page?: number;\n children?: React.ReactNode;\n childAlign?: \"left\" | \"right\";\n}) {\n const { paths, 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 images: LightboxImage[] = useMemo(() =>\n paths.map(p => ({ src: getImageUrl(p), label: getImageLabel(p) })),\n [paths]\n );\n\n if (isLoading) {\n return (\n <figure className=\"not-prose py-6 md:py-8\">\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=\"aspect-square 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\">\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 isSingleWithChildren = images.length === 1 && children;\n // 2-3 images should break out of the content width\n const shouldBreakOut = images.length >= 2;\n\n const imageElement = (index: number, img: LightboxImage, className?: string) => (\n <div\n key={index}\n title={img.label}\n className={`aspect-square 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=\"object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n );\n\n return (\n <>\n <figure className={`not-prose relative py-6 md:py-8 ${shouldBreakOut ? (isTwo ? 'w-[75vw] ml-[calc(-37.5vw+50%)]' : isCompact ? 'w-[96vw] ml-[calc(-48vw+50%)]' : 'w-[var(--gallery-width)] ml-[calc(-45vw+50%)]') : ''}`}>\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-10 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 ) : isSingle ? (\n <div className=\"max-w-[70%] 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 gap-3\">\n {images.map((img, index) => imageElement(index, img, 'flex-1'))}\n </div>\n ) : (\n <div ref={scrollRef} className=\"flex 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-[30%] min-w-[250px] aspect-square 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=\"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":";;;;;;;;;AAaA,SAAS,0BAA0B,KAA0C;AAC3E,YAAU,MAAM;AACd,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI;AAET,UAAM,cAAc,CAAC,MAAkB;AAErC,UAAI,KAAK,IAAI,EAAE,MAAM,KAAK,KAAK,IAAI,EAAE,MAAM,EAAG;AAE9C,YAAM,EAAE,YAAY,aAAa,YAAA,IAAgB;AACjD,YAAM,aAAa,cAAc;AACjC,YAAM,cAAc,aAAa,eAAe,cAAc;AAG9D,UAAK,cAAc,EAAE,SAAS,KAAO,eAAe,EAAE,SAAS,GAAI;AACjE,UAAE,eAAA;AAAA,MACJ;AAAA,IACF;AAEA,OAAG,iBAAiB,SAAS,aAAa,EAAE,SAAS,OAAO;AAC5D,WAAO,MAAM,GAAG,oBAAoB,SAAS,WAAW;AAAA,EAC1D,GAAG,CAAC,GAAG,CAAC;AACV;AAEA,SAAS,cAAc,MAAsB;AAC3C,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,SAAS;AAC1C,SAAO,SACJ,QAAQ,mCAAmC,EAAE,EAC7C,QAAQ,SAAS,GAAG,EACpB,QAAQ,QAAQ,GAAG,EACnB,KAAA;AACL;AAEA,SAAS,YAAY,MAAsB;AACzC,SAAO,QAAQ,IAAI;AACrB;AAEA,SAAwB,QAAQ;AAAA,EAC9B;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,OAAO;AAAA,EACP;AAAA,EACA,aAAa;AACf,GAWG;AACD,QAAM,EAAE,OAAO,WAAW,QAAA,IAAY,iBAAiB;AAAA,IACrD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AAED,QAAM,WAAW,YAAY,MAAM,MAAM;AACzC,QAAM,YAAY,OAAuB,IAAI;AAC7C,4BAA0B,SAAS;AAEnC,QAAM,SAA0B;AAAA,IAAQ,MACtC,MAAM,IAAI,CAAA,OAAM,EAAE,KAAK,YAAY,CAAC,GAAG,OAAO,cAAc,CAAC,IAAI;AAAA,IACjE,CAAC,KAAK;AAAA,EAAA;AAGR,MAAI,WAAW;AACb,+BACG,UAAA,EAAO,WAAU,0BAChB,UAAA,oBAAC,SAAI,WAAU,+DACZ,UAAA,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,MACrB;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,WAAU;AAAA,QAEV,UAAA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,KAAA;AAAA,UAAK;AAAA,QAAA;AAAA,MAC1C;AAAA,MANK;AAAA,IAAA,CAQR,GACH,EAAA,CACF;AAAA,EAEJ;AAEA,MAAI,SAAS;AACX,+BACG,UAAA,EAAO,WAAU,+BAChB,UAAA,qBAAC,OAAA,EAAI,WAAU,6DACb,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAM,WAAU,eAAc,aAAa,KAAK;AAAA,MACjD,oBAAC,QAAA,EAAK,WAAU,+CAA8C,UAAA,YAAA,CAAS;AAAA,IAAA,EAAA,CACzE,EAAA,CACF;AAAA,EAEJ;AAEA,QAAM,WAAW,OAAO,WAAW;AACnC,QAAM,QAAQ,OAAO,WAAW;AAChC,QAAM,YAAY,OAAO,UAAU;AACnC,QAAM,uBAAuB,OAAO,WAAW,KAAK;AAEpD,QAAM,iBAAiB,OAAO,UAAU;AAExC,QAAM,eAAe,CAAC,OAAe,KAAoB,cACvD;AAAA,IAAC;AAAA,IAAA;AAAA,MAEC,OAAO,IAAI;AAAA,MACX,WAAW,6EAA6E,aAAa,EAAE;AAAA,MACvG,SAAS,MAAM,SAAS,KAAK,KAAK;AAAA,MAElC,UAAA;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,KAAK,IAAI;AAAA,UACT,KAAK,IAAI;AAAA,UACT,WAAU;AAAA,QAAA;AAAA,MAAA;AAAA,IACZ;AAAA,IATK;AAAA,EAAA;AAaT,SACE,qBAAA,UAAA,EACE,UAAA;AAAA,IAAA,qBAAC,UAAA,EAAO,WAAW,mCAAmC,iBAAkB,QAAQ,oCAAoC,YAAY,kCAAkC,kDAAmD,EAAE,IACpN,UAAA;AAAA,MAAA,CAAC,wBAAwB,CAAC,YACzB,oBAAC,OAAA,EAAI,WAAU,iEACb,UAAA,oBAAC,cAAA,EAAa,OAAc,SAAA,CAAoB,EAAA,CAClD;AAAA,MAGD,4CACE,OAAA,EAAI,WAAW,sEAAsE,eAAe,SAAS,KAAK,gCAAgC,IACjJ,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAI,WAAU,+KACZ,SAAA,CACH;AAAA,QACA,qBAAC,OAAA,EAAI,WAAU,WACb,UAAA;AAAA,UAAA,oBAAC,cAAA,EAAa,OAAc,SAAA,CAAoB;AAAA,UAC/C,aAAa,GAAG,OAAO,CAAC,CAAC;AAAA,UAC1B,oBAAC,eAAA,EAAc,SAAkB,OAAO,aAAA,CAAc;AAAA,QAAA,EAAA,CACxD;AAAA,MAAA,EAAA,CACF,IACE,WACF,qBAAC,OAAA,EAAI,WAAU,uBACb,UAAA;AAAA,QAAA,oBAAC,cAAA,EAAa,OAAc,SAAA,CAAoB;AAAA,QAC/C,aAAa,GAAG,OAAO,CAAC,CAAC;AAAA,QAC1B,oBAAC,eAAA,EAAc,SAAkB,OAAO,aAAA,CAAc;AAAA,MAAA,EAAA,CACxD,IACE,YACF,oBAAC,OAAA,EAAI,WAAU,cACZ,UAAA,OAAO,IAAI,CAAC,KAAK,UAAU,aAAa,OAAO,KAAK,QAAQ,CAAC,EAAA,CAChE,IAEA,oBAAC,OAAA,EAAI,KAAK,WAAW,WAAU,wDAC5B,UAAA,OAAO,IAAI,CAAC,KAAK,UAChB;AAAA,QAAC;AAAA,QAAA;AAAA,UAEC,OAAO,IAAI;AAAA,UACX,WAAU;AAAA,UACV,SAAS,MAAM,SAAS,KAAK,KAAK;AAAA,UAElC,UAAA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,KAAK,IAAI;AAAA,cACT,KAAK,IAAI;AAAA,cACT,WAAU;AAAA,YAAA;AAAA,UAAA;AAAA,QACZ;AAAA,QATK;AAAA,MAAA,CAWR,GACH;AAAA,MAGD,CAAC,wBAAwB,CAAC,YACzB,oBAAC,OAAA,EAAI,WAAU,iEACb,UAAA,oBAAC,eAAA,EAAc,SAAkB,OAAO,cAAc,EAAA,CACxD;AAAA,IAAA,GAEJ;AAAA,IAEC,SAAS,UAAU,SAAS,kBAAkB,QAC7C;AAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA,eAAe,SAAS;AAAA,QACxB,SAAS,SAAS;AAAA,QAClB,YAAY,SAAS;AAAA,QACrB,QAAQ,SAAS;AAAA,MAAA;AAAA,IAAA;AAAA,EACnB,GAEJ;AAEJ;"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/components/gallery/index.tsx"],"sourcesContent":["import { useMemo, useRef, useEffect } 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 filename = path.split('/').pop() || path;\n return filename\n .replace(/\\.(png|jpg|jpeg|gif|svg|webp)$/i, '')\n .replace(/[_-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction getImageUrl(path: string): string {\n return `/raw/${path}`;\n}\n\nexport default function Gallery({\n path,\n globs = null,\n caption,\n captionLabel,\n title,\n subtitle,\n limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | null;\n caption?: string;\n captionLabel?: string;\n title?: string;\n subtitle?: string;\n limit?: number | null;\n page?: number;\n children?: React.ReactNode;\n childAlign?: \"left\" | \"right\";\n}) {\n const { paths, 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 images: LightboxImage[] = useMemo(() =>\n paths.map(p => ({ src: getImageUrl(p), label: getImageLabel(p) })),\n [paths]\n );\n\n if (isLoading) {\n return (\n <figure className=\"not-prose py-6 md:py-8\">\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=\"aspect-square 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\">\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 isSingleWithChildren = images.length === 1 && children;\n // 2-3 images should break out of the content width\n const shouldBreakOut = images.length >= 2;\n\n const imageElement = (index: number, img: LightboxImage, className?: string) => (\n <div\n key={index}\n title={img.label}\n className={`aspect-square 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=\"object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]\"\n />\n </div>\n );\n\n return (\n <>\n <figure className={`not-prose relative py-6 md:py-8 ${shouldBreakOut ? (isTwo ? 'w-[75vw] ml-[calc(-37.5vw+50%)]' : isCompact ? 'w-[96vw] ml-[calc(-48vw+50%)]' : 'w-[var(--gallery-width)] ml-[calc(-45vw+50%)]') : ''}`}>\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 ) : isSingle ? (\n <div className=\"max-w-[70%] 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 gap-3\">\n {images.map((img, index) => imageElement(index, img, 'flex-1'))}\n </div>\n ) : (\n <div ref={scrollRef} className=\"flex 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-[30%] min-w-[250px] aspect-square 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=\"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,4 +1,4 @@
1
- const version = "0.1.42";
1
+ const version = "0.1.43";
2
2
  const packageJson = {
3
3
  version
4
4
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -150,7 +150,7 @@ export default function Gallery({
150
150
  )}
151
151
 
152
152
  {isSingleWithChildren ? (
153
- <div className={`grid items-start gap-10 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] ${childAlign === 'left' ? '' : 'md:[&>div:first-child]:order-2'}`}>
153
+ <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'}`}>
154
154
  <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">
155
155
  {children}
156
156
  </div>