zone5 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/cli/index.js +9 -1
- package/dist/components/Zone5.svelte +23 -5
- package/dist/components/Zone5.svelte.d.ts +5 -3
- package/dist/components/Zone5Justified.svelte +128 -0
- package/dist/components/Zone5Justified.svelte.d.ts +10 -0
- package/dist/components/constants.d.ts +13 -0
- package/dist/components/constants.js +13 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +2 -0
- package/dist/gallery/config.d.ts +32 -0
- package/dist/gallery/config.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/remark.d.ts +11 -1
- package/dist/remark.js +75 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,64 @@ widths = [400, 800, 1200, 1600, 2400]
|
|
|
90
90
|
<Zone5 images={[image1, image2]} />
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Layout Modes
|
|
94
|
+
|
|
95
|
+
Zone5 supports three layout modes:
|
|
96
|
+
|
|
97
|
+
#### Justified Mode (Default)
|
|
98
|
+
|
|
99
|
+
Row-based layout (like Flickr/Google Photos). Each row fills the full width while preserving aspect ratios. Panoramic images (aspect ratio > 3) automatically get their own row.
|
|
100
|
+
|
|
101
|
+
```svelte
|
|
102
|
+
<Zone5 images={images} mode="justified" />
|
|
103
|
+
|
|
104
|
+
<!-- With custom row height and gap -->
|
|
105
|
+
<Zone5
|
|
106
|
+
images={images}
|
|
107
|
+
mode="justified"
|
|
108
|
+
targetRowHeight={250}
|
|
109
|
+
gap={12}
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Wall Mode
|
|
114
|
+
|
|
115
|
+
Fixed-height grid layout. Images are cropped to fill their containers.
|
|
116
|
+
|
|
117
|
+
```svelte
|
|
118
|
+
<Zone5 images={images} mode="wall" />
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Waterfall Mode
|
|
122
|
+
|
|
123
|
+
Column-based masonry layout. Images are distributed across columns and maintain their aspect ratios.
|
|
124
|
+
|
|
125
|
+
```svelte
|
|
126
|
+
<Zone5 images={images} mode="waterfall" />
|
|
127
|
+
|
|
128
|
+
<!-- With custom column breakpoints -->
|
|
129
|
+
<Zone5
|
|
130
|
+
images={images}
|
|
131
|
+
mode="waterfall"
|
|
132
|
+
columnBreakpoints={{ 640: 2, 1024: 4 }}
|
|
133
|
+
/>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Setting Mode in Markdown
|
|
137
|
+
|
|
138
|
+
Use the `zone5mode` frontmatter property:
|
|
139
|
+
|
|
140
|
+
```markdown
|
|
141
|
+
---
|
|
142
|
+
zone5mode: justified
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+

|
|
146
|
+

|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Valid values: `wall`, `waterfall`, `justified`
|
|
150
|
+
|
|
93
151
|
### Using in Markdown/MDX
|
|
94
152
|
|
|
95
153
|
Add the remark plugin to your `svelte.config.js`:
|
package/dist/cli/index.js
CHANGED
|
@@ -8198,7 +8198,15 @@ async function vh(t, i, a, n) {
|
|
|
8198
8198
|
}
|
|
8199
8199
|
}
|
|
8200
8200
|
async function wh(t, i) {
|
|
8201
|
-
const n =
|
|
8201
|
+
const n = `---
|
|
8202
|
+
# Zone5 Gallery Configuration
|
|
8203
|
+
# https://cwygoda.github.io/zone5/docs/reference/remark-plugin-api#frontmatter-options
|
|
8204
|
+
|
|
8205
|
+
# Gallery layout mode: "justified" (default) | "wall" | "waterfall"
|
|
8206
|
+
# zone5mode: justified
|
|
8207
|
+
---
|
|
8208
|
+
|
|
8209
|
+
# Photo Gallery
|
|
8202
8210
|
|
|
8203
8211
|
${i.map(({ relativePath: s }) => {
|
|
8204
8212
|
const l = Rt(s);
|
|
@@ -5,21 +5,32 @@
|
|
|
5
5
|
import Zone5Single from './Zone5Single.svelte';
|
|
6
6
|
import Zone5Wall from './Zone5Wall.svelte';
|
|
7
7
|
import Zone5Waterfall from './Zone5Waterfall.svelte';
|
|
8
|
-
import
|
|
8
|
+
import Zone5Justified from './Zone5Justified.svelte';
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_COLUMN_BREAKPOINTS,
|
|
11
|
+
DEFAULT_TARGET_ROW_HEIGHT,
|
|
12
|
+
DEFAULT_GAP,
|
|
13
|
+
} from './constants';
|
|
9
14
|
import type { ImageData } from './types';
|
|
10
15
|
|
|
11
16
|
interface Props {
|
|
12
|
-
columnBreakpoints?: { [key: number]: number };
|
|
13
17
|
images: ImageData[];
|
|
14
|
-
mode?: 'wall' | 'waterfall';
|
|
18
|
+
mode?: 'wall' | 'waterfall' | 'justified';
|
|
15
19
|
nocaption?: boolean;
|
|
20
|
+
// Waterfall mode options
|
|
21
|
+
columnBreakpoints?: { [key: number]: number };
|
|
22
|
+
// Justified mode options
|
|
23
|
+
targetRowHeight?: number;
|
|
24
|
+
gap?: number;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
let {
|
|
19
|
-
columnBreakpoints = DEFAULT_COLUMN_BREAKPOINTS,
|
|
20
28
|
images,
|
|
21
|
-
mode = '
|
|
29
|
+
mode = 'justified',
|
|
22
30
|
nocaption = false,
|
|
31
|
+
columnBreakpoints = DEFAULT_COLUMN_BREAKPOINTS,
|
|
32
|
+
targetRowHeight = DEFAULT_TARGET_ROW_HEIGHT,
|
|
33
|
+
gap = DEFAULT_GAP,
|
|
23
34
|
}: Props = $props();
|
|
24
35
|
|
|
25
36
|
const imageStore = useImageRegistry();
|
|
@@ -71,4 +82,11 @@
|
|
|
71
82
|
{images}
|
|
72
83
|
onImageClick={imageStore ? handleImageClick : undefined}
|
|
73
84
|
/>
|
|
85
|
+
{:else if mode === 'justified'}
|
|
86
|
+
<Zone5Justified
|
|
87
|
+
{images}
|
|
88
|
+
{targetRowHeight}
|
|
89
|
+
{gap}
|
|
90
|
+
onImageClick={imageStore ? handleImageClick : undefined}
|
|
91
|
+
/>
|
|
74
92
|
{/if}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { ImageData } from './types';
|
|
2
2
|
interface Props {
|
|
3
|
+
images: ImageData[];
|
|
4
|
+
mode?: 'wall' | 'waterfall' | 'justified';
|
|
5
|
+
nocaption?: boolean;
|
|
3
6
|
columnBreakpoints?: {
|
|
4
7
|
[key: number]: number;
|
|
5
8
|
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
nocaption?: boolean;
|
|
9
|
+
targetRowHeight?: number;
|
|
10
|
+
gap?: number;
|
|
9
11
|
}
|
|
10
12
|
declare const Zone5: import("svelte").Component<Props, {}, "">;
|
|
11
13
|
type Zone5 = ReturnType<typeof Zone5>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Img from './Zone5Img.svelte';
|
|
3
|
+
import { DEFAULT_TARGET_ROW_HEIGHT, DEFAULT_GAP, PANORAMA_THRESHOLD } from './constants';
|
|
4
|
+
import type { ImageData } from './types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
images: ImageData[];
|
|
8
|
+
onImageClick?: (index: number) => void;
|
|
9
|
+
targetRowHeight?: number;
|
|
10
|
+
gap?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
images,
|
|
15
|
+
onImageClick,
|
|
16
|
+
targetRowHeight = DEFAULT_TARGET_ROW_HEIGHT,
|
|
17
|
+
gap = DEFAULT_GAP,
|
|
18
|
+
}: Props = $props();
|
|
19
|
+
|
|
20
|
+
let containerWidth = $state(0);
|
|
21
|
+
|
|
22
|
+
interface JustifiedRow {
|
|
23
|
+
images: { image: ImageData; idx: number }[];
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate the row height that makes all images fit the container width exactly.
|
|
29
|
+
* Formula: h = (containerWidth - totalGaps) / sum(aspectRatios)
|
|
30
|
+
*/
|
|
31
|
+
function calculateRowHeight(
|
|
32
|
+
row: { image: ImageData; idx: number }[],
|
|
33
|
+
width: number,
|
|
34
|
+
gapSize: number,
|
|
35
|
+
): number {
|
|
36
|
+
const aspectRatioSum = row.reduce((sum, item) => sum + item.image.properties.aspectRatio, 0);
|
|
37
|
+
const totalGapWidth = (row.length - 1) * gapSize;
|
|
38
|
+
const availableWidth = width - totalGapWidth;
|
|
39
|
+
return availableWidth / aspectRatioSum;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Calculate justified rows using a greedy algorithm.
|
|
44
|
+
* Images are added to rows until the row height drops to or below the target height.
|
|
45
|
+
*/
|
|
46
|
+
let rows = $derived.by((): JustifiedRow[] => {
|
|
47
|
+
if (images.length === 0) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Use containerWidth if available, otherwise fall back to a default for SSR/testing
|
|
52
|
+
const effectiveWidth = containerWidth > 0 ? containerWidth : 1200;
|
|
53
|
+
|
|
54
|
+
const result: JustifiedRow[] = [];
|
|
55
|
+
let currentRow: { image: ImageData; idx: number }[] = [];
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < images.length; i++) {
|
|
58
|
+
const image = images[i];
|
|
59
|
+
const aspectRatio = image.properties.aspectRatio;
|
|
60
|
+
|
|
61
|
+
// Handle panoramas: if we have images in the current row, finalize it first
|
|
62
|
+
if (aspectRatio > PANORAMA_THRESHOLD && currentRow.length > 0) {
|
|
63
|
+
result.push({
|
|
64
|
+
images: [...currentRow],
|
|
65
|
+
height: calculateRowHeight(currentRow, effectiveWidth, gap),
|
|
66
|
+
});
|
|
67
|
+
currentRow = [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
currentRow.push({ image, idx: i });
|
|
71
|
+
|
|
72
|
+
// Panorama gets its own row with height capped at target
|
|
73
|
+
if (aspectRatio > PANORAMA_THRESHOLD) {
|
|
74
|
+
const panoramaHeight = Math.min(effectiveWidth / aspectRatio, targetRowHeight);
|
|
75
|
+
result.push({
|
|
76
|
+
images: [...currentRow],
|
|
77
|
+
height: panoramaHeight,
|
|
78
|
+
});
|
|
79
|
+
currentRow = [];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rowHeight = calculateRowHeight(currentRow, effectiveWidth, gap);
|
|
84
|
+
|
|
85
|
+
// If row height is at or below target, finalize this row
|
|
86
|
+
if (rowHeight <= targetRowHeight) {
|
|
87
|
+
result.push({
|
|
88
|
+
images: [...currentRow],
|
|
89
|
+
height: rowHeight,
|
|
90
|
+
});
|
|
91
|
+
currentRow = [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle last row: left-align by capping height at target
|
|
96
|
+
if (currentRow.length > 0) {
|
|
97
|
+
const calculatedHeight = calculateRowHeight(currentRow, effectiveWidth, gap);
|
|
98
|
+
result.push({
|
|
99
|
+
images: currentRow,
|
|
100
|
+
height: Math.min(calculatedHeight, targetRowHeight),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
});
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<div
|
|
109
|
+
class="zone5-justified flex flex-col"
|
|
110
|
+
bind:clientWidth={containerWidth}
|
|
111
|
+
role="list"
|
|
112
|
+
style:gap="{gap}px"
|
|
113
|
+
>
|
|
114
|
+
{#each rows as row, rowIdx (rowIdx)}
|
|
115
|
+
<div class="zone5-justified-row flex" style:height="{row.height}px" style:gap="{gap}px">
|
|
116
|
+
{#each row.images as { image, idx } (idx)}
|
|
117
|
+
<div
|
|
118
|
+
class="zone5-justified-item shrink-0"
|
|
119
|
+
role="listitem"
|
|
120
|
+
style:width="{row.height * image.properties.aspectRatio}px"
|
|
121
|
+
style:height="{row.height}px"
|
|
122
|
+
>
|
|
123
|
+
<Img {image} cover class="w-full h-full" onclick={onImageClick ? () => onImageClick(idx) : undefined} />
|
|
124
|
+
</div>
|
|
125
|
+
{/each}
|
|
126
|
+
</div>
|
|
127
|
+
{/each}
|
|
128
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ImageData } from './types';
|
|
2
|
+
interface Props {
|
|
3
|
+
images: ImageData[];
|
|
4
|
+
onImageClick?: (index: number) => void;
|
|
5
|
+
targetRowHeight?: number;
|
|
6
|
+
gap?: number;
|
|
7
|
+
}
|
|
8
|
+
declare const Zone5Justified: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type Zone5Justified = ReturnType<typeof Zone5Justified>;
|
|
10
|
+
export default Zone5Justified;
|
|
@@ -7,6 +7,19 @@ export declare const DEFAULT_COLUMN_BREAKPOINTS: {
|
|
|
7
7
|
readonly 768: 3;
|
|
8
8
|
readonly 1024: 4;
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Default target row height for justified mode in pixels.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_TARGET_ROW_HEIGHT = 300;
|
|
14
|
+
/**
|
|
15
|
+
* Default gap between images in justified mode in pixels.
|
|
16
|
+
*/
|
|
17
|
+
export declare const DEFAULT_GAP = 8;
|
|
18
|
+
/**
|
|
19
|
+
* Aspect ratio threshold for panoramic images.
|
|
20
|
+
* Images with aspect ratio greater than this get their own row in justified mode.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PANORAMA_THRESHOLD = 3;
|
|
10
23
|
/**
|
|
11
24
|
* Height for single image in wall mode (Tailwind class)
|
|
12
25
|
*/
|
|
@@ -7,6 +7,19 @@ export const DEFAULT_COLUMN_BREAKPOINTS = {
|
|
|
7
7
|
768: 3, // md: 3 columns
|
|
8
8
|
1024: 4, // lg: 4 columns
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Default target row height for justified mode in pixels.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_TARGET_ROW_HEIGHT = 300;
|
|
14
|
+
/**
|
|
15
|
+
* Default gap between images in justified mode in pixels.
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_GAP = 8;
|
|
18
|
+
/**
|
|
19
|
+
* Aspect ratio threshold for panoramic images.
|
|
20
|
+
* Images with aspect ratio greater than this get their own row in justified mode.
|
|
21
|
+
*/
|
|
22
|
+
export const PANORAMA_THRESHOLD = 3.0;
|
|
10
23
|
/**
|
|
11
24
|
* Height for single image in wall mode (Tailwind class)
|
|
12
25
|
*/
|
package/dist/config.d.ts
CHANGED
|
@@ -18,6 +18,31 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
18
18
|
resize_gamma: z.ZodOptional<z.ZodNumber>;
|
|
19
19
|
variants: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
|
|
20
20
|
}, z.core.$strip>>>;
|
|
21
|
+
gallery: z.ZodPrefault<z.ZodPipe<z.ZodPrefault<z.ZodObject<{
|
|
22
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
wall: "wall";
|
|
24
|
+
waterfall: "waterfall";
|
|
25
|
+
justified: "justified";
|
|
26
|
+
}>>;
|
|
27
|
+
columnBreakpoints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
28
|
+
targetRowHeight: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
gap: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
panoramaThreshold: z.ZodOptional<z.ZodNumber>;
|
|
31
|
+
}, z.core.$strip>>, z.ZodTransform<{
|
|
32
|
+
columnBreakpoints: {
|
|
33
|
+
[k: string]: number;
|
|
34
|
+
} | undefined;
|
|
35
|
+
mode?: "wall" | "waterfall" | "justified" | undefined;
|
|
36
|
+
targetRowHeight?: number | undefined;
|
|
37
|
+
gap?: number | undefined;
|
|
38
|
+
panoramaThreshold?: number | undefined;
|
|
39
|
+
}, {
|
|
40
|
+
mode?: "wall" | "waterfall" | "justified" | undefined;
|
|
41
|
+
columnBreakpoints?: Record<string, number> | undefined;
|
|
42
|
+
targetRowHeight?: number | undefined;
|
|
43
|
+
gap?: number | undefined;
|
|
44
|
+
panoramaThreshold?: number | undefined;
|
|
45
|
+
}>>>;
|
|
21
46
|
}, z.core.$strip>;
|
|
22
47
|
export type ConfigType = z.infer<typeof ConfigSchema>;
|
|
23
48
|
export declare const walkUntilFound: (path: string) => Promise<string | undefined>;
|
|
@@ -32,6 +57,15 @@ export declare const load: (configDir?: string | undefined) => Promise<{
|
|
|
32
57
|
variants: number[];
|
|
33
58
|
resize_gamma?: number | undefined;
|
|
34
59
|
};
|
|
60
|
+
gallery: {
|
|
61
|
+
columnBreakpoints: {
|
|
62
|
+
[k: string]: number;
|
|
63
|
+
} | undefined;
|
|
64
|
+
mode?: "wall" | "waterfall" | "justified" | undefined;
|
|
65
|
+
targetRowHeight?: number | undefined;
|
|
66
|
+
gap?: number | undefined;
|
|
67
|
+
panoramaThreshold?: number | undefined;
|
|
68
|
+
};
|
|
35
69
|
} | {
|
|
36
70
|
src: string;
|
|
37
71
|
base: {
|
|
@@ -44,6 +78,15 @@ export declare const load: (configDir?: string | undefined) => Promise<{
|
|
|
44
78
|
variants: number[];
|
|
45
79
|
resize_gamma?: number | undefined;
|
|
46
80
|
};
|
|
81
|
+
gallery: {
|
|
82
|
+
columnBreakpoints: {
|
|
83
|
+
[k: string]: number;
|
|
84
|
+
} | undefined;
|
|
85
|
+
mode?: "wall" | "waterfall" | "justified" | undefined;
|
|
86
|
+
targetRowHeight?: number | undefined;
|
|
87
|
+
gap?: number | undefined;
|
|
88
|
+
panoramaThreshold?: number | undefined;
|
|
89
|
+
};
|
|
47
90
|
}>;
|
|
48
91
|
export declare const toToml: (config: ConfigType & {
|
|
49
92
|
src?: string;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { stringify } from 'smol-toml';
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { loadConfig } from 'zod-config';
|
|
7
7
|
import { tomlAdapter } from 'zod-config/toml-adapter';
|
|
8
|
+
import { GalleryConfigSchema } from './gallery/config.js';
|
|
8
9
|
import { ProcessorConfigSchema } from './processor/config.js';
|
|
9
10
|
const BaseConfigSchema = z.object({
|
|
10
11
|
root: z.string().default(cwd()),
|
|
@@ -14,6 +15,7 @@ const BaseConfigSchema = z.object({
|
|
|
14
15
|
const ConfigSchema = z.object({
|
|
15
16
|
base: BaseConfigSchema.prefault({}),
|
|
16
17
|
processor: ProcessorConfigSchema.prefault({}),
|
|
18
|
+
gallery: GalleryConfigSchema.prefault({}),
|
|
17
19
|
});
|
|
18
20
|
export const walkUntilFound = async (path) => {
|
|
19
21
|
try {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for gallery configuration in .zone5.toml
|
|
4
|
+
*
|
|
5
|
+
* These values override the component defaults.
|
|
6
|
+
*/
|
|
7
|
+
export declare const GalleryConfigSchema: z.ZodPipe<z.ZodPrefault<z.ZodObject<{
|
|
8
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
wall: "wall";
|
|
10
|
+
waterfall: "waterfall";
|
|
11
|
+
justified: "justified";
|
|
12
|
+
}>>;
|
|
13
|
+
columnBreakpoints: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
14
|
+
targetRowHeight: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
gap: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
panoramaThreshold: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
}, z.core.$strip>>, z.ZodTransform<{
|
|
18
|
+
columnBreakpoints: {
|
|
19
|
+
[k: string]: number;
|
|
20
|
+
} | undefined;
|
|
21
|
+
mode?: "wall" | "waterfall" | "justified" | undefined;
|
|
22
|
+
targetRowHeight?: number | undefined;
|
|
23
|
+
gap?: number | undefined;
|
|
24
|
+
panoramaThreshold?: number | undefined;
|
|
25
|
+
}, {
|
|
26
|
+
mode?: "wall" | "waterfall" | "justified" | undefined;
|
|
27
|
+
columnBreakpoints?: Record<string, number> | undefined;
|
|
28
|
+
targetRowHeight?: number | undefined;
|
|
29
|
+
gap?: number | undefined;
|
|
30
|
+
panoramaThreshold?: number | undefined;
|
|
31
|
+
}>>;
|
|
32
|
+
export type GalleryConfig = z.output<typeof GalleryConfigSchema>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Schema for gallery configuration in .zone5.toml
|
|
4
|
+
*
|
|
5
|
+
* These values override the component defaults.
|
|
6
|
+
*/
|
|
7
|
+
export const GalleryConfigSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
/** Default gallery layout mode */
|
|
10
|
+
mode: z.enum(['wall', 'waterfall', 'justified']).optional(),
|
|
11
|
+
/** Viewport width to column count mapping for waterfall mode */
|
|
12
|
+
columnBreakpoints: z.record(z.string(), z.number().int().positive()).optional(),
|
|
13
|
+
/** Target row height in pixels for justified mode */
|
|
14
|
+
targetRowHeight: z.number().int().positive().optional(),
|
|
15
|
+
/** Gap between images in pixels for justified mode */
|
|
16
|
+
gap: z.number().int().nonnegative().optional(),
|
|
17
|
+
/** Aspect ratio threshold for panoramic images in justified mode */
|
|
18
|
+
panoramaThreshold: z.number().positive().optional(),
|
|
19
|
+
})
|
|
20
|
+
.prefault({})
|
|
21
|
+
.transform((data) => ({
|
|
22
|
+
...data,
|
|
23
|
+
// Convert string keys to numbers for columnBreakpoints
|
|
24
|
+
columnBreakpoints: data.columnBreakpoints
|
|
25
|
+
? Object.fromEntries(Object.entries(data.columnBreakpoints).map(([k, v]) => [parseInt(k, 10), v]))
|
|
26
|
+
: undefined,
|
|
27
|
+
}));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { load } from './config.js';
|
|
2
2
|
export type { BaseConfigType, ConfigType } from './config.js';
|
|
3
|
+
export type { GalleryConfig } from './gallery/config.js';
|
|
3
4
|
export { default, default as processor } from './processor/index.js';
|
|
4
5
|
export type * from './processor/index.js';
|
|
5
6
|
export type * from './components/types.js';
|
package/dist/remark.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Plugin } from 'unified';
|
|
2
|
+
import type { GalleryConfig } from './gallery/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the remarkZ5Images plugin
|
|
5
|
+
*/
|
|
6
|
+
export interface RemarkZ5ImagesOptions {
|
|
7
|
+
/** Gallery configuration from .zone5.toml */
|
|
8
|
+
gallery?: GalleryConfig;
|
|
9
|
+
}
|
|
2
10
|
/**
|
|
3
11
|
* Remark plugin to process Zone5 images
|
|
12
|
+
*
|
|
13
|
+
* @param options - Plugin options including gallery config from .zone5.toml
|
|
4
14
|
*/
|
|
5
|
-
export declare const remarkZ5Images: Plugin
|
|
15
|
+
export declare const remarkZ5Images: Plugin<[RemarkZ5ImagesOptions?]>;
|
package/dist/remark.js
CHANGED
|
@@ -25,10 +25,56 @@ function buildImagesExpression(imageData) {
|
|
|
25
25
|
const imageExpressions = imageData.map((img) => `{...${img.key}, properties: {...${img.key}.properties, alt: ${JSON.stringify(img.alt)}, title: ${JSON.stringify(img.title)}}}`);
|
|
26
26
|
return '[' + imageExpressions.join(',') + ']';
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a string property node
|
|
30
|
+
*/
|
|
31
|
+
function createStringProperty(name, value) {
|
|
32
|
+
return {
|
|
33
|
+
type: 'svelteProperty',
|
|
34
|
+
name,
|
|
35
|
+
shorthand: 'none',
|
|
36
|
+
value: [{ type: 'text', value }],
|
|
37
|
+
modifiers: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a numeric property node (as expression)
|
|
42
|
+
*/
|
|
43
|
+
function createNumberProperty(name, value) {
|
|
44
|
+
return {
|
|
45
|
+
type: 'svelteProperty',
|
|
46
|
+
name,
|
|
47
|
+
shorthand: 'none',
|
|
48
|
+
value: [
|
|
49
|
+
{
|
|
50
|
+
type: 'svelteDynamicContent',
|
|
51
|
+
expression: { type: 'svelteExpression', value: String(value) },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
modifiers: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Create an object property node (as expression)
|
|
59
|
+
*/
|
|
60
|
+
function createObjectProperty(name, value) {
|
|
61
|
+
return {
|
|
62
|
+
type: 'svelteProperty',
|
|
63
|
+
name,
|
|
64
|
+
shorthand: 'none',
|
|
65
|
+
value: [
|
|
66
|
+
{
|
|
67
|
+
type: 'svelteDynamicContent',
|
|
68
|
+
expression: { type: 'svelteExpression', value: JSON.stringify(value) },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
modifiers: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
28
74
|
/**
|
|
29
75
|
* Create a Zone5 Svelte component node
|
|
30
76
|
*/
|
|
31
|
-
function createZone5Component(imageData,
|
|
77
|
+
function createZone5Component(imageData, options) {
|
|
32
78
|
const properties = [
|
|
33
79
|
{
|
|
34
80
|
type: 'svelteProperty',
|
|
@@ -46,19 +92,17 @@ function createZone5Component(imageData, mode) {
|
|
|
46
92
|
modifiers: [],
|
|
47
93
|
},
|
|
48
94
|
];
|
|
49
|
-
if (mode) {
|
|
50
|
-
properties.push(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
modifiers: [],
|
|
61
|
-
});
|
|
95
|
+
if (options.mode) {
|
|
96
|
+
properties.push(createStringProperty('mode', options.mode));
|
|
97
|
+
}
|
|
98
|
+
if (options.columnBreakpoints) {
|
|
99
|
+
properties.push(createObjectProperty('columnBreakpoints', options.columnBreakpoints));
|
|
100
|
+
}
|
|
101
|
+
if (options.targetRowHeight !== undefined) {
|
|
102
|
+
properties.push(createNumberProperty('targetRowHeight', options.targetRowHeight));
|
|
103
|
+
}
|
|
104
|
+
if (options.gap !== undefined) {
|
|
105
|
+
properties.push(createNumberProperty('gap', options.gap));
|
|
62
106
|
}
|
|
63
107
|
return {
|
|
64
108
|
type: 'svelteComponent',
|
|
@@ -174,14 +218,26 @@ function createScriptElement(importMap) {
|
|
|
174
218
|
}
|
|
175
219
|
/**
|
|
176
220
|
* Remark plugin to process Zone5 images
|
|
221
|
+
*
|
|
222
|
+
* @param options - Plugin options including gallery config from .zone5.toml
|
|
177
223
|
*/
|
|
178
|
-
export const remarkZ5Images = () => {
|
|
224
|
+
export const remarkZ5Images = (options = {}) => {
|
|
225
|
+
const galleryConfig = (options.gallery ?? {});
|
|
179
226
|
return (tree, file) => {
|
|
180
227
|
const rootTree = tree;
|
|
181
228
|
const importMap = {};
|
|
182
229
|
const existingKeys = new Set();
|
|
230
|
+
// Get frontmatter values (these override config)
|
|
183
231
|
const fm = file.data.fm;
|
|
184
|
-
const
|
|
232
|
+
const frontmatterMode = fm?.zone5mode;
|
|
233
|
+
// Merge config with frontmatter (frontmatter takes precedence)
|
|
234
|
+
const resolvedOptions = {
|
|
235
|
+
mode: frontmatterMode ?? galleryConfig.mode,
|
|
236
|
+
columnBreakpoints: galleryConfig.columnBreakpoints,
|
|
237
|
+
targetRowHeight: galleryConfig.targetRowHeight,
|
|
238
|
+
gap: galleryConfig.gap,
|
|
239
|
+
panoramaThreshold: galleryConfig.panoramaThreshold,
|
|
240
|
+
};
|
|
185
241
|
// First, collect all Z5 images for the import map
|
|
186
242
|
visit(rootTree, 'image', (node) => {
|
|
187
243
|
if (isZ5Image(node)) {
|
|
@@ -199,7 +255,7 @@ export const remarkZ5Images = () => {
|
|
|
199
255
|
const paragraph = child;
|
|
200
256
|
const z5Images = paragraph.children.filter((ch) => isZ5Image(ch));
|
|
201
257
|
const imageData = collectImageData(z5Images, existingKeys);
|
|
202
|
-
const svelteComponent = createZone5Component(imageData,
|
|
258
|
+
const svelteComponent = createZone5Component(imageData, resolvedOptions);
|
|
203
259
|
// Replace the multi-image paragraph with the svelte component
|
|
204
260
|
node.children.splice(i, 1, svelteComponent);
|
|
205
261
|
}
|
|
@@ -214,7 +270,7 @@ export const remarkZ5Images = () => {
|
|
|
214
270
|
.map((index) => getImageFromParagraph(node.children[index]))
|
|
215
271
|
.filter((img) => img !== null);
|
|
216
272
|
const imageData = collectImageData(imageNodes, existingKeys);
|
|
217
|
-
const svelteComponent = createZone5Component(imageData,
|
|
273
|
+
const svelteComponent = createZone5Component(imageData, resolvedOptions);
|
|
218
274
|
// Calculate how many nodes to remove (including newlines)
|
|
219
275
|
const startIndex = group[0];
|
|
220
276
|
const endIndex = group[group.length - 1];
|
|
@@ -234,7 +290,7 @@ export const remarkZ5Images = () => {
|
|
|
234
290
|
if (isZ5ImageParagraph(child) && child.type === 'paragraph') {
|
|
235
291
|
const imageNode = child.children[0];
|
|
236
292
|
const imageData = collectImageData([imageNode], existingKeys);
|
|
237
|
-
const svelteComponent = createZone5Component(imageData,
|
|
293
|
+
const svelteComponent = createZone5Component(imageData, resolvedOptions);
|
|
238
294
|
// Replace the single image paragraph with the svelte component
|
|
239
295
|
node.children.splice(i, 1, svelteComponent);
|
|
240
296
|
}
|