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.
- package/dist/client/components/gallery/hooks/use-gallery-images.js +28 -17
- package/dist/client/components/gallery/hooks/use-gallery-images.js.map +1 -1
- package/dist/client/components/gallery/index.js +93 -37
- package/dist/client/components/gallery/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/gallery/hooks/use-gallery-images.ts +34 -22
- package/src/components/gallery/index.tsx +84 -11
|
@@ -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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
for (const glob of
|
|
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
|
-
|
|
91
|
+
resolved.push(match);
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
if (
|
|
102
|
-
|
|
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 []
|
|
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":"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"}
|
|
@@ -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
|
-
() =>
|
|
69
|
-
[
|
|
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
|
-
|
|
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(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
/* @__PURE__ */
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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":";;;;;;;;;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,YAAY,KAAK,MAAM,MAAM,EAAE,CAAC;AACtC,QAAM,WAAW,UAAU,MAAM,GAAG,EAAE,SAAS;AAC/C,SAAO,SACJ,QAAQ,mCAAmC,EAAE,EAC7C,QAAQ,SAAS,GAAG,EACpB,QAAQ,QAAQ,GAAG,EACnB,KAAA;AACL;AAEA,SAAS,cAAc,MAAuB;AAC5C,SAAO,gBAAgB,KAAK,IAAI,KAAK,KAAK,WAAW,IAAI;AAC3D;AAEA,SAAS,QAAQ,SAAiB,MAAsB;AACtD,QAAM,cAAc,QAAQ,QAAQ,QAAQ,EAAE;AAC9C,QAAM,cAAc,KAAK,QAAQ,QAAQ,EAAE;AAC3C,SAAO,GAAG,WAAW,IAAI,WAAW;AACtC;AAEA,SAAS,YAAY,MAAc,SAA0B;AAC3D,MAAI,cAAc,IAAI,EAAG,QAAO;AAChC,MAAI,QAAS,QAAO,QAAQ,SAAS,IAAI;AACzC,SAAO,QAAQ,IAAI;AACrB;AAEA,SAAwB,QAAQ;AAAA,EAC9B;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,OAAO;AAAA,EACP;AAAA,EACA,aAAa;AACf,GAYG;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,GAAG,OAAO,GAAG,OAAO,cAAc,CAAC,IAAI;AAAA,IAC1E,CAAC,OAAO,OAAO;AAAA,EAAA;AAGjB,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,2DAA2D,YAAY,2DAA2D,8BAA+B,EAAE,IAChP,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,gFAAgF,eAAe,SAAS,KAAK,gCAAgC,IAC3J,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,2EAC5B,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, 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
|
@@ -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
|
-
|
|
111
|
+
const allImages = collectAllImages(directory).map((img) => img.path);
|
|
112
112
|
|
|
113
|
-
|
|
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
|
-
|
|
115
|
+
const resolved: string[] = [];
|
|
118
116
|
|
|
119
|
-
for (const glob of
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
|
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-[
|
|
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} />
|