veslx 0.1.58 → 0.1.60

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.
@@ -75,40 +75,51 @@ function useGalleryImages({
75
75
  }
76
76
  const directoryPath = resolvedPath || ".";
77
77
  const { directory, error } = useDirectory(directoryPath);
78
- const paths = useMemo(() => {
79
- if (!directory) return [];
80
- let imagePaths;
81
- if (globs && globs.length > 0) {
82
- const allImages = collectAllImages(directory).map((img) => img.path);
78
+ const { paths, rows } = useMemo(() => {
79
+ if (!directory) return { paths: [], rows: [] };
80
+ const allImages = collectAllImages(directory).map((img) => img.path);
81
+ const resolveGlobs = (globList) => {
83
82
  const seen = /* @__PURE__ */ new Set();
84
- imagePaths = [];
85
- for (const glob of globs) {
83
+ const resolved = [];
84
+ for (const glob of globList) {
86
85
  const matches = allImages.filter(
87
86
  (p) => !seen.has(p) && minimatch(p, glob, { matchBase: true })
88
87
  );
89
88
  sortPathsNumerically(matches);
90
89
  for (const match of matches) {
91
90
  seen.add(match);
92
- imagePaths.push(match);
91
+ resolved.push(match);
93
92
  }
94
93
  }
95
- } else {
96
- const imageChildren = directory.children.filter((child) => {
97
- return !!child.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i) && child.type === "file";
98
- });
99
- imagePaths = imageChildren.map((child) => child.path);
100
- }
101
- if (!globs || globs.length === 0) {
102
- sortPathsNumerically(imagePaths);
94
+ let filtered2 = filterPathsByTheme(resolved, resolvedTheme);
95
+ if (limit) {
96
+ filtered2 = filtered2.slice(page * limit, (page + 1) * limit);
97
+ }
98
+ return filtered2;
99
+ };
100
+ if (globs && globs.length > 0) {
101
+ const isGrouped = Array.isArray(globs[0]);
102
+ if (isGrouped) {
103
+ const grouped = globs.map((group) => resolveGlobs(group));
104
+ return { paths: grouped.flat(), rows: grouped };
105
+ }
106
+ const resolved = resolveGlobs(globs);
107
+ return { paths: resolved, rows: [resolved] };
103
108
  }
109
+ const imageChildren = directory.children.filter((child) => {
110
+ return !!child.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i) && child.type === "file";
111
+ });
112
+ const imagePaths = imageChildren.map((child) => child.path);
113
+ sortPathsNumerically(imagePaths);
104
114
  let filtered = filterPathsByTheme(imagePaths, resolvedTheme);
105
115
  if (limit) {
106
116
  filtered = filtered.slice(page * limit, (page + 1) * limit);
107
117
  }
108
- return filtered;
118
+ return { paths: filtered, rows: [filtered] };
109
119
  }, [directory, globs, resolvedTheme, limit, page]);
110
120
  return {
111
121
  paths,
122
+ rows,
112
123
  isLoading: !directory && !error,
113
124
  isEmpty: !!error || directory !== null && paths.length === 0
114
125
  };
@@ -1 +1 @@
1
- {"version":3,"file":"use-gallery-images.js","sources":["../../../../../src/components/gallery/hooks/use-gallery-images.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport { useParams } from \"react-router-dom\";\nimport { useDirectory } from \"../../../../plugin/src/client\";\nimport { FileEntry, DirectoryEntry } from \"../../../../plugin/src/lib\";\nimport { minimatch } from \"minimatch\";\n\n// Recursively collect all image files from a directory tree\nfunction collectAllImages(entry: DirectoryEntry | FileEntry): FileEntry[] {\n if (entry.type === \"file\") {\n if (entry.name.match(/\\.(png|jpg|jpeg|gif|svg|webp)$/i)) {\n return [entry];\n }\n return [];\n }\n // It's a directory - recurse into children\n const images: FileEntry[] = [];\n for (const child of entry.children || []) {\n images.push(...collectAllImages(child));\n }\n return images;\n}\n\nfunction sortPathsNumerically(paths: string[]): void {\n paths.sort((a, b) => {\n const nums = (s: string) => (s.match(/\\d+/g) || []).map(Number);\n const na = nums(a);\n const nb = nums(b);\n const len = Math.max(na.length, nb.length);\n for (let i = 0; i < len; i++) {\n const diff = (na[i] ?? 0) - (nb[i] ?? 0);\n if (diff !== 0) return diff;\n }\n return a.localeCompare(b);\n });\n}\n\nfunction filterPathsByTheme(paths: string[], theme: string | undefined): string[] {\n const pathGroups = new Map<string, { light?: string; dark?: string; original?: string }>();\n\n paths.forEach((path) => {\n const cleanPath = path.split(/[?#]/)[0];\n const themedMatch = cleanPath.match(/^(.*)_(light|dark)\\.(png|jpe?g|gif|svg|webp)$/i);\n if (themedMatch) {\n const baseName = `${themedMatch[1]}.${themedMatch[3]}`;\n const variant = themedMatch[2].toLowerCase() as \"light\" | \"dark\";\n const group = pathGroups.get(baseName) || {};\n group[variant] = path;\n pathGroups.set(baseName, group);\n } else {\n pathGroups.set(path, { original: path });\n }\n });\n\n const filtered: string[] = [];\n pathGroups.forEach((group, baseName) => {\n if (group.original) {\n filtered.push(group.original);\n } else {\n const isDark = theme === \"dark\";\n const preferredPath = isDark ? group.dark : group.light;\n const fallbackPath = isDark ? group.light : group.dark;\n filtered.push(preferredPath || fallbackPath || baseName);\n }\n });\n\n return filtered;\n}\n\n\nexport function useGalleryImages({\n path,\n globs = null,\n limit,\n page = 0,\n}: {\n path?: string;\n globs?: string[] | null;\n limit?: number | null;\n page?: number;\n}) {\n const { resolvedTheme } = useTheme();\n const { \"*\": routePath = \"\" } = useParams();\n\n // Get the current post's directory from the route\n // Route is like \"04-components/README.mdx\" -> \"04-components\"\n // Or \"14-gallery.mdx\" -> \".\" (root level file)\n const currentDir = routePath\n .replace(/\\/?[^/]+\\.mdx?$/i, \"\") // Remove [/]filename.mdx/.md (slash optional for root files)\n .replace(/\\/$/, \"\") // Remove trailing slash\n || \".\";\n\n // Resolve the path relative to current directory\n let resolvedPath = path;\n if (path?.startsWith(\"./\")) {\n // Relative path like \"./images\" -> \"gallery-examples/images\"\n const relativePart = path.slice(2);\n resolvedPath = currentDir === \".\" ? relativePart : `${currentDir}/${relativePart}`;\n } else if (path && !path.startsWith(\"/\") && !path.includes(\"/\")) {\n // Simple name like \"images\" -> \"gallery-examples/images\"\n resolvedPath = currentDir === \".\" ? path : `${currentDir}/${path}`;\n }\n\n // If only globs provided (no path), use root directory\n const directoryPath = resolvedPath || \".\";\n const { directory, error } = useDirectory(directoryPath);\n\n const paths = useMemo(() => {\n if (!directory) return [];\n\n let imagePaths: string[];\n\n if (globs && globs.length > 0) {\n // When globs provided, preserve glob ordering and avoid duplicates.\n const allImages = collectAllImages(directory).map((img) => img.path);\n const seen = new Set<string>();\n imagePaths = [];\n\n for (const glob of globs) {\n const matches = allImages.filter((p) =>\n !seen.has(p) && minimatch(p, glob, { matchBase: true })\n );\n sortPathsNumerically(matches);\n for (const match of matches) {\n seen.add(match);\n imagePaths.push(match);\n }\n }\n } else {\n // No globs - just get images from the specified directory\n const imageChildren = directory.children.filter((child): child is FileEntry => {\n return !!child.name.match(/\\.(png|jpg|jpeg|gif|svg|webp)$/i) && child.type === \"file\";\n });\n imagePaths = imageChildren.map(child => child.path);\n }\n\n if (!globs || globs.length === 0) {\n sortPathsNumerically(imagePaths);\n }\n let filtered = filterPathsByTheme(imagePaths, resolvedTheme);\n\n if (limit) {\n filtered = filtered.slice(page * limit, (page + 1) * limit);\n }\n\n\n return filtered;\n }, [directory, globs, resolvedTheme, limit, page]);\n\n return {\n paths,\n isLoading: !directory && !error,\n isEmpty: !!error || (directory !== null && paths.length === 0),\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-gallery-images.js","sources":["../../../../../src/components/gallery/hooks/use-gallery-images.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport { useParams } from \"react-router-dom\";\nimport { useDirectory } from \"../../../../plugin/src/client\";\nimport { FileEntry, DirectoryEntry } from \"../../../../plugin/src/lib\";\nimport { minimatch } from \"minimatch\";\n\n// Recursively collect all image files from a directory tree\nfunction collectAllImages(entry: DirectoryEntry | FileEntry): FileEntry[] {\n if (entry.type === \"file\") {\n if (entry.name.match(/\\.(png|jpg|jpeg|gif|svg|webp)$/i)) {\n return [entry];\n }\n return [];\n }\n // It's a directory - recurse into children\n const images: FileEntry[] = [];\n for (const child of entry.children || []) {\n images.push(...collectAllImages(child));\n }\n return images;\n}\n\nfunction sortPathsNumerically(paths: string[]): void {\n paths.sort((a, b) => {\n const nums = (s: string) => (s.match(/\\d+/g) || []).map(Number);\n const na = nums(a);\n const nb = nums(b);\n const len = Math.max(na.length, nb.length);\n for (let i = 0; i < len; i++) {\n const diff = (na[i] ?? 0) - (nb[i] ?? 0);\n if (diff !== 0) return diff;\n }\n return a.localeCompare(b);\n });\n}\n\nfunction filterPathsByTheme(paths: string[], theme: string | undefined): string[] {\n const pathGroups = new Map<string, { light?: string; dark?: string; original?: string }>();\n\n paths.forEach((path) => {\n const cleanPath = path.split(/[?#]/)[0];\n const themedMatch = cleanPath.match(/^(.*)_(light|dark)\\.(png|jpe?g|gif|svg|webp)$/i);\n if (themedMatch) {\n const baseName = `${themedMatch[1]}.${themedMatch[3]}`;\n const variant = themedMatch[2].toLowerCase() as \"light\" | \"dark\";\n const group = pathGroups.get(baseName) || {};\n group[variant] = path;\n pathGroups.set(baseName, group);\n } else {\n pathGroups.set(path, { original: path });\n }\n });\n\n const filtered: string[] = [];\n pathGroups.forEach((group, baseName) => {\n if (group.original) {\n filtered.push(group.original);\n } else {\n const isDark = theme === \"dark\";\n const preferredPath = isDark ? group.dark : group.light;\n const fallbackPath = isDark ? group.light : group.dark;\n filtered.push(preferredPath || fallbackPath || baseName);\n }\n });\n\n return filtered;\n}\n\n\nexport function useGalleryImages({\n path,\n globs = null,\n limit,\n page = 0,\n}: {\n path?: string;\n globs?: string[] | string[][] | null;\n limit?: number | null;\n page?: number;\n}) {\n const { resolvedTheme } = useTheme();\n const { \"*\": routePath = \"\" } = useParams();\n\n // Get the current post's directory from the route\n // Route is like \"04-components/README.mdx\" -> \"04-components\"\n // Or \"14-gallery.mdx\" -> \".\" (root level file)\n const currentDir = routePath\n .replace(/\\/?[^/]+\\.mdx?$/i, \"\") // Remove [/]filename.mdx/.md (slash optional for root files)\n .replace(/\\/$/, \"\") // Remove trailing slash\n || \".\";\n\n // Resolve the path relative to current directory\n let resolvedPath = path;\n if (path?.startsWith(\"./\")) {\n // Relative path like \"./images\" -> \"gallery-examples/images\"\n const relativePart = path.slice(2);\n resolvedPath = currentDir === \".\" ? relativePart : `${currentDir}/${relativePart}`;\n } else if (path && !path.startsWith(\"/\") && !path.includes(\"/\")) {\n // Simple name like \"images\" -> \"gallery-examples/images\"\n resolvedPath = currentDir === \".\" ? path : `${currentDir}/${path}`;\n }\n\n // If only globs provided (no path), use root directory\n const directoryPath = resolvedPath || \".\";\n const { directory, error } = useDirectory(directoryPath);\n\n const { paths, rows } = useMemo(() => {\n if (!directory) return { paths: [], rows: [] };\n\n const allImages = collectAllImages(directory).map((img) => img.path);\n\n const resolveGlobs = (globList: string[]) => {\n const seen = new Set<string>();\n const resolved: string[] = [];\n\n for (const glob of globList) {\n const matches = allImages.filter((p) =>\n !seen.has(p) && minimatch(p, glob, { matchBase: true })\n );\n sortPathsNumerically(matches);\n for (const match of matches) {\n seen.add(match);\n resolved.push(match);\n }\n }\n\n let filtered = filterPathsByTheme(resolved, resolvedTheme);\n if (limit) {\n filtered = filtered.slice(page * limit, (page + 1) * limit);\n }\n\n return filtered;\n };\n\n if (globs && globs.length > 0) {\n const isGrouped = Array.isArray(globs[0]);\n if (isGrouped) {\n const grouped = (globs as string[][]).map((group) => resolveGlobs(group));\n return { paths: grouped.flat(), rows: grouped };\n }\n\n const resolved = resolveGlobs(globs as string[]);\n return { paths: resolved, rows: [resolved] };\n }\n\n // No globs - just get images from the specified directory\n const imageChildren = directory.children.filter((child): child is FileEntry => {\n return !!child.name.match(/\\.(png|jpg|jpeg|gif|svg|webp)$/i) && child.type === \"file\";\n });\n const imagePaths = imageChildren.map(child => child.path);\n sortPathsNumerically(imagePaths);\n let filtered = filterPathsByTheme(imagePaths, resolvedTheme);\n if (limit) {\n filtered = filtered.slice(page * limit, (page + 1) * limit);\n }\n\n return { paths: filtered, rows: [filtered] };\n }, [directory, globs, resolvedTheme, limit, page]);\n\n return {\n paths,\n rows,\n isLoading: !directory && !error,\n isEmpty: !!error || (directory !== null && paths.length === 0),\n };\n}\n"],"names":["filtered"],"mappings":";;;;;AAQA,SAAS,iBAAiB,OAAgD;AACxE,MAAI,MAAM,SAAS,QAAQ;AACzB,QAAI,MAAM,KAAK,MAAM,iCAAiC,GAAG;AACvD,aAAO,CAAC,KAAK;AAAA,IACf;AACA,WAAO,CAAA;AAAA,EACT;AAEA,QAAM,SAAsB,CAAA;AAC5B,aAAW,SAAS,MAAM,YAAY,CAAA,GAAI;AACxC,WAAO,KAAK,GAAG,iBAAiB,KAAK,CAAC;AAAA,EACxC;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,OAAuB;AACnD,QAAM,KAAK,CAAC,GAAG,MAAM;AACnB,UAAM,OAAO,CAAC,OAAe,EAAE,MAAM,MAAM,KAAK,CAAA,GAAI,IAAI,MAAM;AAC9D,UAAM,KAAK,KAAK,CAAC;AACjB,UAAM,KAAK,KAAK,CAAC;AACjB,UAAM,MAAM,KAAK,IAAI,GAAG,QAAQ,GAAG,MAAM;AACzC,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,YAAM,QAAQ,GAAG,CAAC,KAAK,MAAM,GAAG,CAAC,KAAK;AACtC,UAAI,SAAS,EAAG,QAAO;AAAA,IACzB;AACA,WAAO,EAAE,cAAc,CAAC;AAAA,EAC1B,CAAC;AACH;AAEA,SAAS,mBAAmB,OAAiB,OAAqC;AAChF,QAAM,iCAAiB,IAAA;AAEvB,QAAM,QAAQ,CAAC,SAAS;AACtB,UAAM,YAAY,KAAK,MAAM,MAAM,EAAE,CAAC;AACtC,UAAM,cAAc,UAAU,MAAM,gDAAgD;AACpF,QAAI,aAAa;AACf,YAAM,WAAW,GAAG,YAAY,CAAC,CAAC,IAAI,YAAY,CAAC,CAAC;AACpD,YAAM,UAAU,YAAY,CAAC,EAAE,YAAA;AAC/B,YAAM,QAAQ,WAAW,IAAI,QAAQ,KAAK,CAAA;AAC1C,YAAM,OAAO,IAAI;AACjB,iBAAW,IAAI,UAAU,KAAK;AAAA,IAChC,OAAO;AACL,iBAAW,IAAI,MAAM,EAAE,UAAU,MAAM;AAAA,IACzC;AAAA,EACF,CAAC;AAED,QAAM,WAAqB,CAAA;AAC3B,aAAW,QAAQ,CAAC,OAAO,aAAa;AACtC,QAAI,MAAM,UAAU;AAClB,eAAS,KAAK,MAAM,QAAQ;AAAA,IAC9B,OAAO;AACL,YAAM,SAAS,UAAU;AACzB,YAAM,gBAAgB,SAAS,MAAM,OAAO,MAAM;AAClD,YAAM,eAAe,SAAS,MAAM,QAAQ,MAAM;AAClD,eAAS,KAAK,iBAAiB,gBAAgB,QAAQ;AAAA,IACzD;AAAA,EACF,CAAC;AAED,SAAO;AACT;AAGO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,OAAO;AACT,GAKG;AACD,QAAM,EAAE,cAAA,IAAkB,SAAA;AAC1B,QAAM,EAAE,KAAK,YAAY,GAAA,IAAO,UAAA;AAKhC,QAAM,aAAa,UAChB,QAAQ,oBAAoB,EAAE,EAC9B,QAAQ,OAAO,EAAE,KACf;AAGL,MAAI,eAAe;AACnB,MAAI,6BAAM,WAAW,OAAO;AAE1B,UAAM,eAAe,KAAK,MAAM,CAAC;AACjC,mBAAe,eAAe,MAAM,eAAe,GAAG,UAAU,IAAI,YAAY;AAAA,EAClF,WAAW,QAAQ,CAAC,KAAK,WAAW,GAAG,KAAK,CAAC,KAAK,SAAS,GAAG,GAAG;AAE/D,mBAAe,eAAe,MAAM,OAAO,GAAG,UAAU,IAAI,IAAI;AAAA,EAClE;AAGA,QAAM,gBAAgB,gBAAgB;AACtC,QAAM,EAAE,WAAW,UAAU,aAAa,aAAa;AAEvD,QAAM,EAAE,OAAO,KAAA,IAAS,QAAQ,MAAM;AACpC,QAAI,CAAC,UAAW,QAAO,EAAE,OAAO,CAAA,GAAI,MAAM,GAAC;AAE3C,UAAM,YAAY,iBAAiB,SAAS,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;AAEnE,UAAM,eAAe,CAAC,aAAuB;AAC3C,YAAM,2BAAW,IAAA;AACjB,YAAM,WAAqB,CAAA;AAE3B,iBAAW,QAAQ,UAAU;AAC3B,cAAM,UAAU,UAAU;AAAA,UAAO,CAAC,MAChC,CAAC,KAAK,IAAI,CAAC,KAAK,UAAU,GAAG,MAAM,EAAE,WAAW,MAAM;AAAA,QAAA;AAExD,6BAAqB,OAAO;AAC5B,mBAAW,SAAS,SAAS;AAC3B,eAAK,IAAI,KAAK;AACd,mBAAS,KAAK,KAAK;AAAA,QACrB;AAAA,MACF;AAEA,UAAIA,YAAW,mBAAmB,UAAU,aAAa;AACzD,UAAI,OAAO;AACTA,oBAAWA,UAAS,MAAM,OAAO,QAAQ,OAAO,KAAK,KAAK;AAAA,MAC5D;AAEA,aAAOA;AAAAA,IACT;AAEA,QAAI,SAAS,MAAM,SAAS,GAAG;AAC7B,YAAM,YAAY,MAAM,QAAQ,MAAM,CAAC,CAAC;AACxC,UAAI,WAAW;AACb,cAAM,UAAW,MAAqB,IAAI,CAAC,UAAU,aAAa,KAAK,CAAC;AACxE,eAAO,EAAE,OAAO,QAAQ,KAAA,GAAQ,MAAM,QAAA;AAAA,MACxC;AAEA,YAAM,WAAW,aAAa,KAAiB;AAC/C,aAAO,EAAE,OAAO,UAAU,MAAM,CAAC,QAAQ,EAAA;AAAA,IAC3C;AAGA,UAAM,gBAAgB,UAAU,SAAS,OAAO,CAAC,UAA8B;AAC7E,aAAO,CAAC,CAAC,MAAM,KAAK,MAAM,iCAAiC,KAAK,MAAM,SAAS;AAAA,IACjF,CAAC;AACD,UAAM,aAAa,cAAc,IAAI,CAAA,UAAS,MAAM,IAAI;AACxD,yBAAqB,UAAU;AAC/B,QAAI,WAAW,mBAAmB,YAAY,aAAa;AAC3D,QAAI,OAAO;AACT,iBAAW,SAAS,MAAM,OAAO,QAAQ,OAAO,KAAK,KAAK;AAAA,IAC5D;AAEA,WAAO,EAAE,OAAO,UAAU,MAAM,CAAC,QAAQ,EAAA;AAAA,EAC3C,GAAG,CAAC,WAAW,OAAO,eAAe,OAAO,IAAI,CAAC;AAEjD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,CAAC,aAAa,CAAC;AAAA,IAC1B,SAAS,CAAC,CAAC,SAAU,cAAc,QAAQ,MAAM,WAAW;AAAA,EAAA;AAEhE;"}
@@ -50,12 +50,14 @@ function Gallery({
50
50
  captionLabel,
51
51
  title,
52
52
  subtitle,
53
+ subtitles,
54
+ size = "lg",
53
55
  limit = null,
54
56
  page = 0,
55
57
  children,
56
58
  childAlign = "right"
57
59
  }) {
58
- const { paths, isLoading, isEmpty } = useGalleryImages({
60
+ const { paths, rows, isLoading, isEmpty } = useGalleryImages({
59
61
  path,
60
62
  globs,
61
63
  limit,
@@ -64,12 +66,21 @@ function Gallery({
64
66
  const lightbox = useLightbox(paths.length);
65
67
  const scrollRef = useRef(null);
66
68
  usePreventSwipeNavigation(scrollRef);
69
+ const galleryWidthMap = {
70
+ sm: "min(80vw, 18rem)",
71
+ md: "min(85vw, 30rem)",
72
+ lg: "min(90vw, 48rem)"
73
+ };
74
+ const galleryStyle = { "--gallery-width": galleryWidthMap[size] };
75
+ const rowsToRender = rows.length > 0 ? rows : [paths];
76
+ const flatPaths = rowsToRender.flat();
77
+ const maxRowColumns = rowsToRender.reduce((max, row) => Math.max(max, row.length), 0);
67
78
  const images = useMemo(
68
- () => paths.map((p) => ({ src: getImageUrl(p, baseUrl), label: getImageLabel(p) })),
69
- [paths, baseUrl]
79
+ () => flatPaths.map((p) => ({ src: getImageUrl(p, baseUrl), label: getImageLabel(p) })),
80
+ [flatPaths, baseUrl]
70
81
  );
71
82
  if (isLoading) {
72
- return /* @__PURE__ */ jsx("figure", { className: "not-prose py-6 md:py-8", children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx(
83
+ return /* @__PURE__ */ jsx("figure", { className: "not-prose py-6 md:py-8", style: galleryStyle, children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx(
73
84
  "div",
74
85
  {
75
86
  className: "aspect-square rounded-sm bg-muted/20 relative overflow-hidden",
@@ -85,16 +96,19 @@ function Gallery({
85
96
  )) }) });
86
97
  }
87
98
  if (isEmpty) {
88
- return /* @__PURE__ */ jsx("figure", { className: "not-prose py-12 text-center", children: /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2.5 text-muted-foreground/40", children: [
99
+ return /* @__PURE__ */ jsx("figure", { className: "not-prose py-12 text-center", style: galleryStyle, children: /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-2.5 text-muted-foreground/40", children: [
89
100
  /* @__PURE__ */ jsx(Image, { className: "h-3.5 w-3.5", strokeWidth: 1.5 }),
90
101
  /* @__PURE__ */ jsx("span", { className: "font-mono text-xs uppercase tracking-widest", children: "No images" })
91
102
  ] }) });
92
103
  }
93
104
  const isSingle = images.length === 1;
94
- const isTwo = images.length === 2;
105
+ images.length === 2;
95
106
  const isCompact = images.length <= 3;
107
+ const isGroupedRows = rowsToRender.length > 1;
108
+ const isScrollRow = images.length > 3;
96
109
  const isSingleWithChildren = images.length === 1 && children;
97
110
  const shouldBreakOut = images.length >= 2;
111
+ const breakoutClass = shouldBreakOut ? isGroupedRows ? "" : isScrollRow ? "gallery-breakout w-screen" : "gallery-breakout w-[var(--gallery-width)] max-w-none" : "";
98
112
  const imageElement = (index, img, className) => /* @__PURE__ */ jsx(
99
113
  "div",
100
114
  {
@@ -113,38 +127,80 @@ function Gallery({
113
127
  index
114
128
  );
115
129
  return /* @__PURE__ */ jsxs(Fragment, { children: [
116
- /* @__PURE__ */ jsxs("figure", { className: `not-prose relative py-6 md:py-8 ${shouldBreakOut ? isTwo ? "gallery-breakout w-[75vw] max-w-[var(--gallery-width)]" : isCompact ? "gallery-breakout w-[96vw] max-w-[var(--gallery-width)]" : "gallery-breakout w-screen" : ""}`, children: [
117
- !isSingleWithChildren && !isSingle && /* @__PURE__ */ jsx("div", { className: "max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]", children: /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }) }),
118
- 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: [
119
- /* @__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 }),
120
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
121
- /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }),
122
- imageElement(0, images[0]),
123
- /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel })
124
- ] })
125
- ] }) : isSingle ? /* @__PURE__ */ jsxs("div", { className: "max-w-[70%] mx-auto", children: [
126
- /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }),
127
- imageElement(0, images[0]),
128
- /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel })
129
- ] }) : isCompact ? /* @__PURE__ */ jsx("div", { className: "flex gap-3", children: images.map((img, index) => imageElement(index, img, "flex-1")) }) : /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "gallery-scroll-row flex gap-3 overflow-x-auto overscroll-x-contain pb-4", children: images.map((img, index) => /* @__PURE__ */ jsx(
130
- "div",
131
- {
132
- title: img.label,
133
- className: "flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group",
134
- onClick: () => lightbox.open(index),
135
- children: /* @__PURE__ */ jsx(
136
- LoadingImage,
130
+ /* @__PURE__ */ jsxs(
131
+ "figure",
132
+ {
133
+ className: `not-prose relative py-6 md:py-8 ${breakoutClass}`,
134
+ style: galleryStyle,
135
+ children: [
136
+ !isSingleWithChildren && !isSingle && /* @__PURE__ */ jsx("div", { className: "max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]", children: /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }) }),
137
+ 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: [
138
+ /* @__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 }),
139
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
140
+ /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }),
141
+ imageElement(0, images[0]),
142
+ /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel })
143
+ ] })
144
+ ] }) : isGroupedRows ? /* @__PURE__ */ jsx("div", { className: "space-y-6", children: rowsToRender.map((rowPaths, rowIndex) => {
145
+ const rowImages = rowPaths.map((p) => ({
146
+ src: getImageUrl(p, baseUrl),
147
+ label: getImageLabel(p)
148
+ }));
149
+ const offset = rowsToRender.slice(0, rowIndex).reduce((acc, row) => acc + row.length, 0);
150
+ const rowSubtitle = subtitles == null ? void 0 : subtitles[rowIndex];
151
+ const rowWrapperClass = "max-w-[var(--gallery-width)] w-full mx-auto";
152
+ const placeholders = Math.max(0, maxRowColumns - rowImages.length);
153
+ const rowCells = [
154
+ ...rowImages.map((img, index) => ({ img, index })),
155
+ ...Array.from({ length: placeholders }, () => ({ img: null, index: -1 }))
156
+ ];
157
+ return /* @__PURE__ */ jsxs("div", { children: [
158
+ /* @__PURE__ */ jsx("div", { className: rowWrapperClass, children: /* @__PURE__ */ jsx(
159
+ "div",
160
+ {
161
+ className: "grid gap-3",
162
+ style: { gridTemplateColumns: `repeat(${Math.max(1, maxRowColumns)}, minmax(0, 1fr))` },
163
+ children: rowCells.map((cell, index) => {
164
+ if (!cell.img) {
165
+ return /* @__PURE__ */ jsx(
166
+ "div",
167
+ {
168
+ className: "aspect-square rounded-sm opacity-0 pointer-events-none"
169
+ },
170
+ `empty-${rowIndex}-${index}`
171
+ );
172
+ }
173
+ return imageElement(offset + cell.index, cell.img, "w-full");
174
+ })
175
+ }
176
+ ) }),
177
+ rowSubtitle && /* @__PURE__ */ jsx("div", { className: "max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]", children: /* @__PURE__ */ jsx(FigureHeader, { subtitle: rowSubtitle }) })
178
+ ] }, `${rowIndex}-${rowPaths.join("|")}`);
179
+ }) }) : isSingle ? /* @__PURE__ */ jsxs("div", { className: "max-w-[var(--gallery-width)] w-full mx-auto", children: [
180
+ /* @__PURE__ */ jsx(FigureHeader, { title, subtitle }),
181
+ imageElement(0, images[0]),
182
+ /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel })
183
+ ] }) : isCompact ? /* @__PURE__ */ jsx("div", { className: "flex gap-3", children: images.map((img, index) => imageElement(index, img, "flex-1")) }) : /* @__PURE__ */ jsx("div", { ref: scrollRef, className: "gallery-scroll-row flex gap-3 overflow-x-auto overscroll-x-contain pb-4", children: images.map((img, index) => /* @__PURE__ */ jsx(
184
+ "div",
137
185
  {
138
- src: img.src,
139
- alt: img.label,
140
- className: "object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
141
- }
142
- )
143
- },
144
- index
145
- )) }),
146
- !isSingleWithChildren && !isSingle && /* @__PURE__ */ jsx("div", { className: "max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]", children: /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel }) })
147
- ] }),
186
+ title: img.label,
187
+ className: "flex-none w-[calc(var(--gallery-width)*0.3)] min-w-[250px] aspect-square overflow-hidden rounded-sm bg-muted/10 cursor-pointer group",
188
+ onClick: () => lightbox.open(index),
189
+ children: /* @__PURE__ */ jsx(
190
+ LoadingImage,
191
+ {
192
+ src: img.src,
193
+ alt: img.label,
194
+ className: "object-contain transition-transform duration-500 ease-out group-hover:scale-[1.02]"
195
+ }
196
+ )
197
+ },
198
+ index
199
+ )) }),
200
+ !isSingleWithChildren && !isSingle && /* @__PURE__ */ jsx("div", { className: "max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]", children: /* @__PURE__ */ jsx(FigureCaption, { caption, label: captionLabel }) })
201
+ ]
202
+ }
203
+ ),
148
204
  lightbox.isOpen && lightbox.selectedIndex !== null && /* @__PURE__ */ jsx(
149
205
  Lightbox,
150
206
  {
@@ -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 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 limit = null,\n page = 0,\n children,\n childAlign = \"right\",\n}: {\n path?: string;\n globs?: string[] | null;\n baseUrl?: string;\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, baseUrl), label: getImageLabel(p) })),\n [paths, baseUrl]\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 ? 'gallery-breakout w-[75vw] max-w-[var(--gallery-width)]' : isCompact ? 'gallery-breakout w-[96vw] max-w-[var(--gallery-width)]' : 'gallery-breakout w-screen') : ''}`}>\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=\"gallery-scroll-row 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-[calc(var(--gallery-width)*0.3)] 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
+ {"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=\"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\" 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={`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\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 const placeholders = Math.max(0, maxRowColumns - rowImages.length);\n const rowCells = [\n ...rowImages.map((img, index) => ({ img, index })),\n ...Array.from({ length: placeholders }, () => ({ img: null, index: -1 })),\n ];\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 {rowCells.map((cell, index) => {\n if (!cell.img) {\n return (\n <div\n key={`empty-${rowIndex}-${index}`}\n className=\"aspect-square rounded-sm opacity-0 pointer-events-none\"\n />\n );\n }\n return imageElement(offset + cell.index, cell.img, 'w-full');\n })}\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 gap-3\">\n {images.map((img, index) => imageElement(index, img, 'flex-1'))}\n </div>\n ) : (\n <div ref={scrollRef} className=\"gallery-scroll-row 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-[calc(var(--gallery-width)*0.3)] 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veslx",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -75,7 +75,7 @@ export function useGalleryImages({
75
75
  page = 0,
76
76
  }: {
77
77
  path?: string;
78
- globs?: string[] | null;
78
+ globs?: string[] | string[][] | null;
79
79
  limit?: number | null;
80
80
  page?: number;
81
81
  }) {
@@ -105,50 +105,62 @@ export function useGalleryImages({
105
105
  const directoryPath = resolvedPath || ".";
106
106
  const { directory, error } = useDirectory(directoryPath);
107
107
 
108
- const paths = useMemo(() => {
109
- if (!directory) return [];
108
+ const { paths, rows } = useMemo(() => {
109
+ if (!directory) return { paths: [], rows: [] };
110
110
 
111
- let imagePaths: string[];
111
+ const allImages = collectAllImages(directory).map((img) => img.path);
112
112
 
113
- if (globs && globs.length > 0) {
114
- // When globs provided, preserve glob ordering and avoid duplicates.
115
- const allImages = collectAllImages(directory).map((img) => img.path);
113
+ const resolveGlobs = (globList: string[]) => {
116
114
  const seen = new Set<string>();
117
- imagePaths = [];
115
+ const resolved: string[] = [];
118
116
 
119
- for (const glob of globs) {
117
+ for (const glob of globList) {
120
118
  const matches = allImages.filter((p) =>
121
119
  !seen.has(p) && minimatch(p, glob, { matchBase: true })
122
120
  );
123
121
  sortPathsNumerically(matches);
124
122
  for (const match of matches) {
125
123
  seen.add(match);
126
- imagePaths.push(match);
124
+ resolved.push(match);
127
125
  }
128
126
  }
129
- } else {
130
- // No globs - just get images from the specified directory
131
- const imageChildren = directory.children.filter((child): child is FileEntry => {
132
- return !!child.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i) && child.type === "file";
133
- });
134
- imagePaths = imageChildren.map(child => child.path);
135
- }
136
127
 
137
- if (!globs || globs.length === 0) {
138
- sortPathsNumerically(imagePaths);
128
+ let filtered = filterPathsByTheme(resolved, resolvedTheme);
129
+ if (limit) {
130
+ filtered = filtered.slice(page * limit, (page + 1) * limit);
131
+ }
132
+
133
+ return filtered;
134
+ };
135
+
136
+ if (globs && globs.length > 0) {
137
+ const isGrouped = Array.isArray(globs[0]);
138
+ if (isGrouped) {
139
+ const grouped = (globs as string[][]).map((group) => resolveGlobs(group));
140
+ return { paths: grouped.flat(), rows: grouped };
141
+ }
142
+
143
+ const resolved = resolveGlobs(globs as string[]);
144
+ return { paths: resolved, rows: [resolved] };
139
145
  }
140
- let filtered = filterPathsByTheme(imagePaths, resolvedTheme);
141
146
 
147
+ // No globs - just get images from the specified directory
148
+ const imageChildren = directory.children.filter((child): child is FileEntry => {
149
+ return !!child.name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i) && child.type === "file";
150
+ });
151
+ const imagePaths = imageChildren.map(child => child.path);
152
+ sortPathsNumerically(imagePaths);
153
+ let filtered = filterPathsByTheme(imagePaths, resolvedTheme);
142
154
  if (limit) {
143
155
  filtered = filtered.slice(page * limit, (page + 1) * limit);
144
156
  }
145
157
 
146
-
147
- return filtered;
158
+ return { paths: filtered, rows: [filtered] };
148
159
  }, [directory, globs, resolvedTheme, limit, page]);
149
160
 
150
161
  return {
151
162
  paths,
163
+ rows,
152
164
  isLoading: !directory && !error,
153
165
  isEmpty: !!error || (directory !== null && paths.length === 0),
154
166
  };
@@ -1,4 +1,4 @@
1
- import { useMemo, useRef, useEffect } from "react";
1
+ import { useMemo, useRef, useEffect, 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";
@@ -69,24 +69,28 @@ export default function Gallery({
69
69
  captionLabel,
70
70
  title,
71
71
  subtitle,
72
+ subtitles,
73
+ size = "lg",
72
74
  limit = null,
73
75
  page = 0,
74
76
  children,
75
77
  childAlign = "right",
76
78
  }: {
77
79
  path?: string;
78
- globs?: string[] | null;
80
+ globs?: string[] | string[][] | null;
79
81
  baseUrl?: string;
80
82
  caption?: string;
81
83
  captionLabel?: string;
82
84
  title?: string;
83
85
  subtitle?: string;
86
+ subtitles?: string[];
87
+ size?: "sm" | "md" | "lg";
84
88
  limit?: number | null;
85
89
  page?: number;
86
90
  children?: React.ReactNode;
87
91
  childAlign?: "left" | "right";
88
92
  }) {
89
- const { paths, isLoading, isEmpty } = useGalleryImages({
93
+ const { paths, rows, isLoading, isEmpty } = useGalleryImages({
90
94
  path,
91
95
  globs,
92
96
  limit,
@@ -97,14 +101,25 @@ export default function Gallery({
97
101
  const scrollRef = useRef<HTMLDivElement>(null);
98
102
  usePreventSwipeNavigation(scrollRef);
99
103
 
100
- const images: LightboxImage[] = useMemo(() =>
101
- paths.map(p => ({ src: getImageUrl(p, baseUrl), label: getImageLabel(p) })),
102
- [paths, baseUrl]
104
+ const galleryWidthMap: Record<"sm" | "md" | "lg", string> = {
105
+ sm: "min(80vw, 18rem)",
106
+ md: "min(85vw, 30rem)",
107
+ lg: "min(90vw, 48rem)",
108
+ };
109
+ const galleryStyle = { "--gallery-width": galleryWidthMap[size] } as CSSProperties;
110
+
111
+ const rowsToRender = rows.length > 0 ? rows : [paths];
112
+ const flatPaths = rowsToRender.flat();
113
+ const maxRowColumns = rowsToRender.reduce((max, row) => Math.max(max, row.length), 0);
114
+
115
+ const images: LightboxImage[] = useMemo(
116
+ () => flatPaths.map(p => ({ src: getImageUrl(p, baseUrl), label: getImageLabel(p) })),
117
+ [flatPaths, baseUrl]
103
118
  );
104
119
 
105
120
  if (isLoading) {
106
121
  return (
107
- <figure className="not-prose py-6 md:py-8">
122
+ <figure className="not-prose py-6 md:py-8" style={galleryStyle}>
108
123
  <div className="grid grid-cols-3 gap-3 max-w-[var(--gallery-width)] mx-auto">
109
124
  {[...Array(3)].map((_, i) => (
110
125
  <div
@@ -124,7 +139,7 @@ export default function Gallery({
124
139
 
125
140
  if (isEmpty) {
126
141
  return (
127
- <figure className="not-prose py-12 text-center">
142
+ <figure className="not-prose py-12 text-center" style={galleryStyle}>
128
143
  <div className="inline-flex items-center gap-2.5 text-muted-foreground/40">
129
144
  <Image className="h-3.5 w-3.5" strokeWidth={1.5} />
130
145
  <span className="font-mono text-xs uppercase tracking-widest">No images</span>
@@ -136,9 +151,18 @@ export default function Gallery({
136
151
  const isSingle = images.length === 1;
137
152
  const isTwo = images.length === 2;
138
153
  const isCompact = images.length <= 3;
154
+ const isGroupedRows = rowsToRender.length > 1;
155
+ const isScrollRow = images.length > 3;
139
156
  const isSingleWithChildren = images.length === 1 && children;
140
- // 2-3 images should break out of the content width
157
+ // 2+ images should break out of the content width
141
158
  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"
165
+ : "";
142
166
 
143
167
  const imageElement = (index: number, img: LightboxImage, className?: string) => (
144
168
  <div
@@ -157,7 +181,10 @@ export default function Gallery({
157
181
 
158
182
  return (
159
183
  <>
160
- <figure className={`not-prose relative py-6 md:py-8 ${shouldBreakOut ? (isTwo ? 'gallery-breakout w-[75vw] max-w-[var(--gallery-width)]' : isCompact ? 'gallery-breakout w-[96vw] max-w-[var(--gallery-width)]' : 'gallery-breakout w-screen') : ''}`}>
184
+ <figure
185
+ className={`not-prose relative py-6 md:py-8 ${breakoutClass}`}
186
+ style={galleryStyle}
187
+ >
161
188
  {!isSingleWithChildren && !isSingle && (
162
189
  <div className="max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]">
163
190
  <FigureHeader title={title} subtitle={subtitle} />
@@ -175,8 +202,54 @@ export default function Gallery({
175
202
  <FigureCaption caption={caption} label={captionLabel} />
176
203
  </div>
177
204
  </div>
205
+ ) : isGroupedRows ? (
206
+ <div className="space-y-6">
207
+ {rowsToRender.map((rowPaths, rowIndex) => {
208
+ const rowImages = rowPaths.map((p) => ({
209
+ src: getImageUrl(p, baseUrl),
210
+ label: getImageLabel(p),
211
+ }));
212
+ const offset = rowsToRender.slice(0, rowIndex).reduce((acc, row) => acc + row.length, 0);
213
+ const rowSubtitle = subtitles?.[rowIndex];
214
+ const rowWrapperClass = "max-w-[var(--gallery-width)] w-full mx-auto";
215
+ const placeholders = Math.max(0, maxRowColumns - rowImages.length);
216
+ const rowCells = [
217
+ ...rowImages.map((img, index) => ({ img, index })),
218
+ ...Array.from({ length: placeholders }, () => ({ img: null, index: -1 })),
219
+ ];
220
+
221
+ return (
222
+ <div key={`${rowIndex}-${rowPaths.join("|")}`}>
223
+ <div className={rowWrapperClass}>
224
+ <div
225
+ className="grid gap-3"
226
+ style={{ gridTemplateColumns: `repeat(${Math.max(1, maxRowColumns)}, minmax(0, 1fr))` }}
227
+ >
228
+ {rowCells.map((cell, index) => {
229
+ if (!cell.img) {
230
+ return (
231
+ <div
232
+ key={`empty-${rowIndex}-${index}`}
233
+ className="aspect-square rounded-sm opacity-0 pointer-events-none"
234
+ />
235
+ );
236
+ }
237
+ return imageElement(offset + cell.index, cell.img, 'w-full');
238
+ })}
239
+ </div>
240
+ </div>
241
+
242
+ {rowSubtitle && (
243
+ <div className="max-w-[var(--content-width)] mx-auto px-[var(--page-padding)]">
244
+ <FigureHeader subtitle={rowSubtitle} />
245
+ </div>
246
+ )}
247
+ </div>
248
+ );
249
+ })}
250
+ </div>
178
251
  ) : isSingle ? (
179
- <div className="max-w-[70%] mx-auto">
252
+ <div className="max-w-[var(--gallery-width)] w-full mx-auto">
180
253
  <FigureHeader title={title} subtitle={subtitle} />
181
254
  {imageElement(0, images[0])}
182
255
  <FigureCaption caption={caption} label={captionLabel} />